久々に.NETでなんかした記事です。
もくじ
- 何を作ったのか/使い方
- アドインの中はどうなっているのか
- WPFで良い感じにする
- HtmlAgilityPackで検索しつつ画像をとってくる
- PowerPointアドインの流儀に沿って画像を追加する
- まとめと補足
1. 何を作ったのか/使い方
PowerPointの拡張として「いらすとやのイラストを検索し、ポチるとスライドに追加できる」というのを作りました。
使い方はGitHubのほうを見てください。
GitHub側にも記載していますが、バイナリ版のインストーラは取りあえずGoogle Driveに突っ込んであります。
2. アドインの中はどうなっているのか
GitHubにソースあるので読んでもらえればいいのですが、大きく3セクションに分かれています1。
- WPFのユーザーコントロールで体裁を整える
- いらすとやのウェブページから指定キーワードでの検索結果を取得し、サムネイルや実際の画像のURLを抽出する
- 得られた画像をPowerPointアドインの流儀に沿って追加する
それぞれ独立性が高い機能です。
3. WPFのユーザーコントロールで体裁を整える
PowerPointアドインはWindows FormのUserControlベースでUIを作らないといけないことになっています。
しかし私はWPFのUserControlが使いたかったため、ElementHostを使って 実質WPFのコントロールでUI作ってるような状況に持ち込みました。
ThisAddIn.cs
using System.Windows.Forms.Integration; using System.Windows.Forms; using System.IO; using Microsoft.Office.Interop.PowerPoint; //... private void PrepareCustomControl() { //Windows FormsのUserControl var userControl = new UserControl(); userControl.Controls.Add(new ElementHost() { //IrasutoyaSearchPane: WPFのほうのUserControl Child = new IrasutoyaSearchPane() { DataContext = new IrasutoyaSearchViewModel() }, //DockStyle.FillにしておくとWPF側のUIがカスタムUI全体に広がる: レイアウト的には無難 Dock = DockStyle.Fill }); var pane = this.CustomTaskPanes.Add(userControl, "Irasutoya Search"); pane.Visible = true; }
一般的なやり方は「WPFコントロールをWindowsフォームに配置する」とかに載っています。
また、今回は検索結果の各アイテムを「サムネイル画像とテキストをセットにしたボタン」
という形で表示したかったので、以下のようにItemsControl
とDataTemplate
を組み合わせて
体裁を整えています。
<UserControl x:Class="Baku.IrasutoyaPpt.IrasutoyaSearchPane" ... > <UserControl.Resources> <!-- 検索結果で得られたアイテムを「サムネイラスト + 文字要約」が載ったボタンにする --> <DataTemplate x:Key="SearchResultTemplateKey" DataType="{x:Type local:IrasutoyaItemViewModel}"> <Button Command="{Binding AddToSlideCommand}" Margin="5" HorizontalAlignment="Stretch"> <StackPanel HorizontalAlignment="Stretch"> <Image Margin="3" Source="{Binding ThumbnailUrl, Mode=OneWay}" Stretch="Uniform"/> <TextBlock Margin="3" Text="{Binding ThumbnailText}" TextWrapping="WrapWithOverflow" HorizontalAlignment="Center"/> </StackPanel> </Button> </DataTemplate> <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> </UserControl.Resources> <Grid> ... <!-- スクロール可能 + 検索結果を一覧表示可能 --> <ScrollViewer Grid.Row="3" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"> <ItemsControl ItemsSource="{Binding Results}" ItemTemplate="{StaticResource SearchResultTemplateKey}"/> </ScrollViewer> ... </Grid> </UserControl>
この辺の流れについては「ItemsControl 攻略 ~ 外観のカスタマイズ」とかが参考になります。
もし書籍で似たような事を勉強したい場合は「プログラミングWindows 第6版」あたりを読みましょう。
4. HtmlAgilityPackで検索しつつ画像をとってくる
他所のブログにもさんざん書かれている話ですが、いらすとやにはAPIが無いので、 いわゆるウェブスクレイピングが必要です。
C#
の場合、HtmlAgilityPackというパッケージがあり、
これを使うと比較的楽ちんに要素抽出ができます。2
やり方は3ステップに区切れます。
WebClient
などを使い、検索結果のHTMLを持ってくる- HTMLテキストを
HtmlAgilityPack
に読ませてHtmlDocument
にし、XPath
で読める状態に持ち込む XPath
を使って良い感じに解析する
アドインのコードよりもサンプルで紹介した方が分かりやすいため、 ここではコンソールアプリケーションの例を示します。
まず新規作成したプロジェクトでNuGetパッケージからHtmlAgilityPack
を追加します。
また、参照としてSystem.Web
を追加しておきます。
その状態で、次のようなコードを書きます。
using System; using System.Text; using System.Linq; using System.Net; using System.Web; using HtmlAgilityPack; namespace TestIrasutoyaSearch { class Program { static void Main(string[] args) { //1. HTMLを拾う Console.WriteLine("検索したいワードを入力してください"); string keyword = Console.ReadLine(); string url = "http://www.irasutoya.com/search?q=" + HttpUtility.UrlEncode(keyword); string html = new WebClient() { Encoding = Encoding.UTF8 }.DownloadString(url); //2. HtmlDocumentに変換 var doc = new HtmlDocument(); doc.LoadHtml(html); //3. XPathを渡してサムネイルの要素を指定 //NOTE: ノードが見つからないとnullで帰ってくる var thumbnails = doc .DocumentNode .SelectNodes(@"//div[@class=""date-outer""]//div[@class=""boxim""]/a") ?? Enumerable.Empty<HtmlNode>(); foreach(HtmlNode node in thumbnails) { Console.WriteLine($"Thumbnail link: {node.GetAttributeValue("href", "")}"); Console.WriteLine($"Thumbnail elem: {node.InnerHtml}\n\n"); } //4. 「前のページ」「次のページ」といった要素があればその移動先URLを取得 string nextPageUrl = doc .DocumentNode .SelectSingleNode(@"//a[@class=""blog-pager-older-link""]") ?.GetAttributeValue("href", "") ?? ""; Console.WriteLine($"Next: {nextPageUrl}"); //NOTE: 今回の例ではつねに先頭の検索結果を出すため、「前のページ」は無い string prevPageUrl = doc .DocumentNode .SelectSingleNode(@"//a[@class=""blog-pager-newer-link""]") ?.GetAttributeValue("href", "") ?? ""; Console.WriteLine($"Prev: {prevPageUrl}"); } } }
実行して「ロボット」を検索した場合の出力を抜粋するとこんな感じになります。
検索したいワードを入力してください ロボット Thumbnail link: http://www.irasutoya.com/2016/12/blog-post_911.html Thumbnail elem: <script type='text/javascript'> document.write(bp_thumbnail_resize("https://3.bp.blogspot.com/-p8VAdsMe--w/WGHFF1wOnYI/AAAAAAABArg/8dYEgeMpQiIswCGM9dW2-edhW3dRuzOEgCLcB/s72-c/thumbnail_higogata_robot.jpg","いろいろな人型ロボットのイラスト")); </script> Thumbnail link: http://www.irasutoya.com/2015/01/blog-post_52.html Thumbnail elem: <script type='text/javascript'> document.write(bp_thumbnail_resize("http://1.bp.blogspot.com/-7lDnPmW63NM/VJ6XHeyIqVI/AAAAAAAAqFg/CzKEJobd9BQ/s72-c/omochaya.png","おもちゃ屋のイラスト")); </script> Thumbnail link: http://www.irasutoya.com/2013/09/blog-post_589.html Thumbnail elem: <script type='text/javascript'> document.write(bp_thumbnail_resize("http://1.bp.blogspot.com/-uPm7jG2nRMs/UbVu_9m0d9I/AAAAAAAAUlc/PpDn9P3KBmc/s72-c/mokei_puramo.png","プラモデルを作っている男の子のイラスト")); </script> Next: Prev: 続行するには何かキーを押してください . . .
各サムネイル要素に対し、そのサムネイルに対応するイラストページのURLが取得できていることが分かります。 実際のアドインでは、各イラストページに対してもふたたび上と似たような処理を適用し、 最終的にフルサイズ版イラストのURLを取得しています。
上の例では表示されてませんが、ヒット件数が多いキーワードを指定すると、 「Next:」のURLも表示されるようになります。これを使えばページ遷移みたいなことも可能になります。
もし手元で試す場合は、いらすとやの検索ボックスを使った場合と 見比べてみると分かりやすいかと思います。
なお、ここで「この検索に使うXPath
の文字列ってどうやったら分かるの?」という疑問が出てくると思います。
基本文法はMSDNで読んだりググると出てきます。
実際にやるときの方法は私の場合こんな感じです。
- ウェブブラウザの開発者モードでソースとビジュアル階層を見比べる
- 抽出したい要素の階層を見つける
- その要素のタグや属性値を見て、うまく特定できそうな
XPath
をカンで決める
典型的なパターンではclass
属性を指定したdiv
とかで割とうまくいきますが、臨機応変にやってください。
さらにもう一つ。上記コードの処理ではサムネイルの画像URLが完全には拾えません。 JavaScriptが絡んでるのがその原因です。
アドインのコードでは、抽出した要素に含まれるJavaScriptコードを
さらに正規表現でバラして、サムネイル画像のURLだけ取り出す、といったこともやっています。
HTMLの中身のテキスト解析はHtmlAgilityPack
単体ではどうにもならないので、
うまく諦めて他の方法に切り替えるのも大事なポイントです。
5. PowerPointアドインの流儀に沿って画像を追加する
ふたたびアドインのコードに戻って、仕上げの処理です。 HTMLからの処理としては画像URLを抽出し、画像をダウンロードするところまでが可能です。
それをPowerPointの画像に追加する際は、(ちょっとダサいですが、) いったんローカルファイルに保存することで処理が通ります。
ThisAddIn.cs
using System.Windows.Forms.Integration; using System.Windows.Forms; using System.IO; using Microsoft.Office.Interop.PowerPoint; //... public void AddImageToCurrentSlide(byte[] binImage) { string fileName = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); File.WriteAllBytes(fileName, binImage); (this.Application?.ActiveWindow?.View?.Slide as Slide) ?.Shapes ?.AddPicture( fileName, Microsoft.Office.Core.MsoTriState.msoFalse, Microsoft.Office.Core.MsoTriState.msoTrue, 100, 100 ); }
現在のスライドを指定する方法((this.Application?.ActiveWindow?.View?.Slide as Slide)
)も、
知ってると便利そうな小技かもしれません。
6. まとめと補足
なんだかんだでOfficeのアドインは作りやすいなあと思いました。
そして最後の最後で今更な補足になりますが、本アドインのネタは私のオリジナルではないです。 ネタ元はTwitterでピリ辛(@lifeslash)さんがやっていたもので、 より具体的に言うならこの記事の後発的なマネです。
そちらのケースでは見たところ「WinForms+スクレイピング正規表現縛り」でやってそうに見えたので、 「WPF + HtmlAgilityPackのほうがそれっぽいし手間減るのでは」みたいな直感に基づいて ガーッと作ったのが今回のやつです。
…まあ、この辺は知識背景にもよるので何とも言えませんが。
- 社会人のテクニックとして「なんでもいいから3つに分けておけばそれっぽくなる」というライフハックを学びました。 ↩
- もちろん一択ではありません。他の例としてはC#でスクレイピング:HTMLパース(Linq to Html)のためのSGMLReader利用法等があります。 ↩