Open3

PowerShellを布教するためのなにか~シエル女子学園シリーズ~

iranikairanika

何故こんなものを書いているのか

PowerShellの魅力を何度も説明するのに疲れたので、URL投げつければ済むようにしたいと思った。

端的に言えば

  1. PowerShellはシェルとしても強いのに
  2. .Netのライブラリが呼び出せるからC#で出来ることは大抵できるし
  3. シェルなのに文字列以外の型があり
  4. オブジェクト指向や関数型のマルチパラダイムでも(広義の)シェルスクリプトが書ける上に
  5. コマンドライン引数のパーサーが構文レベルで組み込まれていて宣言的に書けるから
  6. 《PowerShellはシェルとしても、スクリプト言語としても強い》

という話。

もちろん向き不向きはある。PowerShellはシェルなので。
ただ、PowerShellはシェルとしてもスクリプト言語としても強力だということを知ってほしい。
だから、入門書ではなく「PowerShellの何が嬉しいのか」を布教するためにこれを書くことにした。
入門書をお求めの方には、吉崎さんの「PowerShell実践ガイドブック」をオススメします。素晴らしい良書です。

何故PowerShellなのか

私はPowerShell以外でもプログラミングをします。
Nim/Rust/C#/Typescript/BashScript/etc...

そんな中で、なぜPowerShellを使うのかと聞かれたら「プログラミング言語としても書きやすいし、シェルとしてもインターフェースとしても優秀だから」と答えます。

百聞は一見にしかずなので、サンプルのスクリプトを見てもらったほうが早い。

# Get-GlobalIPAddr.ps1
[CmdletBinding()]
param (
    [string]$url = "https://api.ipify.org?format=json",
    [switch]$ToJson,
    [switch]$ToObject
)

# $urlのAPIが返すJSONがObjectとして$resに格納される
$res = Invoke-RestMethod $url

if ($ToJson){
    return $res | ConvertTo-Json # Jsonとして出力
}elseif ($ToObject){
    return $res # PSCustomObjectとして出力
}else{
    return $res.ip # IPだけ出力
}

<# 出力の様子
PS [NIKA-DESKTOP]> .\Get-GlobalIP.ps1
192.0.2.0
PS [NIKA-DESKTOP]> .\Get-GlobalIP.ps1 -ToJson
{
  "ip": "192.0.2.0"
}
PS [NIKA-DESKTOP]> .\Get-GlobalIP.ps1 -ToObject

ip
--
192.0.2.0

PS [NIKA-DESKTOP]>
PS [NIKA-DESKTOP]> (.\Get-GlobalIP.ps1 -ToObject).GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    PSCustomObject                           System.Object

PS [NIKA-DESKTOP]>
#>

これだけで、IPアドレスをJSON/PSObject/Textで出力するスクリプトの完成。
param (...)は引数についての構文で、中で変数を宣言することで勝手にパーサーみたいなことをしてくれる。型指定等も宣言的で良い。[Paramator(Mandatory)]をつければ必須オプションにすることもできる。
もちろん、PowerShellは補完が効くので入力-tocくらいでタブキー押せば-ToJsonに補完してくれる。

Unix哲学の中に「シェルスクリプトによって梃子の効果と移植性を高める」という教えがあるが、PowerShell Scriptも広義のシェルスクリプトなのでソフトウェア同士を繋げて梃子の効果を高めることが出来る。shやBashも素敵だが、PowerShellは.Netというライブラリ資産を活用しやすいだけでなく、後述するConvertTo-Jsonなどデータ形式のシリアライズやデシリアライズのコマンドレットが標準で組み込まれているため、生成したデータオブジェクトをJsonやCSV等で出力しやすいという魅力がある。もちろん読み込みも。
また、ソフトウェア同士をつなげるという点でPowerShellはREST APIのCLIとしても優秀でsh系シェルでありがちな、システムにjqが入っているかどうかで悩まなくていい。
移植性については賛否があると思うが、PowerShellのテキスト出力をBashでアレコレすることもできるので私は十分だと考えている。

話が逸れたが、要はさくっとツールを作るのに便利で強力な《シェルスクリプトが書ける》ので私はPowerShellを好んでいる。

何が嬉しいのか

<ここに何が嬉しいのか書く>

よくある疑問・誤解

Q.Windowsでしか動かないでしょ

PowerShell(>=6.0)はマルチプラットフォームです。
Windows/Linux系/MacOSでも動きます。
WindowsPowerShell(<=5.1)とPoweShellはもはや別物です。

AWS LambdaなどのFaaSでもPowerShellは広くサポートされています。
クラウドシェルの多くがLinux上で動くPowerShellです。

Q.curlコマンドとか使えないでしょ?

シェルなので実行ファイルにパスが通っていたら使える。Linux系でもパス通っていたらlsとかsedとかshで使えるコマンドが再利用できる。そこがシェルのいいところ。

iranikairanika

【閑話】部室にて

「先輩、何してるんですか」

《あさひ》がノートPCの画面を肩越しに覗き込んできたので、《まどか》は動かしていた手を止めてマナーのなっていない後輩の頭頂部にチョップを叩き込んだ。

「あさひ、ショルダーハックはやめろって何度も言っているだろ」

「すみません、まどか先輩……つい癖で……」

あさひは両手で頭頂部を擦りながら「それよりも――」と話の続けた。

「なんですか、この『PowerShellを布教するためのなにか』って」

「新入部員にPowerShellの魅力を説明するのも疲れてきたから記事に纏めようと思って」

「へぇ……でも文章が固いですね」

「仕方ないだろ。こういう性格なんだから」

あさひは書きかけの記事を読みながら「ふむふむ」と唸っている。

「先輩、これ私も記事書いていいですか?」

「構わないが、何を書くんだ?」

「AngleParseの記事です」

ーーーー

iranikairanika

AngleParse

AngleParseはHTMLを簡単にパースするためのモジュールです。
詳細は作者のkamome283さんの解説記事を読んだほうが早いです。

https://qiita.com/kamome283/items/5b976a27ed203e959b09

それでは、AngleParseでDLsiteのランキングを抽出するコードを見てみましょう。
※この記事はスクレイピングを推奨するものではありません。

get-ranking.ps1


function Get-Ranking {
    param(
        [switch]$Brief,
        [switch]$HtmlOnly,
        [switch]$DebugShowBody, # -debug オプションと一緒に使ってください
        [Parameter(ValueFromPipeline)]$body = "",
        $Floor = "home",
        $Category = "voice", #game, comic, voice, 空白は総合ランキング
        $term = "week",
        $sort = "sale", #空白は人気順,販売数順はsale
        $Limit30d = "30d"
    )

    process {
        Import-Module AngleParse
        if ($body -eq ""){
            #TODO: URL生成をパラメーターでやる
            #https://www.dlsite.com/home/ranking/week?category=voice&date=30d&sort=sale
            $url = "https://www.dlsite.com/$($Floor)/ranking/$($term)?category=$($category)&sort=$($sort)&date=$($Limit30d)"
            Write-Debug "body was not input.`n $url"
            $body = (Invoke-WebRequest $url).Content
        }else{
            Write-Debug "body is inputed."
        }
        if ($DebugShowBody) { Write-Debug $body }
        if ($HtmlOnly) { return $body.Content } # HTMLだけを返すモード
    
        $r_asmr = $body | Select-HtmlContent "#ranking_table > tbody > tr" , @{
            "サークル" = "td > dl > dd.maker_name > a"
            work_name = "td > dl > dt > a"
            work_id = "td > dl > dt > a", [AngleParse.Attr]::Href, { $_ -replace "(https://.*/product_id/|\.html)","" }
            work_price = "td > dl > dd.work_price_wrap > span.work_price", { $_ -replace "(円|,)","" }
            discount = "td > dl > dd.work_price_wrap > span.work_price.discount", { $_ -replace "(円|,)","" }
            star_count = "li.work_rating > div", { $_ -replace "(\(|\)|,)","" }
            star_rate = "td > ul > li.work_rating > div", [AngleParse.Attr]::Class, { (($_ -replace "star_rating","") -replace "star_","").Trim() }
            review_count = "td > ul > li.work_review > span > div > a",{ $_ -replace "(\(|\)|,)","" }
            work_dl = "td > ul > li.work_dl > div > span", { $_ -replace "(\(|\)|,)","" }
            sales_date = "td > ul > li.sales_date", { ((($_ -replace "販売日:","") -replace "(年|月)","-") -replace "日","").Trim() }
            "声優" = "td > dl > dd.maker_name > span.author > a"
            "ジャンル" = "td > dl > dd.search_tag > a"
            rank_no = "td.ranking_count > div > div.rank_no"
        }
    
        if ($Brief){
            return $r_asmr | Select-Object -Property work_name,work_price,circle_name,star_count
        }else{
            return $r_asmr            
        }
    }

}

実行結果の様子はこんな感じです。

PS :>. .\get-ranking.ps1
PS :> (Get-Ranking -sort "sale" -Debug)[0..3]
DEBUG: body was not input.
 https://www.dlsite.com/home/ranking/week?category=voice&sort=sale&date=30d

star_count   : 2610
ジャンル     : {萌え, 癒し, バイノーラル/ダミヘ, ASMR}
声優         : 春花らん
star_rate    : 50
work_id      : RJ403038
work_name    : 【ブルーアーカイブ】ユウカASMR~頑張るあなたのすぐそばに~
サークル     : Yostar
discount     : 1188
work_price   : 1188
rank_no      : 1
work_dl      : 19811
review_count : 41
sales_date   : 2022-07-17

star_count   : 188
ジャンル     : {健全, 癒し, ASMR, ロリ…}
声優         : 浅見ゆい
star_rate    : 50
work_id      : RJ404647
work_name    : 砂塵荒野と草原の国の少女 放浪系安眠音声作品【CV:浅見ゆいさま/2時間34分】
サークル     : チームランドセル
discount     : 1056
work_price   : 1056
rank_no      : 2
work_dl      : 1067
review_count : 2
sales_date   : 2022-07-28

star_count   : 661
ジャンル     : {萌え, 癒し, バイノーラル/ダミヘ, ASMR…}
声優         : 一之瀬りと
star_rate    : 50
work_id      : RJ400655
work_name    : お耳癒やしエステサロンへようこそ
サークル     : いちのや
discount     : 1056
work_price   : 1056
rank_no      : 3
work_dl      : 3230
review_count : 11
sales_date   : 2022-07-20

セレクタパイプライン

AngleParseではセレクタパイプラインによって、データ加工がしやすくなっています。

例えばwork_idの抽出処理を見てみましょう。

work_id = "td > dl > dt > a", [AngleParse.Attr]::Href, { $_ -replace "(https://.*/product_id/|\.html)","" }

これは、以下のようなイメージでデータの加工が行われていきます。

"td > dl > dt > a" | [AngleParse.Attr]::Href | { $_ -replace "(https://.*/product_id/|\.html)","" }