こんにちは。エンジニアのAraoです。今回は、RubyでHTMLをパースする際に非常によく用いられるNokogiriで、tableを転置するメソッドを紹介します。

Nokogiriとは

Nokogiri

Nokogiriとは、XML、HTMLをパースできるRuby gemで、RubyでWebスクレイピングをする人はほぼ全員が使ったことがあると言ってもいいぐらいのモジュールです。ウェブスクレイピングがメイン事業となっている弊社でも、全員がこのNokogiriを使って仕事をしています。

余談ですが、Railsにも標準でNokogiriが含まれているため、プログラミング初心者がRailsに挑戦しようとしたものの、Nokogiriのインストールにコケて挫折してしまうケースが多いみたいです。Nokogiriのインストールにはgccや特定のライブラリが必要なので、インストールにコケた場合は公式のインストール方法を参考にするのが良いと思います。

Installing Nokogiri – Nokogiri

tableの転置とは

行列を扱ったことのある方には転置(transpose)はおなじみだと思います。そうでない方は、表の縦方向の番号と横方向の番号を入れ替える操作のことと思っていただければ、とりあえずは問題ないかと思います。具体的には以下のようなイメージになります。

1 2 3
4 5 6
  ↓transpose
1 4
2 5
3 6

1 2 3
4 5 6
7 8 9
  ↓transpose
1 4 7
2 5 8
3 6 9

HTMLのtableタグは、表組みを実現するためのタグです。tableタグで表全体を、trタグで行を、thタグもしくはtdタグでひとつひとつのセルを、それぞれ表現します。ブラウザではこんな感じに表示されているものは、HTMLではこんな風に書かれています。hoge、piyo、fugaの各属性は後でメソッドを紹介するときのためのものなので、今は無視してしまって大丈夫です。

<table border="1" hoge="hoge!" fuga="fuga!">
  <tr>
    <th piyo="piyo!">1_1</th>
    <th>1_2</th>
    <th piyo="piyo!">1_3</th>
    <th>1_4</th>
  </tr>
  <tr>
    <td>2_1</td>
    <td piyo="piyo!">2_2</td>
    <td>2_3</td>
    <td piyo="piyo!">2_4</td>
  </tr>
  <tr>
    <td piyo="piyo!">3_1</td>
    <th>3_2</th>
    <td piyo="piyo!">3_3</td>
    <td>3_4</td>
  </tr>
  <tr>
    <td>4_1</td>
    <td piyo="piyo!">4_2</td>
    <td>4_3</td>
    <td piyo="piyo!">4_4</td>
  </tr>
  <tr>
    <td piyo="piyo!">5_1</td>
    <td>5_2</td>
    <td piyo="piyo!">5_3</td>
    <td>5_4</td>
  </tr>
</table>

Webスクレイピングをしていると、内容は同じなのにページによって縦持ちだったり横持ちだったりすることがたまにあります。そんなときにtableの転置ができると、必要な場合にtableの転置を行ってから、共通のパースロジックを適用することが可能なため、ロジック全体の見通しが良くなります。具体的には、以下のようなことがしたい、ということです。

これを

こうする

Nokogiriでtableを転置するメソッドの実装

やっていること自体は意外とシンプルなので、メソッドも割とスッキリしています。また、tableタグとth、tdタグの属性は維持するような仕様になっています。trタグの属性は維持しようがないため、維持しません。

require 'nokogiri'

def transpose_nokogiri_table(table_node)
  # init transpose table node
  transpose_table_node = Nokogiri::XML::Node.new('table', table_node)
  
  # add attributes of original table
  table_node.attributes.each do |key, val|
    transpose_table_node.set_attribute(key, val)
  end
    
  # prepare tr
  table_node.at_css('tr').css('th, td').length.times do
    transpose_table_node.add_child('')
  end
  
  # pick up th and td from original table and put them in transpose table
  table_node.css('tr').each do |tr|
    tr.css('th, td').each_with_index do |td, col_idx|
      transpose_table_node.css('tr')[col_idx].add_child(td.dup)
    end
  end
  
  transpose_table_node
end

メソッドの引数はNokogiriで取り出したtableのNodeで、返り値はそのtableを転置したtableのNodeになります。実際に動作確認をしたいときは、以下のようにしてHTMLをHTMLに変換すると分かりやすいと思います。

html = open('table1.html').read
doc = Nokogiri::HTML.parse(html)
table_node = doc.at_css('table')
transpose_table_node = transpose_nokogiri_table(table_node)
open('table2.html', 'w') do |fp|
  fp.puts transpose_table_node.to_html
end

実際にテーブルを変換してみると、以下のようになります。

<table border="1" hoge="hoge!" fuga="fuga!">
  <tr>
    <th piyo="piyo!">1_1</th>
    <th>1_2</th>
    <th piyo="piyo!">1_3</th>
    <th>1_4</th>
  </tr>
  <tr>
    <td>2_1</td>
    <td piyo="piyo!">2_2</td>
    <td>2_3</td>
    <td piyo="piyo!">2_4</td>
  </tr>
  <tr>
    <td piyo="piyo!">3_1</td>
    <th>3_2</th>
    <td piyo="piyo!">3_3</td>
    <td>3_4</td>
  </tr>
  <tr>
    <td>4_1</td>
    <td piyo="piyo!">4_2</td>
    <td>4_3</td>
    <td piyo="piyo!">4_4</td>
  </tr>
  <tr>
    <td piyo="piyo!">5_1</td>
    <td>5_2</td>
    <td piyo="piyo!">5_3</td>
    <td>5_4</td>
  </tr>
</table>
<table border="1" hoge="hoge!" fuga="fuga!">
  <tr>
    <th piyo="piyo!">1_1</th>
    <td>2_1</td>
    <td piyo="piyo!">3_1</td>
    <td>4_1</td>
    <td piyo="piyo!">5_1</td>
  </tr>
  <tr>
    <th>1_2</th>
    <td piyo="piyo!">2_2</td>
    <th>3_2</th>
    <td piyo="piyo!">4_2</td>
    <td>5_2</td>
  </tr>
  <tr>
    <th piyo="piyo!">1_3</th>
    <td>2_3</td>
    <td piyo="piyo!">3_3</td>
    <td>4_3</td>
    <td piyo="piyo!">5_3</td>
  </tr>
  <tr>
    <th>1_4</th>
    <td piyo="piyo!">2_4</td>
    <td>3_4</td>
    <td piyo="piyo!">4_4</td>
    <td>5_4</td>
  </tr>
</table>

さらに……

ここまで扱ってきたtableには、セル結合が含まれていませんでした。そもそも転置自体がセル結合の存在を全く考慮していないためです。しかし実際のWebページではしょっちゅうセル結合が登場しますし、中には行によってセルの数が異なるtableもあります。そこで転置の前処理として、セル結合があったりセルが不足しているテーブルを対象に、セル結合を解除し、セルの不足を補うメソッドも開発しました。以下のような感じになります。こちらのコードはやや長いため、後ほど別の方法で公開しようと思います。

セル結合の解除とセルの不足を補えば、以下のように転置することができます。

まとめと今後の予定

NokogiriでWebスクレイピングするときに、あると便利なtableを対象にした拡張メソッドの実装を紹介しました。先ほど省略したセル結合を解除してセルの不足を補うメソッドや、table以外を対象にした汎用メソッドがいくつかあるため、それらをまとめたNokogiri拡張gemを作成して公開しようと思っています。公開した際はこちらのブログでも紹介するので、Webスクレイピング専門家の考える便利メソッド群をご期待ください。