usp Tukubai

HTMLで書かれたデータを取り込む

問題

みずほ銀行の宝くじ公式ページに載っている当せん番号一覧を表にして保存したい。ただし、Webに掲載されているデータはHTMLになっているため、マークアップに関するデータが含まれている。これを外し、プレーンテキストの表にしたいというケースを考える。

解答

まず、curl(1)コマンドで目的のURLからHTMLテキストをダウンロードする。FreeBSDを使っているならシステムにデフォルトでインストールされているfetch(1)コマンドを使ってもよい。あとはこれを、sed(1)やawk(1)などのコマンドを使って整形する。今回のケースでの整形のコツは、一旦HTMLテキストの中にある改行を全部取り去ってから、必要なタグの前 (または前後) にだけ改行を入れることにある。

サンプルプログラム

宝くじの公式ページで公開されている「ジャンボ宝くじ」の当せん番号を収集するプログラムを次に掲載する。なお、当せん番号案内のトップは各抽せん回へのリンクになっており、実際の番号はそのリンク先にあったため、トップページでリンクを収集した後、さらに各々のページへアクセスし、当せん番号を収集している。

=====================================================================
リスト1.LOTTERY.DB:「ジャンボ宝くじ」当せん番号案内一覧を収集するプログラム
======================================================================
#!/bin/sh
#
################################################################
# LOTTERY.DB 当たりたいなら買うしかない。宝くじデータベース!!
# (SHELLというディレクトリを作成し、その下に配置してください)
#
# Written by N.Tounaka(tounaka@usp-lab.com) / Date : 13 Aug.2012
# Arrenged by USP MAGAZINE(mag@uap-lab.com) / Date : 08 Sep.2012
################################################################

# シェル変数のセット
homedir=$(pwd); homedir=${homedir%/*}
pomd=$homedir/POMPA
top="http://www.mizuhobank.co.jp"
LF=$(printf '\\\012_');LF=${LF%_}

# tacコマンドの定義(*BSDのみ)
[ -n "$(uname | grep BSD)" ] && alias tac='tail -r'

# 1.何はともあれ、宝くじ当せんデータベース(テキスト表)をつくります
# 宝くじ公式ページに記されているジャンボ宝くじの一覧を取り出す
curl -s ${top}/takarakuji/tsujyo/jumbo/index.html             |
# 必要な区間の行(<table>~</table>を抽出
sed -n '/<table/,/<\/table>/p'                                |
# 一旦改行コードを全部取り去ってしまう
tr -d '\n'                                                    |
# <a>タグの処理: リンクURLとリンク文字列だけ抜き出す
sed 's!<a [^>]*href="\([^"]*\)"[^>]*>\([^<]*\)</a>!\1 \2!g'   |
# <th>タグを<td>と同等に扱うため、置換
sed 's!<th!<td!g;s!</th!</td!g'                               |
# <tr>タグが出てきたらその手前に改行コードを挿入
sed 's/<tr/'"$LF"'<tr/g'                                      |
# <td>タグが出てきたらタグを外し,印"@"を付け,前後に改行を挿入
sed 's!<td[^>]*>\([^<]*\)</td>!'"$LF"'@\1'"$LF"'!g'           |
# <td>タグ要素はスペース区切り,<tr>タグでは改行し,テキスト表に
awk '/^<tr/{if(time>0){print ""}else{printf(" ")}time++}      \
     /^@/  {printf("%s ",$0)}                                 \
     END   {print ""}'                                        |
# <td>タグ要素のうち、空だったものを "_" に変換する
sed 's/ @ / _ /g'                                             |
# <td>タグ要素に付けていた印"@"を完全に除去
tr -d '@'                                                     |
# テーブルの項目名(元<th>要素)の行を取り除く
tail -n +2                                                    |
# "yyyy年m月d日"→"yyyymmdd"
sed 's/^\([0-9]*\)年\([0-9]*\)月\([0-9]*\)日/\1,0\2,0\3/'     |
sed 's/^\([0-9]*\),0*\([0-9][0-9]\),0*\([0-9][0-9]\)/\1\2\3/' |
# 抽せん日、URL、宝くじ名を順番に取り出し、
while read day url name; do
  # 当せん番号を調べる
  curl -s ${top}${url}                                          |
  # 先程と同様に、HTML中のテーブル要素をテキスト表に変換
  sed -n '/<table/,/<\/table>/p'                                |
  tr -d '\n'                                                    |
  sed 's!<th!<td!g;s!</th!</td!g'                               |
  sed 's/<tr/'"$LF"'<tr/g'                                      |
  sed 's!<td[^>]*>\([^<]*\)</td>!'"$LF"'@\1'"$LF"'!g'           |
  awk '/^<tr/{if(time>0){print ""}else{printf(" ")}time++}      \
       /^@/  {printf("%s ",$0)}                                 \
       END   {print ""}'                                        |
  sed 's/ @ / _ /g'                                             |
  tr -d '@'                                                     |
  # &emsp; なる文字列を含む場合があるのでコイツを空白に変換
  sed 's/&emsp;/ /g'                                            |
  # 1等前後賞のためのレコード追加
  tac                                                           |
  awk '{if($1=="1等の前後賞"){s=$1;m=$2}                        \
        else if($1=="1等")                                      \
          {print;print s,m,$3,$4+1"番";print s,m,$3,$4-1"番"}   \
        else print                                           }' |
  # 1等組違い賞のためのレコード追加
  awk '{if($1=="1等の組違い賞"){s=$1;m=$2}                      \
        else if($1=="1等"){print;print s,m,"各組共通",$4}       \
        else print}'                                            |
  awk 'NF==4'                                                   |
  sort -n -k1,1                                                 |
  awk '{print "'$day' '$name'",$0}'                             ;
# LOTTERY_DAT 1:抽せん日 2:宝くじ名 3:等 4:賞金 5:組 6:番号
done                                        > $pomd/LOTTERY_DAT

# 2.抽せん日選択のドロップダウンリスト生成用に、インデックスを作る
self 2 $pomd/LOTTERY_DAT            |
uniq                                |
# KIND_LIST   1:インデックス 2:宝くじ名
awk '{printf("%02d %s\n", NR, $1)}' > $pomd/KIND_LIST

# 3.組番号選択のドロップダウンリスト生成用に、インデックスを作る
seq 99                              |
awk '{printf("%02d\n",$1)}'         |
# GROUP_LIST  1:インデックス 2:組
self 1 1                            > $pomd/GROUP_LIST

exit 0

宝くじには「前後賞」や「組違い賞」といった変則的なデータがあるので、それらについても展開するようにしてある。最終的に第一フィールドから、抽せん日、宝くじ名、等、賞金、組、番号というテキスト表データ (LOTTERY_DAT) として抽出している。

このスクリプトによって生成されるテキストデータは次のとおり。

20120807 第625回全国自治宝くじ(2000万サマー) 1等 2000万円 組下1ケタ0組 120302番
20120807 第625回全国自治宝くじ(2000万サマー) 1等 2000万円 組下1ケタ0組 139302番
20120807 第625回全国自治宝くじ(2000万サマー) 1等 2000万円 組下1ケタ1組 101210番
20120807 第625回全国自治宝くじ(2000万サマー) 1等 2000万円 組下1ケタ2組 136935番
20120807 第625回全国自治宝くじ(2000万サマー) 1等 2000万円 組下1ケタ8組 146564番
20120807 第625回全国自治宝くじ(2000万サマー) 1等の前後賞 10万円 組下1ケタ0組 120301番
20120807 第625回全国自治宝くじ(2000万サマー) 1等の前後賞 10万円 組下1ケタ0組 120303番
20120807 第625回全国自治宝くじ(2000万サマー) 1等の前後賞 10万円 組下1ケタ0組 139301番
20120807 第625回全国自治宝くじ(2000万サマー) 1等の前後賞 10万円 組下1ケタ0組 139303番
20120807 第625回全国自治宝くじ(2000万サマー) 1等の前後賞 10万円 組下1ケタ1組 101209番
20120807 第625回全国自治宝くじ(2000万サマー) 1等の前後賞 10万円 組下1ケタ1組 101211番
20120807 第625回全国自治宝くじ(2000万サマー) 1等の前後賞 10万円 組下1ケタ2組 136934番
20120807 第625回全国自治宝くじ(2000万サマー) 1等の前後賞 10万円 組下1ケタ2組 136936番
20120807 第625回全国自治宝くじ(2000万サマー) 1等の前後賞 10万円 組下1ケタ8組 146563番
20120807 第625回全国自治宝くじ(2000万サマー) 1等の前後賞 10万円 組下1ケタ8組 146565番
20120807 第625回全国自治宝くじ(2000万サマー) 2等 1万円 下3ケタ 871番
20120807 第625回全国自治宝くじ(2000万サマー) 3等 300円 下1ケタ 5番
20120807 第624回全国自治宝くじ(サマージャンボ) サマーバケーション賞 10万円 下4ケタ 1211番
20120807 第624回全国自治宝くじ(サマージャンボ) 1等 4億円 41組 105615番
20120807 第624回全国自治宝くじ(サマージャンボ) 1等の前後賞 5000万円 41組 105614番
20120807 第624回全国自治宝くじ(サマージャンボ) 1等の前後賞 5000万円 41組 105616番
20120807 第624回全国自治宝くじ(サマージャンボ) 1等の組違い賞 10万円 各組共通 105615番
20120807 第624回全国自治宝くじ(サマージャンボ) 2等 500万円 68組 185568番
20120807 第624回全国自治宝くじ(サマージャンボ) 2等 500万円 77組 162668番
20120807 第624回全国自治宝くじ(サマージャンボ) 3等 100万円 各組共通 189984番
20120807 第624回全国自治宝くじ(サマージャンボ) 4等 1万円 下3ケタ 546番
20120807 第624回全国自治宝くじ(サマージャンボ) 5等 3000円 下2ケタ 36番
20120807 第624回全国自治宝くじ(サマージャンボ) 6等 300円 下1ケタ 5番
20120612 第621回全国自治宝くじ(ドリーム10(TEN)) 1等 10万円 下3ケタ 484番
                    :

解説

このプログラムにはOpen usp Tukubaiのコマンドが一つも出てこないのだが、Webアプリ開発でHTMLやXMLのパースは欠かせない話ということで取り上げた。大抵の言語ではDOMやSAXを使うところ、シェルスクリプトならsed(1)やawk(1)、tr(1)といったテキストを処理するコマンドだけでこなすことができる。

改行を入れ直す

シェルスクリプトでHTMLやXMLのタグをパースするコツは、改行を一旦全部除去してから必要なところにだけ再び挿入する点にある。例えばHTMLには<table>というタグがある。この中には<tr>や<td>といった行、列を示すタグがある。このテキストから行、列を正しく認識してテーブルの中身を取り出すにはどうすればよいだろうか。

まずsed(1)で<tr>の前後に改行を入れ、<tr>を単独行にしておけばよい。その後awk(1)で"<tr"始まり行を読み込んだ時に、新しいレコードに移ったと認識できる。<td>に関しても、<td>~</td>という単位で単独行にしておけば、やはり"<td"始まりの行を一つの列であると認識して、値を収集できる。

この時<td>から</td>の間に改行があると都合が悪いので、最初にあらかじめ取り去っておく。

不正なHTMLも問題なし

Webに存在するHTMLのほとんどは何らかの不正な記述になっていると指摘されている。みずほ銀行のジャンボ宝くじ当せん番号案内トップページのHTMLデータも少々間違っている。抽せん回一覧ページで、抽せん日の<td>が</th>で閉じられている(2012年10月現在)。

DOMやSAXといったパーサを使う場合、このあたりの処理をパーサに依存せざるをえないが、今回のように自前で処理している場合には、自分の求めるように処理させることができる。

usp Tukubaiユニバーサル・シェル・プログラミング研究所の登録商標。

※1 Open usp Tukubaiは最新バージョンを採用のこと。古いバージョンでは適切に動作しない可能性がある。

※2 USP MAGAZINE 2012 autumnより加筆修正後転載。

※3 本ページで公開されているプログラムとそれに付随するデータの著作権およびライセンスは、特に断りがない限りOpen usp Tukubai本体と同じMITライセンスに準拠するものとする。

last modified: 2014-01-13 16:01:13