私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「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>の間とか)で改行されていても大丈夫なようにしましたが、テストケースにはそういうデータはありませんでした。ちょっとテストケースが不十分ではないでしょうか?