シェルスクリプトでクロールする

2016年4月15日

クロールなんてPythonやRubyを使えば楽なのかもしれません。 それでもシェルスクリプトでやるならこんなことを覚えておくといいかも。

検索サイトの振る舞いを知る

※オリジナル記事ではYahoo!ファイナンスのクロールについて書いていましたが、現在はクロールが禁止されているため該当部の内容を削除しました。

サイトによってはクロール対象のアドレスから別ページへの転送がかかっていることがあります。wgetでクロールする場合、メッセージを確認した上で再度転送先を読みに行くなどの処理をする必要があります。

wget http://target.address -o ~/tmp/query.log
grep -e "特定のメッセージ" ~/tmp/query.log > /dev/null
if test $? -eq 0; then
  〜実際の処理〜
fi

wgetの-oオプションに続けてファイルを指定すると、出力をファイルに保存できます。エラー出力も標準出力へ転送されるので、301リダイレクトのようなメッセージも処理できるようになります。

grepで検索結果があるかどうかはリターンコードを確認すればわかるので、シェル変数$?の結果によって処理を分岐します。

ただし、こんな面倒なことをしなくてもcurl -Lを使えば転送先アドレスを追跡できますけどね。

grepで必要な部位を抽出

今回少し失敗したのが値の抽出です。表の中にある値が欲しいとかよくあるパターンですが、そのキーワードが重複していた場合に余計なものを抽出してしまいます。

いろいろな値が表にまとまっています。

表の中からある商品の「価格」を抜き出す場合、一旦その商品名と「価格」を含むコードだけ抽出してからhtmlタグを外すという作業をします。

商品名 謎のみかん
重さ 850kg/個
価格 277万7778円(税別)
商品名 MacBook early 2016(梅)
重さ 0.92kg/台
価格 14万8800円(税別)
商品名 山のマグロ
重さ 0.35g/尾
価格 時価

こんな感じのソースです。

<p>いろいろな値が表にまとまっています。</p>
<p>表の中からある商品の「価格」を抜き出す場合、一旦その商品名と「価格」を含むコードだけ抽出してからhtmlタグを外すという作業をします。</p>
<table style="float:left; margin-right:1em; margin-bottom:1em;">
  <tr><th style="width:4em;">商品名</th><th>謎のみかん</th></tr>
  <tr><td>重さ</td><td>850kg/個</td></tr>
  <tr><td>価格</td><td>277万7778円(税別)</td></tr>
</table>
<table>
  <tr><th style="width:4em;">商品名</th><th>MacBook early 2016(梅)</th></tr>
  <tr><td>重さ</td><td>0.92kg/台</td></tr>
  <tr><td>価格</td><td>14万8800円(税別)</td></tr>
</table>
<table>
  <tr><th style="width:4em;">商品名</th><th>山のマグロ</th></tr>
  <tr><td>重さ</td><td>0.35g/尾</td></tr>
  <tr><td>価格</td><td>時価</td></tr>
</table>

単純にgrep -e “商品名” -e “価格”などとやると、表の上の説明文まで引っ掛けてしまいます。そこでhtmlタグに囲まれた商品名と価格だけ抜き出してやる必要があります。

$ grep -e ">商品名<" -e ">価格<" test.html
    <tr><th style="width:4em;">商品名</th><th>謎のみかん</th></tr>
    <tr><td>価格</td><td>277万7778円(税別)</td></tr>
    <tr><th style="width:4em;">商品名</th><th>MacBook early 2016(梅)</th></tr>
    <tr><td>価格</td><td>14万8800円(税別)</td></tr>
    <tr><th style="width:4em;">商品名</th><th>山のマグロ</th></tr>
    <tr><td>価格</td><td>時価</td></tr>

これでOK。

sedでhtmlタグを除去する

取得したhtmlソースから単純にhtmlタグを除去するにはsedで一撃です。

$ grep -e ">商品名<" -e ">価格<" test.html | sed 's/<[^>]*>//g'
    商品名謎のみかん
    価格277万7778円(税別)
    商品名MacBook early 2016(梅)
    価格14万8800円(税別)
    商品名山のマグロ
    価格時価

htmlタグは必ず<で始まって>で終わり、その内側の文字は不定です。 言い換えると先頭<と終端>の内側にある文字は>以外の何かです。

正規表現で指定した文字群のうちの一文字を表すには[と]で囲み、指定した文字群を除外した一文字を表すには[^と]で囲みます。 従ってhtmlタグを正規表現を使って表すと<[^>]*>になります。

ちなみに*は直前の一文字の0個以上の繰り返しを表します。

trでインデントを外す

sed ‘s/^ *//’でもインデントは外せますが、スペースとタブが入り乱れていると面倒です。trはsedと同じく文字置換コマンドですが、tr -dとすれば置換ではなく単純に削除ができます。

ここではPOSIX文字クラスを使って処理しています。(sedでも使えます。)

$ grep -e ">商品名<" -e ">価格<" test.html | sed 's/<[^>]*>//g' | tr -d "[:blank:]"
商品名謎のみかん
価格277万7778円(税別)
商品名MacBook early 2016(梅)
価格14万8800円(税別)
商品名山のマグロ
価格時価

一部のPOSIX文字クラスと正規表現を対比してみます。

意味 POSIX文字クラス 正規表現
文字クラス記法 略記法
アルファベットと数字 [:alnum:] [0-9A-Za-z] [\d\u\l]
数字 [:digit:] [0-9] \d
アルファベット [:alpha:] [A-Za-z] [\u\l]
大文字 [:upper:] [A-Z] \u
小文字 [:lower:] [a-z] \l
スペースや水平タブ [:blank:] [ \t]