tech.guitarrapc.cóm

Technical updates

XAMLを書くときに使っている拡張機能

UWP アプリを書く必要があり、何度か挫折したXAMLに真剣に取り組んでいます。

今回は、XAMLを書くにあたって自分が導入している拡張機能とめちゃめちゃ便利なアプリの紹介です。

目次

拡張機能

次の2つをいれることで、XAML に悩まされることが減ったのでお勧めです。 ときにStylerは、いわゆる自動的なフォーマッターでオススメです。 私がこういう拡張をいれる時に共通しているのは、「機械的に自動化されるのに慣れるのは大事」ということで、フォーマッターはその中で優先して導入すると効果があると感じます。

marketplace.visualstudio.com

marketplace.visualstudio.com

XamlStyler

XAML を書いていて面倒だと感じるのが、コントロールの要素が読みにくいことです。 その原因は、自分にとっては「コントロールごとにプロパティの要素が違ったり、並び順が異なる」ことにあります。 しかし、膨大な数のプロパティとコントロールは自分の好みにあうように並び替えたり管理しきれません、やりたくないです。 管理できないものは「保存時に自動フォーマットをかけて、それに慣れる」と考え事が減るので、自動フォーマットする拡張機能としてXamlStylerを導入しました。

この拡張は@nuits_jpさんに教えていただきました、ありがとうございます!

Visual Studioの拡張機能で導入でき、ソースもGitHub で公開されています。*1

github.com

拡張をいれると、XAMLを開いて保存(Ctrl + s) するだけで自動的にフォーマットしてくれます。 例えば今までいれてなかったXAMLで適用すると、こんな感じでフォーマットされました。

VS拡張の弱点は各自のVS環境に依存することですが、XamlStylerは外部Configurationで設定できます。 Settings.XamlStyler としてプロジェクトに配置しておきましょう。 これでVSの設定よりも優先されるので、チームで統一するにはちょうどいい感じです。

次のコミットで、明示的に制御するために追加しました。

github.com

Script Integrationとしてコンソールアプリもあるので、適当にCIでフォーマットかけることもできます。が、まぁいったんいいでしょう。.NET 4.5.2 だし、Linux/macOS で動かないし。

github.com

Inline Color Picker

XAML上で色設定時にインラインでカラーピッカーを出してくれるColor Picker拡張です。

UIが古いのですが、VS CodeなどでCSS書いていてよくあるアレなのであって便利です。

XAML 記述の確認

ドキュメント優先、よりも動かして目で見てXAMLを確認する方が圧倒的に理解も手も動かしやすいです。

そこで、次の3つのUWPアプリを片隅に表示させて開発しています。

普段からXAML Controls Galleryを開いています。 通常のコントロールの利用方法に悩むことが格段に減ったおすすめアプリです。

このアプリは@okazuki さんに教えていただきました!ありがとうございます。

Windows UI Library が2018年8月に発表され、そのnugetライブラリMicrosoft.UI.Xaml を使ったXAML実装サンプルアプリです。 XAMLで表現できるFluent Design 向けのコントロールのサンプル実装が目的らしいので、ちょうどほしかったやつという感じです。

Getting Started が公開されています。

github.com docs.microsoft.com

アプリはMicrosoft Store でインストールできます。

www.microsoft.com

また、アプリのサンプル実装のソースコードも公開されています。(Microsoft.UI.Xaml使用は、devブランチ参照)

github.com

アプリは標準コントロールの利用例ですが、ソースコードと一緒にみれるのでかなり助かります。

WindowsCompositionSamples

Windows.UI.Xaml を使った最新のデモとコードの確認ができます。

ソースコードも公開されています。

github.com

こちらも Microsoft Storeでインストールできます。

www.microsoft.com

WindowsCommunityToolkit

コミュニティによる、UWPアプリ作成時に使う拡張です。 標準にない便利なコントロールや拡張が多くあります。

ソースコードも公開されています。

github.com

また、MicrosoftからGetting Started が公開されています。

docs.microsoft.com

アプリはMicrosoft SToreでインストールできます。

www.microsoft.com

微妙に使いにくい感じがするので、もうちょっと触ってみないとばっちりかどうかはわからないです。

まとめ

自分にとってXAMLで一番困る挫折ポイントがコントロールや記述がしっくりこないことだったので、拡張やアプリを使ってまじめに取り込んでいます。 さすがにUnityやXcodeやった後だと以前より苦しみが少ないのですが、XAMLは改めてみても独特というかViewをいかに分離できるかを念頭に組まれている感じが強いです。

そのための仕組みとしてのコードビハインド、正確には双方向バインディングが機能としてあるのはかなり強力ですが、XAML独自の記述がネックに感じます。*2 VMでReactivePropertyを使うと、 MVRPと似たコードの雰囲気になります。 結果、違いとなるXAMLとのバインディングを把握すればいつもと変わらないコードの書き味になるように感じるので、この路線でいくのが性に合ってそうです。

そういえば、ReactiveProperty にこのPRが取り込まれると Asyncとの相性が抜群によくなるのではよ.... (困ってたらPR作っていただきました、神か

github.com

XAMLは4Kで多きめのモニタで書いていると効率明らかにいいので、なるほどUnityと同じ....という感じです。

*1:ドキュメントもGitHub参照で

*2:仕組みを理解するのが近道なのはどのフレームワークでも同様ですね

IFTTT と Zapier と Integromat それぞれでGoogle Formsの回答をSlackに通知してみる

「Google Forms に入力があったらSlack *1に通知したい」

今回の記事は、IFTTTやZapier、あるいはIntegromatを使って通知を行う方法についてです。

目次

動機

Google Forms を外部に公開したり、内部に公開して回答を受けてアクションすることは、度々あります。 そして、そのたびに「気づけないと意味がない、でも気付くためにSlackに通知するにはどうすれば楽にできるのか」を悩み勝ちです。

これまでも、Google App Script(GAS)を書いて通知、GASからServerlessにWebhookを投げて通知などをやっていましたが、いずれもコードが必要なためノンプログラマーがカジュアルにやるには苦しいです。 そして、コードでやる以上GitHubで管理したいですが、GASのデプロイとか考えたくありません。 コードを書かない、コードで書く必要がないという選択が必要なシーンも多いので、考えてみましょう。*2

seleck.cc

誰であっても障壁を感じることなく*3、自分の仕事を自動化で楽にして、時間が作れずにできなった本当にやりたいことに注力できる土壌や文化はあってしかるべきだと思っています。

ということで選択するのがいわゆるGlueサービス、と呼ばれるインターネット上の各種サービスをノンプログラミング/レスプログラミングでつなぎ合わせるサービスである、IFTTTやZapier、Integromatです。

普段はどれを使っているのか

私自身は、個人のSlackではIFTTTをメインに構築して、会社としてはZapierを使っていました。

用途 メイン サブ
個人 IFTTT -
会社 Zapier -

これは、次の理由からです。

  • 個人 : シンプルな操作が多く、IFTTTの方が設定の手間が少ない。また、使っているPhilips HueなどがIFTTTにしかない
  • 法人 : SNSからの連携、ChatworkなどIFTTTではできず、複数のZapをかませたいなどZapierではないとできないサービスや処理がある

また、個人としても法人としても「同系統サービスはなるべく1つのサービスで済むなら集約させたい」という思いもあり絞っていました。

Integromatの利用

Integromatは去年から見ていたのですが、いまいち使いたいサービスがなく、すでにIFTTT、Zapierで組んでいたのでスルーしていました。 ZapierやIFTTTに感じていた2つのストレス、「視認性の悪さ」、「ちょっとしたことですぐに処理を挟む必要性」、「Zapierの課金の高さ」を解消できそうなので、Integromatも用いてみました。

用途 メイン サブ
個人 Integromat -
会社 Integromat -

ただし、3日使ってみて Basic($9課金)もした結果、メインをIntegromatにすることはやめてIFTTT/Zapierと併用しています。

用途 メイン サブ
個人 IFTTT Integromat
会社 Zapier Integromat

https://www.integromat.com/en/pricingwww.integromat.com

利用サービスが増えましたが、IFTTT/Zapierでできなかったことを任せられるので、用途を定めればいいかと判断しました。*4

Integromatの強さ

Integromatは、かなり理想的なサービスと思っています。 特に次のポイントを評価しています。

  • ifがなどのフィルタ時でもシナリオのUIが分かりやすい*5
  • 組み込みのパーサーや配列ハンドリングがある
  • 実行分だけとすると無料で1000op/monthという破格さ
  • 課金もBasic $9~とZapierより安い
  • Google SpreatSheetへの書き戻しなど処理が強力

非エンジニアから、シナリオUIが可愛い、理解しやすいと高評価だったことも好ましいと思っています。Zapierは繋ぐとカオスなのですごくいいですね。

↓は2つのmoduleをつなぐUIですが、可愛さです。

Integromatをメインにしない理由

Integromatをメインからサブに戻した理由は、「実際に処理の必要な更新がなく、実行しない場合でもオペレーションが消費される」という特性を使って気付いたからです。 ZapierやIFTTTは、対象サービスの対象処理が実行された時にだけ「Zapなどのオペレーションを消費」するのに対して、Integromatは「スケジュール実行でポーリングする場合、実行されていなくてもスカシ実行でもオペレーションを消費」します。 計算すると、Integromatは通常の15分に一度のスケジュールでも96op/day 消費します。*6 15分スケジュールだと、無料アカウント(1000op/month)でひと月持たず、Basic(10000op/month)でも3ジョブ程度でしょう。「InstantやWebhookがない」けど「高頻度スケジュールで実行したい処理」はIntegromatにあまり向いていないと判断しました。*7

実際に使い始めてオペレーションを消費している原因を把握して、構成を組みなおすまでのOperation消費は次の通りです。

4シナリオで15分頻度スケジュールを実行した結果、一日で388使っているのが分かります。*8

例えば、Google Formsの新規入力があったらSlackやメールに通知、返信、などの処理はなるべくリアルタイムに処理したく15分に一度に設定するとあっという間にいっぱいになります。

IntegromatとZapier/IFTTTの利用用途

Integromatは実行分だけOperationを消費することが確定する処理にのみ利用することにして、次の用途と定めました。

  • Webhook を受けて実行する処理
  • Schedule に Instantがある処理
  • 1op/week、1op/month、1op/yearなど非常に低頻度に繰り返す処理
  • Router/Aggregatorなどを用いて分岐する処理

IFTTT/Zapierは、Integromatで処理が向いていない他の用途に使っています。

  • 対象サービスの特定のオペレーションを起点に実行される処理
  • HueやSNSなどIntegromatで処理しにくい処理

自動化を組むときは、まずIntegromatでInstantに処理できるか/Webhookかを確認、出来なければIFTTT/Zapierという順に利用しています。

Google Forms の回答をSlackに通知してみる

余談が長くなりました。

当初はIntegromatのみの記事を書くつもりでしたが、結局先述の理由でIFTTT/Zapierで組んでいます。

Google Forms は次の構成です。

回答先のスプレッドシートを設定しておきます。

結論どれで組むといいのか

ZapierかIFTTTがおすすめです。

この記事の用途でIntegromatは向いていません。理由は先述の通りです。

ただし、Webhook経由でトリガーが発動して、SpreadSheetに書き込み、Slackに通知、Twitterにポストを行って.... など、InstantなTriggerを起点に処理が実行されるならIntegromatが最高です。

Integromat

シナリオは、Google Forms -> Slackをつなぎます。

まずは、Google Forms モジュールを選択します。

Google Forms Moduleは、SpreadSheetの更新を検知させることができます。

フォームと回答のスプレッドシートにアクセスできるGoogleアカウントで接続し、ファイルとワークシートを選択します。

行は全てでいいでしょう。Formsからの自動生成なら先頭行はカラム名のはずです。

次はSlack通知をつなげます。Add another module でSlackをつなげましょう。

Create a messageでメッセージを送れます。Create a Postだとリンク先に飛べなかったのでMessageでいいかと思います。

あとはメッセージを送るChannelとメッセージを組みます。どのような値が来るかは、直前のGoogle Forms ModuleからのFetchによって可視化されています。

メッセージを作るときには、Text、Math、Date、配列操作、Parseなど強力な関数が用意されているのでZapierのような関数を組む必要がほぼありません。

一度保存します。

試しに今の回答でSlackに飛ばします。Run Onceしてみましょう。

それぞれのModuleに処理数が表示されます。

今回はスプレッドシートに回答がすでに4行あり、Google Forms Moduleの設定時に一度に2行処理するように設定していました。 そのため、Google Forms は1度の処理ですが、Slack Moduleは2度処理されて2つの回答がポストされています。

こういった詳細もわかるのは神ですね。

Slackを確認すると2つメッセージが送られていることが分かります。

あとは確認頻度スケジュール設定です。Integromatは、Zapier/IFTTTと違ってScheduleにInstantがない限りは定期的に実行します。(そしてOperationを消費します)

回答があるたびに実行をなるべく早く送信したいなら、Scheduleを短くすることになりますが、Operationが激しくなるのでものを選ぶのがここです。

Activate にすれば有効になります。

以上で、Integromatの設定は完了です。

ちなみに、Slack送信時にShow Advanced Settings でBotのアイコンや名前などいろいろ調整できます。

Zapier

Zapierの場合は次のように組めます。

Trigger Apps に Google Forms を選択します。

New Responseを選択。

接続するGoogle アカウントを認証してTestします。

トリガー対象のスプレッドシートとワークシートを選びます。

これで完了なので、Fetchして接続を試します。

トリガーを受けたら処理するActionは「Slackへメッセージ送信」です。Slackを選びましょう。

Channel Messageを送ります。

Slackとの接続をテストします。

Slackのメッセージを作ります。Integromat とは選べる内容が異なりますね、行やURLは取れません。*9 送信先のURLはZapierにテキスト埋めなの何とかならないでしょうか。

さぁ、ではテスト実行してみましょう。

Slackに送れていますね。

名前を付けてZapをOnにすれば完了です。

Zapierは、処理が縦に並ぶため複数のStepを組んだ時にどんな流れになるのかが見にくいのかが良くなるといいですね。

IFTTT

IFTTTの場合は、Google Formsではなくスプレッドシートで作成します。

新しい入力検知なので、New row added to the spreadsheetです。

パスかURLでシートを選択します。

あとはthatを選んで、Slackにポストします。

文章を適当に作りましょう。

始めからある程度いい感じの文が入っています。

カラムはプレビューできません。

保存しましょう。

Check now で実行をテストできます。

適当にRowを追加してから実行するとSlackにメッセージが連動します。

これで完了です。

まとめ

Google App Script でコード書いて、という処理に比べるとコーディングがない上にUXがある程度定型化しているのとストレートフォワードなのが分かります。

「誰でもこのような自動化が組め、何かを自動化できそうと気づいたら5分もかからずサクッと組める。」

この良さは、プログラミングをする理由と同様だと思います。 例えプログラミングができなくても、普段「何かをtriggerに手作業で行っていた単純な作業」は自動化できます、誰かにお願いせずとも自分で組める環境は非常に重要です。

もしも簡単に条件付けできるなら、それもZapierやIntegromatで自動化フローに載せられます。*10

例えば、Integromatなら質問1が「オプション 1」の場合はXXXX処理、違うならZZZZもFilterとRouterで簡単です。

ぜひ、普段の提携作業を自動化してしまって、本当にやりたいことをやってください。この記事がそのきっかけになれば幸いです。

*1:Teams でも Chatworkでも置き換えてください

*2:もちろん増えてくると定義をTerraformで~とかInfrastructure as Codeの辿る道がくるのですが

*3:あるいは障壁を意識することなく

*4:Integromatに統一したかったです

*5:英語であることは問題ではない

*6:4op/1hour * 24hour = 96op/day

*7:これらはIFTTT/Zapierで十二分にこなせていることもあり

*8:他にも1op/dayのシナリオをいくつか組んでます

*9:このためZapierでSpreadsheetの再帰書き換えは面倒です

*10:Integraomatの方が簡単に組めます

はてなブログの予約投稿でTwitter投稿をするとFacebookにも投稿されるようにIFTTTを設定する

予約投稿時のFacebookに同時にシェアする機能が廃止されるようです。

staff.hatenablog.com

やりたいことはようは、予約投稿に合わせて自動的にFacebookにも投げられればいいので、IFTTTを使って自動化してみましょう。

前提として、Twitterには投稿しているものとします。

目次

前提

IFTTTのtriggerとしてTwitter検索機能を使うため、以下の条件で発動させます。

  • Twitter投稿は行っている
  • Twitterは鍵垢ではない
  • IFTTTアカウントでFacebook連携しても構わない
  • Facebook投稿はTwitterよりも時間差が生じてもいい(最大15分)
  • #はてなブログ#fb タグを予約投稿時につける

もしも鍵垢の場合、IFTTTで連携しても構わないなら同様の操作が可能です。

通常の投稿時のシェアとの競合回避

#はてなブログタグだけだと、通常の投稿時のシェアタグにデフォルトで含まれて、Facebook投稿をやってしまったときに二重になります。

そこで、IFTTT発動条件のタグとして #fb というタグを追加しました。

公開時のフロー

  1. 記事を書く
  2. 編集オプション > 予約投稿を設定する
    1. Twitterに投稿だけ有効にする
    2. #はてなブログfb タグを有効にする
  3. 記事を予約投稿する

連動結果

Twitter投稿すると、FBにも投稿されます。

なお、#fb タグなしのトリガーに反応させた投稿だとこのようになります。

IFTTTの設定

IFTTTでは以下の設定を行います。

  • Twitter Triggerを選択
  • New tweet from search を選択

  • 検索条件を入力

「Retweetを除外、指定したタグ、アカウント、URLを含む最新のTweet」だけ引っかかります。

#はてなブログ #fb -RT -@アカウント名 はてなブログのURL

  • Facebook投稿は、{{FirstLinkUrl}} のみ

これで完成です。

例えば私ならこうなります。

まとめ

予約ではなく、通常の投稿時のシェアはこれからも使えます。 そして私は一切予約投稿使ってないので、初めてその制約を知りました。

技術的に面白いので、ServerlessやLogicAppを試すのもいいですが、私はこういうのはIFTTT程度で済ませるのが実用的だと思っています。

Fastly Japanが新オフィスに引っ越したと聞いて行ってきた

静的コンテンツだけでなく動的コンテンツもキャッシュしたい、よりユーザーに近いエッジでコンピューティングしたい。そのような時にFastly社の名前が上がってくることが増えてきたと感じます。

先日、Fastly JapanのMio MatsudaさんからFastly日本オフィスを移転したとのことで訪問する機会をいただいたので訪問レポートです。以前サンフランシスコのFastly本社を訪問したのは2016年だったので、2年ぶりの国を変えての訪問になります。

tech.guitarrapc.com

目次

近年のFastly様紹介

Fastly Yamagoya Meetup 2017や、日経新聞さんのCDNをエッジルーティングに使う事例などで話題になったCDNのFastlyさんは、私自身好んで使っているサービスです。

毎年開催されているミートアップ ALTITUDEの内容も興味深いものが多いです。

www.fastly.com

VCLを基盤にしたエッジでのモジュールやディクショナリを行えるSDKの提供に始まり、Realtime everything というコンセプトはCDN側でのロギングや統計をはじめとして「リアルタイムに処理をしたいという要望」に応えてくれます。Fastlyを使っていくには動的な処理をいかにやるか、ということと向き合うことであり、VCLと仲良くやっていくといい感じになります。

www.fastly.com

先日Limited PreviewとしてWAFを出されたことで、IncapsulaでWAF + CDNを、という面もそろそろFastly で対応できそうです。

Web アプリケーションファイアウォール (WAF)(2020) | Fastly ヘルプガイド

Remote Log Streamingに関しても、まだ公式プラグインはないもののDatadog の Log Management との接続ができ、S3やGCS、BigQueryよりもお手軽かつ監視環境に一体化されて良いです。

qiita.com

cloudplatform-jp.googleblog.com

個人的には、GraphQLをはじめとして「API結果をエッジにキャッシュがありな設計」ならFastlyを使うのもありだと感じています。

https://blog.apollographql.com/caching-graphql-results-in-your-cdn-54299832b8e2

また、Datadog のFastly Plugin も機能リクエストした結果、Service Account名をAPIキーごとにつけてチャートに出せるようになるなど、細かなフィードバックにも対応していただけて良かったです。

docs.datadoghq.com

Fastly Yamagoya Meetup 2017でのFastlyの舞台裏のお話しは非常にエキサイティングだったので、ますます注目です。今回の訪問は、これまでサテライトオフィスで頑張ってこられた日本オフィスがどうなるのかを含めて非常に楽しみにしていました。

所在地

新オフィスは新橋駅から徒歩6分程度の場所にある、Tsao Hibiyaとのことでランチ時間に合わせて伺いました。

当日は晴れで、新橋の大通りからすぐなので迷子ガチ勢の私でも迷わず到着できました。

Fastly Japanが入っているビルは2017竣工と新しく外観も中もきれいです。暑かった外から入った時に、緑が爽やかに感じたのが印象的です。

8F に向かいます。

オフィスの雰囲気

エレベータを降りてすぐ左にFatlyならではのセリフ、右にガラス扉があります。

受付はなく、すぐに働かれている皆様が見えたのでご挨拶をしてフロアにお邪魔しました。

[まさかの写真を忘れました!!]

広々としたフロアにはまだデスクも少なく、サンフランシスコ同様扉がないオープンスペースで開放感があります。当日は9名の方がおいでで、席の半分は空席でした。伺ったところ、暑くなってきてリモート勤務されている方が増えてきたとのことで、働く場所が自由なのが日本でも徹底されており素敵です。

固定席の隣には、USから来られた方のためのフリーアドレススペースが広々とあります。

デスクスペースの脇には、ゆったりとしたソファスペースがあり、Fastlyクッションやブランドカラーと同じ色の調度品がさりげなくあります。

壁にはFastlyといえばといったフレーズがあり、反対側にはメンバーの方の写真があるのもサンフランシスコと同じ雰囲気を作っていました。

会議室は「速いもの」をテーマにサンフランシスコで命名されており、日本ならではと言える「新幹線の名前」が採用されていました。

なるほどHAYATE。

会議室は、会議室状況が入口のタブレットで把握できること以外にもZoomが完備されています。

特に圧巻だったのがSAKURAです。Zoomで会議が始まると、天井にあるカメラを自動的にホワイトボートに向けたり、自在に共有する視点を調整できるようになっています。ナニコレスゴイ。Fastly社のファシリティ担当者がサンフランシスコから来て設定されたそうです。エキサイティングですね、やりたい!

キッチンスペース

オフィススペースの隣にはキッチンスペースがあります。ここまで見てきた通り、サンフランシスコと同様のファシリティの提供を基本に設計されており、同じレベルの社内サービスを提供するというファシリティ担当者の熱い情熱を感じます。毎週定期的にランチが振る舞われるのも同様で、日本ではUber Eatsや出前館を使って皆さんで集まって食事をとられているとのことです。

サンフランシスコにもあったのと同じトースターがあるのにはコダワリを感じました。*1

ドリンクスペースもあり、ゆっくり過ごしてほしいという感じが伝わります。*2

キッチン横のオープンスペースの壁はブランドカラーとなっており、ここでもブランドイメージが徹底されています。

世界時計があるのもサンフランシスコと同じで、世界をまたにPOPがあるという印象が強いですね。

しばしお話しを伺った後、お土産にFastly Tシャツもらっちゃいました。(通算4枚目)

さいごに

いかがでしたでしょうか?引っ越しに際しては、突然水曜にパッキングしてとアナウンスがあり金曜に移転したという話もあり、相変わらずのスピード感がおもしろ素晴らしかったです。

これまでのサテライトオフィスは会議室の確保に難儀されていたとのことで、新オフィスは会議室が豊富にあり楽しい雰囲気でした。サンフランシスコと同じ雰囲気を随所に感じるので、Fastly様でミートアップなどの機会が将来行われることを楽しみにしています

ご対応いただいた、Mio Matsudaさん、Fastly Japanの皆様、本当にありがとうございました!

*1:こういうコダワリ好きです

*2:IPAもあるのが印象的です

PowerShellっぽく陸上自衛隊のイラク派遣日報をまとめてダウンロードしてみる

面白い記事があったので、私もやってみます。

blog.daruyanagi.jp

毎度毎度、PowerShellっぽさとは何かなぁ思うのですが、PowerShell実践ガイドブックでもWebサイトのステータス監視などを書いたので、良い題材な気がします。

目次

C# だとどう書くのか

C#ならAngleSharpを使って次のようなコードでダウンロード処理を行うことができます。 私の自宅では、60Mbpsを維持して5分でおわりました。

gist.github.com

PowerShellだとどうなるでしょうか。

元記事の処理

元記事では次のように書いています。

$source = "https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html"
$folder = "C:\Users\Hideto\pdf"

$result = Invoke-WebRequest $source -UseBasicParsing
$urls = $result.Links.href | Get-Unique |  where { $_ -match ".pdf" }

foreach ($url in $urls)
{
    $file = ($url  -split "/")[-1]
    Invoke-WebRequest -Uri $url -OutFile (Join-Path $folder $file)
}

PowerShell 6.0で、ここから何処まで手早く書きつつ、素早く処理できるか考えてみましょう。

必要な処理を抜き出す

必要な処理は、次の4つとわかります。

  • サイトの構造からリンクURLを抜き出す
  • PDFのパスのみを取得する
  • 重複があればはじく
  • ダウンロードする

インライン処理

URLや、パスに関しては、処理を括弧でくくることでインライン処理しつつプロパティにアクセスできます。 変数に保持したいかどうか次第ですが、書き捨てでコンソールで書くならこんなのでもいいでしょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href

パイプラインとメソッド形式の選択

さて、PDFに絞る方法ですが、パイプラインの入力をStringクラスのEndsWithメソッドで絞るのが楽でしょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")}

コレクション入力を都度処理するときは、パイプラインで書くのも楽ですがメソッド形式で書くという選択もあります。 メソッド形式については過去の記事をどうぞ。

tech.guitarrapc.com

PowerShell実践ガイドブックでは書かなかったのですが、.ForEach().Where() メソッドは、foreach構文同様にコレクションを処理する時に一度メモリにため込むため、パイプラインよりも高速に動作します。ただしメモリに保持するということは、膨大な大きさのコレクションではメモリを大量に使うため対象のサイズに注意が必要です。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.Where({$_.EndsWith(".pdf")})

なお、一行が長くなって改行したい場合は、.で改行します。.WhereではなくWhereとなるので気を付けましょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")})

あるいは{(で改行するのもいいでしょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.Where({
$_.EndsWith(".pdf")})

メソッド形式で数をフィルタ (Select-Object -First 1 に相当する処理は、Whereメソッドで指定します。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}, "First", 1)

こういう処理を書いている時は、1個だけ試したい、というのはあるあるですからね。

一意に絞る

URLの数を見てみると、1220個ありますが、PDFを末尾に持つURLで絞ると870個です。 しかし、ここには重複したURLが含まれているため、一意(ユニーク)なURLに絞りましょう。 PDFのURLは順不同に並んでいるため、Get-UniqueではなくSort-Object -Uniqueをする必要があります。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique

これで、435個に絞られました。

URLからファイル名を取る

元記事では、$file = ($url -split "/")[-1]と書いており/で分割してできた配列の最後*1をとっています。 PowerShellらしいといえばらしいのですが、Split-Pathを使うと配列を意識せず/の最後をとれます。

Split-Path PDFのURLパス -Leaf

PDFファイルのURL、ファイル名まで取れたのを確認してみましょう。 1つだけ試すならこれで。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}, "First", 1).
ForEach({Split-Path $_ -Leaf})

パイプラインならこうです。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf") | 
Select-Object -First 1 | 
ForEach {Split-Path $_ -Leaf}

ダウンロードする

1つだけダウンロードするならこうです。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}, "First", 1).
ForEach({Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf)})

全てなら、一意に絞りましょう。

((Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}) | 
Sort-Object -Unique).
ForEach({Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf)})

パイプラインでも、同様に書けます。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique | 
ForEach {Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf)}

また、パイプラインの場合は-PipelineVariable を使ってメソッドよりも柔軟に書くことができます。 -PipelineVariableは、自動変数を一度変数に受けて明示的な変数に割り当てることを不要にするので非常に便利です。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique -PipelineVariable pdf | 
ForEach{Invoke-WebRequest -Uri $pdf -OutFile (Split-Path $_ -Leaf)}

もちろんこれでダウンロードできるのですが、1つ1つのファイルをダウンロードしたら次に行く同期処理です。 そのため、ダウンロードに時間がかかり435ファイルで6分かかります。

次にこれを高速化してみましょう。

非同期ダウンロードで高速化する

PowerShellの非同期技法は、大きく2つです。 Job機能を使うか、.NETの非同期構文です。

Jobを使った非同期処理

PowerShellらしさ、となるとJob機能ですがオーバーヘッドが大きい処理のため、必ずしも適切ではないケースがあります。今回のような「膨大な数」がまさにそのケースで、こういったときには.NETランタイムを使うと非同期処理がしやすいです。

Jobに変数を渡す時は、スクリプトブロックにparam句を使って -ArgumentList パラメーターを使って渡しますが、実行すると全くダウンロードされないことが分かるでしょう。 これがJobの問題点で、ダウンロードのような処理を大量のジョブでリソース割り当てを行うと大概うまくいきません。 ただの分散処理なら問題ないのですが、ダウンロードは陥りやすいでしょう。

$jobs = (Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique | 
ForEach{Start-Job{param($x,$file,$path) cd $path; Invoke-WebRequest -Uri $x -OutFile $file} -ArgumentList $_,(Split-Path $_ -Leaf),$pwd}
Receive-Job $jobs -Wait

Taskを使った非同期処理

.NETのTaskを使ってみましょう。HttpClient経由でダウンロードします。 先ほどのC#の処理からダウンロード部分を抜き出してヒア文字列としたら、Add-Typeでクラスをコンパイル/読み込みします。 あとは、uri一覧をPowerShellで取得してダウンロードを呼び出すだけです。

おおよそC#と同程度の時間で終わります。

gist.github.com

C#側にダウンロード、非同期ロジックを任せることができるので、PowerShellのコードがシンプルなことがわかります。

まとめ

たぶんPowerShellっぽさは、「型を必要になるまで意識せずに」、「適当にコマンドつなげたら書ける」の2点だと思います。 なので、同期処理の場合はワンライナーにしましたし、するのは違和感ないと思います。 一方で、Jobが入った瞬間難しい見た目ですね。.NETランタイム使うのも、唐突に新しい考えが入った印象が強いと思います。 自分が書きやすく、未来の自分が読むのも困らないように書くにはコツがあります。

  • コマンドレットのエイリアスはあまり使わない(Whereや%のような頻出以外は、なるべくフルで)
  • パラメーターを必ず用いる
  • パスやディレクトリを直接触らない

あとは、シェルっぽいやり方として

  • 変数にすることでDRYができるならするが、DRYにならずメンテもしないなら書き捨てる
  • コマンドはパイプラインでつなげていく

見た目が難しくなる = 読み下しが難しくなる要素は、スクリプトブロックなどの「読み手に解釈」を求めるものがあるかもしれません。 渡す順番が処理に影響する、スコープが影響するのは難しいでしょう。

  • スクリプトブロックでparamを使い、-ArgumentListパラメーターで渡す

数が少ないなら同期で十分です。 もし数が多く、非同期で書きたい場合はJobか.NETのTaskを使うといいでしょう。

gist.github.com

参考

PowerShell のStart-Jobに非同期数制御があれば、また話はべつなのですが.... 自宅の回線がネックになってるので、余り速度が速くなりませんでしたが900Mbpsとか超えている環境では顕著に変わるでしょう。

コマンドの長さと、Invoke-WebRequest がパイプラインからの入力を受け付けないのがネックで長いですね。

*1:PowerShellは配列の最後の要素に-1でアクセスできます