今回はWebからデータをダウンロードしてみます。Web上にあるデータは永続的ではないため、データをダウンロードしてローカルディスクに保存しておきたい人にも少しは役立つかもしれません。Webからデータをダウンロードすると言っても様々な形式のファイルがあります。テキストファイルもあれば画像ファイルもありますし、映像や音声ファイル、プログラムファイル等々…

ここでは、HTMLファイルと画像ファイルをダウンロードしてみます。HTMLファイルをダウンロードするのは、書かれてる内容を抽出するためです。いわゆるスクレイピングというやつです。

Webは巨大なデータベースのはずですが、欲しいデータをうまく抽出するのは結構面倒です。Web APIで必要なデータを得ることができるサイトはわずかです。そもそも、ちょっとした内容でしかないものにAPIを用意するというのもコスト的にどうなのかというのもあります。

という事で、たどり着いた方法の一つがHTMLファイル内に書かれた内容を抽出するというスクレイピングだったのでしょう。幸いにしてHTMLはプレーンテキストではなく、テキストに意味づけされたタグが書かれていますので、これをキーにして必要なデータを抽出する事ができます。

ただし、このスクレイピングには問題もあります。HTMLの構造によってはデータを簡単に抽出できない事があります。特に一番の問題はサイトもしくはページがリニューアルされた場合です。つまりHTMLファイルの構成が変わった場合、その都度対応するためにプログラムを見直さないといけないという事です。

スクレイピングによる抽出は永続的なものではないので、ここらへんはあきらめるしかありません。

画像ファイルのダウンロード

 まずは画像ファイルをダウンロードしてみます。HTMLファイルを先にやった方がいいような気もしますが、意外と画像ファイルの方が簡単です。それは、ダウンロードするだけで今回は何も処理しないからです(もちろん、やりたければ後で画像処理することもできます)。
 ダウンロードする画像ファイルですが、この連載の最初に掲載されている魔法使いのイラストにしましょう。

https://news.mynavi.jp/series/natonakucommand/

 ダウンロードする画像のURLは画像の上にマウスポインタを移動させ右ボタンを押します(Macで1ボタンマウスの場合はcontrolキーを押したままクリックしてください)。表示されるメニューから画像のURLをコピーします。これで準備OKです。

 コピーした画像のURLですが以下のようになっています。ただ、サイトがリニューアルされたりすると画像URLが変わってしまうことがあるので注意してください。

https://news.mynavi.jp/publicimg/series/image/2296/index.jpg

 それではコマンドを使って画像をダウンロードします。使うコマンドはcurlです。なお、PowerShell上でcurlコマンドを使う場合、curlでなくcurl.exeとしてください。PowerShellではcurlはInvoke-WebRequestのエイリアスとして定義されているため、UNIXでのcurlとは挙動が異なります。今回、UNIX系でのシェルは何を使っても構いません。シェルに依存する機能は使用していないためです。

 curlのオプションはたくさんありますが、単純に画像データをダウンロードするだけなら何もオプションを指定しなくても大丈夫です。なお、ダウンロードの進捗状況を表示したくない場合は-sオプションを指定してください。

 curlの後にダウンロードするURLをペーストします。その後にリダイレクト記号の>を入力してからダウンロードした画像を保存する場所(ファイルパス、ファイル名)を指定します。あとはリターンキーを押せば画像がダウンロードされ保存されます。以下のコマンド例だとデスクトップにimage.jpgという名前で画像が保存されます。

curl https://news.mynavi.jp/publicimg/series/image/2296/index.jpg > ~/Desktop/image.jpg

画像ファイルはJPEGでもPNGでも、このような感じでダウンロードすればOKです。

PowerShell (Windows版、Mac版)の場合はcurlではなくInvoke-WebRequestを使います。以下のように指定するとデスクトップにimage.jpgが保存されます。もし、WindowsのPowerShellでInvoke-WebRequest実行時にエラーが出るようであれば一度Internet Explorerを起動した後に実行してください。

Invoke-WebRequest https://news.mynavi.jp/publicimg/series/image/2296/index.jpg -OutFile ~/Desktop/image.jpg

HTMLファイルのダウンロード

それでは次にHTMLファイルをダウンロードしてみます。HTMLファイルのダウンロードも画像のダウンロードと同じです。 この連載記事の目次ページのURLをコピーします。ブラウザのアドレスバーのURLをコピーしておきます。ちなみに、この連載記事の目次ページのURLは以下のようになっています。

https://news.mynavi.jp/series/natonakucommand/

画像のダウンロードと同じようにcurlの後にURLをペーストします。HTMLファイルはテキストなので以下のように入力後にリターンキーを押すと内容が表示されます。

curl https://news.mynavi.jp/series/natonakucommand/

PowerShellの場合は以下のようにすると内容が表示されます。(文字コードによっては文字化けしますが、ここでは無視。気になる人は-Encodingオプションを指定して、その後にUTF8などを指定してください)
これ以外のページでうまく表示されない場合は、この記事の一番最後のおまけの項目を読んでください。

(Invoke-WebRequest https://news.mynavi.jp/series/natonakucommand/).Content

その後にリダイレクト記号の>を書き、保存先の場所を指定します。
HTMLファイル以外のファイルなども同様にダウンロードできます。

スクレイピングしてみる

 それでは次にダウンロードしたHTMLファイルから必要なデータを抜き出してみます。いわゆるスクレイピングというやつです。
 コマンドを使ってHTMLファイルからデータを抜き出すというのは、実際の所おすすめしません。やるなら、流行りのPythonや便利なツールを使いましょう。その方が後々のためにもなります。

 じゃあ、どうして無理にコマンドでやるのかというと、そういう連載だから…です。結論から言うと途中で挫折してしまい最後はPerlを使ってしまいました。でも、ワンライナーなのでコマンドという事で、ご容赦ください。だいたい、動けばオッケーみたいなテイストの連載ですので、大目に見てください。

 という事で、頑張ってコマンドでスクレイピングします。面倒なので、ここでは動作環境をUNIX系とします。実際に動かしている環境はmacOS BigSurです。

 まずは、この連載記事の目次ページのタイトルを抜き出してみます。
スクレイピングする際に毎回curlでネット経由でデータを持ってきて解析するのはコストがかかる(時間がかかる)ので、一度どこかにダウンロードしておくことにします。例によってわかりやすくするためデスクトップにindex.htmlというファイル名で保存します。

この連載記事の目次ページのHTMLファイルをデスクトップにダウンロードするは以下のようになります。ダウンロード後はcd ~/Desktop/とコマンドを入力してカレントディレクトリをデスクトップに変更しておいてください。以後のコマンドはカレントディレクトリはデスクトップになっていることが前提です。

curl https://news.mynavi.jp/series/natonakucommand/ >~/Desktop/index.html

ダウンロードしたHTMLファイルの中にあるtitleタグの内容を取得します。一般的にtitleタグ内には文字しかないので、最初に作るにはよさそうです。とりあえずsedを使って処理してみます。以下のようにすればtitleタグの内容が出力されます。色々試行錯誤する可能性がある場合はsedのオプションの後にファイル名を指定するよりcatとパイプラインでデータを流していった方がよさそうです。sedの内容はtitleタグで囲まれた内容を正規表現(パターンマッチ)でマッチさせ出力します。()はsedに限らず正規表現でマッチした文字列を抜き出す場合に使います(sedの正規表現は2種類ある上、実行環境によってオプションが-e,-rだったり異なるので面倒です)。マッチした文字列は\1として参照できます。あとはpで表示という流れです。
以下のように入力し実行すればtitleタグの内容が表示されるはずです。

cat index.html| sed -n 's|<title>\(.*\)</title>|\1|p'

が、残念ながらこの連載の目次は期待通りに表示されません。なぜか余計な部分までマッチしているようです。| moreでページ単位で表示するとわかりやすいかもしれません。

多くのWebページでは問題ないのに、どうして?と思いよくみるとSVGにもtitleタグがあるではありませんか。大昔は画像で表示されていたアイコンやロゴなどもデバイスの解像度を問わないベクター形式のSVGの方が向いているからでしょうけど、これは思わぬ障害です。

HTMLの構造上headタグ内にあるtitleを抜き出すか、最初に出てくるtitleタグの内容だけを抜き出せば良いはずです。よほど文法を無視したHTMLファイルでもない限り最初のtitleタグの内容だけを抜き出せば問題ないはずです。ただ、sedだと細かい指定が難しいので以下のようにしてマッチした2行目以降は全部削除する方向にしました。
これで、一応期待通りの結果になります。

cat index.html| sed -n 's|<title>\(.*\)</title>|\1|p' | sed '2,$d'

xmllintを使う

 スクレイピングにsedを使うのは向いていないのですが、Perl, Rubyでプログラムを組むのも面倒だという人もいるかもしれません。特定のタグ内容だけ欲しいならxmllintを使う方法もあります。Macの場合は、かなり古い時代から標準で入っているので古くなったマシンの再利用にもいいかもしれません(確認したらMacOS X 10.6にも入っていました)。UbuntuやDebianなどLinuxのディストリビューションに入っていない場合はインストールすれば使うことができます。

 xmllintのオプションなどはいくつかありますが、ここではHTMLタグをXPathを使って抜き出すことにします。XPathについては検索すると入門記事などが出てきますので、概要だけ把握してもらえば良いかと思います。

  ・xmllintオプション(Ubuntu)
http://manpages.ubuntu.com/manpages/cosmic/man1/xmllint.1.html
・XPath(Mozilla)
https://developer.mozilla.org/ja/docs/Web/XPath

--xpathオプションの後に抜き出したいタグのXPathを指定します。XPathはタグの階層を指定していきます。titleタグの場合、htmlタグ内のheadタグ内にあるのでパス表記は/html/head/title/になります。これは絶対パス表記なのですが、相対パス表記にすることもできます。headタグは1つしかないためです。相対パスの場合はパスの先頭を//のようにします。つまり//head/title/としてもtitleタグを示すことになります。titleタグが1つしかなければ//title/としても構いませんが、SVGタグ内にtitleタグがあると駄目なので//head/title/あたりが妥当でしょう。
 タグの内容を抜き出す場合、XPathには便利なtext()関数があります。XPathにtext()を書くと、そのテキスト内容を取得することができます。ということで以下のようにするとtitleタグの内容が表示されます。そのまま実行するとエラーが表示されるので2>/dev/nullとしてエラー出力を消しています。

cat index.html| xmllint --xpath '//head/title/text()' --html - 2>/dev/null

ページ内の任意のタグ内容を抜き出す

 xmllintとXPathを組み合わせれば手軽にタグ内容を抜き出せますが、問題もあります。Webページから抜き出す内容のXPathをどうやって把握するかです。都合のいいことにGoogle Chromeなら以下の手順で簡単に目的のXPathを取得できます。

(1)XPathを取得したいWebページを表示します。

(2)取得したい内容の上でマウスの右ボタンをクリックします。Macで1ボタンしかないマウスの場合はcontrolキーを押したままクリックします。するとポップアップメニューが表示されます。

(3)デベロッパーツールが表示され、取得したい部分がハイライトされます。

(4)ハイライトされた部分でマウスの右ボタンをクリックします。メニューが表示されるのでCopy→Copy XPathを選択します。

(5)XPathがクリップボードにコピーされます。今回の場合は以下のXPathになります。

/html/body/div[1]/div[2]/div[2]/div/div/p

 これで準備OKです。あとは以下のようにコマンドを入力すれば連載のリード文が表示されます。

cat index.html| xmllint --xpath '/html/body/div[1]/div[2]/div[2]/div/div/p/text()' --html - 2>/dev/null

 出力できればあとはリダイレクトでファイルに保存してもいいですし、さまざまなテキスト処理をする事もできます。
スクレイピングをコマンドでやると、やはり手間がかかるのでPythonなどスクレイピングが得意なプログラム言語などを使った方がいいと思います。

PowerShellでタグ内容を読み出す

 HTMLタグの内容を読み出したり処理するのはWindows版のPowerShellの方が楽です。何と言ってもWebが普及した後に作られたのは大きなアドバンテージです。
 まず、titleタグの内容を抜き出すには以下のようにします。簡単です。

(Invoke-WebRequest https://news.mynavi.jp/series/natonakucommand/).ParsedHtml.title

次に連載タイトルのリード文ですが、summaryTop_descriptionクラス名が割り当てられているので以下のように指定するだけです。やはり、こういう処理はPowerShellの方が簡単です。

(Invoke-WebRequest https://news.mynavi.jp/series/natonakucommand/).ParsedHtml.body.getElementsByClassName("summaryTop_description")[0].innerText

UNIX系では苦労した(?)のに、このPowerShellの簡単さはありがたいところではないでしょうか。ちなみに機能も豊富です。

おまけ

 HTMLタグを削除してテキストとして保存したい、処理したい場合もあります。これをコマンドで頑張ると以下のような感じになります。文字実体参照・実体参照は不等号記号のみで&は未処理です。ただ、こんなに頑張ってコマンドでやる必要はないと思うので便利なツールやプログラミング言語を使うのをおすすめします。

cat index.html | sed '/<script/,/script>/d' | sed 's|<!--.*-->||g' | sed '/<!--/,/-->/d' | sed 's|<[^>]*>||g' | sed '/</,/>/d' | sed 's/&gt;/</g' | sed 's/&lt;/>/g' | sed -e 's/^ *//g' | perl -pe 's/\r/\n/' | cat -s | sed '/^$/d'

ちなみにWindows版のPowerShellだと以下のように指定するだけで、とても簡単です。

(Invoke-WebRequest https://news.mynavi.jp/series/natonakucommand/).ParsedHtml.body.innerText

HTMLファイル内にあるimgタグから画像URLを抽出することもできます。xmllint使うと以下のようになります。trコマンドは改行に変換するために使っています。

cat index.html| xmllint --xpath '//img/@src' --html - 2>/dev/null | tr ' ' '\n'

Windows版のPowerShellの場合は以下のようになります。少し長いのですが、行単位に分解してもらえば分かりやすいかもしれません。

$html=(Invoke-WebRequest https://news.mynavi.jp/series/natonakucommand/).ParsedHtml.body.getElementsByTagName("img");for($i=0; $i -lt $html.length; $i++){ Write-Output($html[$i].src) }

PowerShell万歳といきたいところですけど、これはWeb+DOM+JavaScriptの知識が必要なので、これはこれで大変そうです。やはり、Pythonあたりでスクレイピングするのが汎用性も高くてよいのではないでしょうか。

著者 仲村次郎
いろいろな事に手を出してみたものの結局身につかず、とりあえず目的の事ができればいいんじゃないかみたいな感じで生きております。