私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「HTMLをスクレイプしてCSVに変換しよう」(CodeIQ)を参照してください。
あなたはとあるWebページに含まれるテーブルの中身をスクレイプして、Excelなどで集計がしやすいようCSVファイルに変換する仕事を任されました。
またその仕事は定期的に行う必要があるため、手作業で行うのではなく自動化したいとのこと。
そこで、HTMLからCSVに変換するプログラムを作ることになりました。
求められるプログラムの前提条件は、以下の通りとなります。
標準入力から、HTML準拠(Wikipedia参照)のテキストデータが送られる
入力されるHTMLはフォーマットが正しい、具体的にはW3Cの構文検証サイトでエラーがないことを前提とする
また、HTMLはUTF-8でエンコーディングされ、多バイト文字は含まれないものとする
HTMLには、tableタグが1つだけ含まれる(テーブルは単体である)
テーブル内のセルはtr, th, tdタグで構成され、セルが連結されることはない
HTML内のテーブルを表形式として抽出し、CSVフォーマット(Wikipedia参照)で標準出力に返すこと
出力するCSVはコンマ「,」(U+002C) 区切りで、すべてのフィールドをダブルクォート「"」(U+0022)で囲むこと
CSVのフィールドとして出力される文字列は、thまたはtdタグ内のテキストに限定すること
thおよびtdタグの扱いは、CSV出力時において変える必要はない
thおよびtdタグ内に、さらにタグが含まれる場合、タグ自体は除去し配下のテキストだけを抽出すること
文字列フィールドは文字実体参照(Wikipedia参照)を用いてアンエスケープ処理をすること
ただし、<(小なり記号)、>(大なり記号)、&(アンパサンド)のみの対応でよいものとする
以下、置換例となります。
【入出力サンプル】
標準入力
<!DOCTYPE html> <html lang="en">
<head>
<meta charset="utf-8"/>
<title>title</title>
</head>
<body>
<table>
<tr>
<th>header1</th>
<th>header2</th>
</tr>
<tr>
<td>100</td>
<td>this is <b>content</b></td>
</tr>
</table>
</body>
</html>
標準出力
"header1","header2"
"100","this is content"
Rubyで解答しています。
#!/usr/bin/ruby # 1行分のデータを配列にする def splitRow(line) data = [] splited = line.split("</td>") for s in splited # タグを削除 ut = s.gsub(/<.+?>/, "") # アンエスェープ us = ut.gsub("<", "<").gsub(">", ">").gsub("&", "&") data << '"' + us + '"' end return data end # htmlデータのテーブル内のデータを配列に変換する def getTableRows(html) ret = [] md = html.match(/<table.*?>(.*)<\/table>/) table = md[1].gsub(/<th.*?>/, "<td>").gsub(/<td.*?>/, "<td>").gsub(/<\/th>/, "</td>").gsub(/<tr.*?>/, "").split("</tr>") for t in table ret << splitRow(t) end return ret end # 配列をCSV形式で表示する def printAsCsv(data) for d in data puts d.join(",") end end # main html="" while line = gets line.strip! if line.empty? then next end html += line end data = getTableRows(html) printAsCsv(data)
特に難しいわけではありません。正規表現に強ければもっと簡潔にかける気がします。
ポイントはフォーマットを可能な限り統一してから処理することだと思います。
入力値はどこで開業されているかわからないので、まず入力値から開業を取り払って全て連結してしまいます。その後、<table>〜</table>を抜き出します。
次に<th>l;と</th>l;は<td>l;と</td>l;に置き換えてしまいます。
そのデータに対し、<tr>〜</tr>を一つずつ取り出し、さらにそこから<td>l;と</td>l;を順次取り出します。そして、そのデータに含まれるタグを削除し、アンエスケープ処理してダブルクォーテーションマークで囲んでやれば良いわけです。
入力値を一つの文字列に連結し、getTableRows()に渡します。
getTableRows()は入力値からタグを取り去ったのテーブルの項目を行毎の配列として返すので、その結果をprintAsCsv()で印字します。
htmlデータからタグを取り去ったのテーブルの項目を行毎の配列として返します。
22行目で<table>〜</table>の間を抜き出します。
23行目で<th>l;と</th>l;は<td>l;と</td>l;に置き換えます。
25〜27行目のループで1行ずつデータを取り出し、結果として記録します。
1行分のデータを配列に変換します。
</td>を区切り文字として分割します。すると1項目ずつの配列(<td>とその他のタグは残っている)になります。
その項目毎にループで(7〜14行目)次の処理をします。
<と>で囲まれた部分(最小マッチ)を除きます。これでhtmlタグを全て削除できます。
次にアンエスケープ処理を行います。
これで必要なテキストができるのでダブルクォーテーションマークで囲んで結果に追加します。
二次元配列を1行ずつコンマで連結して印字するだけです。
一応、タグに属性が記述されていたり、変なところ(<td>と</td>の間とか)で改行されていても大丈夫なようにしましたが、テストケースにはそういうデータはありませんでした。ちょっとテストケースが不十分ではないでしょうか?