MAGAZINE
ルーターマガジン
クローリング時にセル結合されたtableを綺麗にパースする(Table to Array)
こんにちは。学生エンジニアのTsuboiです。
Webクローリング/スクレイピングをする際、しばしばセル結合されたtableをパースする必要が出てきます。
今回は、セル結合されているtableを綺麗にHTMLからArrayに変換するRubyコードをご紹介したいと思います。
セル結合されたtableをパースしたい
以下のようにセル結合されたtableをパースすることを考えます。
ただし、このままArrayに変換しようとすると...
[["Table name", "Col1", "Col2", "Col3", "Col4", "Col5"], ["Row1", "cell1_1", "cell1_2", "cell1_3", "cell1_4", "cell1_5"], ["Row2", "cell2_1", "cell2_2 cell2_3", "cell2_4", "cell2_5"], ["Row3", "cell3_1", "cell3_2", "cell3_3", "cell3_4", "cell3_5
cell4_5"], ["Row4", "cell4_1 cell4_2
cell5_1 cell5_2", "cell4_3", "cell4_4"], ["Row5", "cell5_3", "cell5_4", "cell5_5"]]
このようにセル結合を考慮しないで変換されてしまいます。これでは元のtableを再現できません。
以下のように変換することを目標とします。
[["Table name", "Col1", "Col2", "Col3", "Col4", "Col5"], ["Row1", "cell1_1", "cell1_2", "cell1_3", "cell1_4", "cell1_5"], ["Row2", "cell2_1", "cell2_2 cell2_3", "cell2_2 cell2_3", "cell2_4", "cell2_5"], ["Row3", "cell3_1", "cell3_2", "cell3_3", "cell3_4", "cell3_5
cell4_5"], ["Row4", "cell4_1 cell4_2
cell5_1 cell5_2", "cell4_1 cell4_2
cell5_1 cell5_2", "cell4_3", "cell4_4", "cell3_5
cell4_5"], ["Row5", "cell4_1 cell4_2
cell5_1 cell5_2", "cell4_1 cell4_2
cell5_1 cell5_2", "cell5_3", "cell5_4", "cell5_5"]]
colspanとrowspan
tableの中のセルはHTMLではtd要素で表され、セル結合はcolspanとrowspanという属性で表されます。
例として上のcell2_2,cell2_3とcell3_5,cell4_5をHTMLで見てみると...
<td colspan=2>cell2_2 cell2_3</td>
<td rowspan=2>cell3_5<br>cell4_5</td>
このように横に結合する際はcolspanを、縦に結合する際はrowspanを結合の長さだけ指定します。
ロジックの概要
今回のコードロジックの概要は下図の通りです。
rowspanへの対応
talbeは行を表すtr要素で構成されるため、rowspanを考慮するためには一工夫必要になります。
$rowspan_count = {}
def record(rowspan_value)
$rowspan_count[$html_in_row.length] = {"remaining_rowspan" => rowspan_value - 1, "content" => $cell_node.inner_html}
end
rowspanを記録するグローバルなHashを定義し、 rowspan属性をもつセルに対しては、このHashにrowspanの値とセルの中身を記録します。Hashのキーは列のインデックスです。
def copy_rowspan_content
$html_in_row << $rowspan_count[$html_in_row.length]["content"]
$rowspan_count[$html_in_row.length - 1]["remaining_rowspan"] -= 1
if $rowspan_count[$html_in_row.length - 1]["remaining_rowspan"] == 0
$rowspan_count.delete($html_in_row.length - 1)
end
end
上のセルと結合されている場合は上のセルの中身をコピーし、下のセルと結合されていない場合はHashのキーを削除します。
colspanへの対応
上述の通り、tableは行を表すtr要素で構成されるため、colspanへの対応はさほど難しくありません。
(colspan.value.to_i - 1).times do
$html_in_row << $cell_node.inner_html
end
このようにcolspan - 1の数だけ余分にセルの中身を記録すれば済みます。
コード全容
以下に今回のコードの全容を示します。HTML解析にはNokogiriを用いています。
require "nokogiri"
require "pp"
# input: HTML String
# output: HTML String の二次元配列
def copy_rowspan_content
$html_in_row << $rowspan_count[$html_in_row.length]["content"]
$rowspan_count[$html_in_row.length - 1]["remaining_rowspan"] -= 1
if $rowspan_count[$html_in_row.length - 1]["remaining_rowspan"] == 0
$rowspan_count.delete($html_in_row.length - 1)
end
end
def record(rowspan_value)
$rowspan_count[$html_in_row.length] = {"remaining_rowspan" => rowspan_value - 1, "content" => $cell_node.inner_html}
end
def record_rowspan(rowspan_value, colspan)
if rowspan_value > 1
record(rowspan_value)
unless colspan.nil?
(colspan.value.to_i - 1).times do
$html_in_row << $cell_node.inner_html
record(rowspan_value)
end
end
end
end
def row_to_array(row)
node_index = 0
cell_nodeset = row.css("th,td")
$html_in_row = []
until (cell_nodeset.nil? or node_index >= cell_nodeset.length) and $rowspan_count[$html_in_row.length].nil?
unless $rowspan_count[$html_in_row.length].nil?
copy_rowspan_content
else
$cell_node = cell_nodeset[node_index]
rowspan = $cell_node.attribute("rowspan")
colspan = $cell_node.attribute("colspan")
if not rowspan.nil?
record_rowspan(rowspan.value.to_i, colspan)
elsif not colspan.nil?
(colspan.value.to_i - 1).times do
$html_in_row << $cell_node.inner_html
end
end
$html_in_row << $cell_node.inner_html
node_index += 1
end
end
return $html_in_row
end
def table_to_array(table)
begin
doc = Nokogiri::HTML.parse(table)
rescue => e
puts "parse error"
end
$rowspan_count = {}
html_in_table = []
row_nodeset = doc.css("tr")
row_nodeset.each do |row|
html_in_table << row_to_array(row)
end
return html_in_table
end
if __FILE__ == $0
table_sample = DATA.read
pp table_to_array(table_sample)
end
__END__
<table>
<tr>
<th>Table name</th>
<th>Col1</th>
<th>Col2</th>
<th>Col3</th>
<th>Col4</th>
<th>Col5</th>
</tr>
<tr>
<th>Row1</th>
<td>cell1_1</td>
<td>cell1_2</td>
<td>cell1_3</td>
<td>cell1_4</td>
<td>cell1_5</td>
</tr>
<tr>
<th>Row2</th>
<td>cell2_1</td>
<td colspan=2>cell2_2 cell2_3</td>
<td>cell2_4</td>
<td>cell2_5</td>
</tr>
<tr>
<th>Row3</th>
<td>cell3_1</td>
<td>cell3_2</td>
<td>cell3_3</td>
<td>cell3_4</td>
<td rowspan=2>cell3_5<br>cell4_5</td>
</tr>
<tr>
<th>Row4</th>
<td colspan=2 rowspan=2>cell4_1 cell4_2<br>cell5_1 cell5_2</td>
<td>cell4_3</td>
<td>cell4_4</td>
</tr>
<tr>
<th>Row5</th>
<td>cell5_3</td>
<td>cell5_4</td>
<td>cell5_5</td>
</tr>
</table>
このようにルーターではWebクローリングを便利に・確実に・迅速に行うべく、日夜データ構造と戦っております。皆様もぜひお試し下さい。
CONTACT
お問い合わせ・ご依頼はこちらから