--- title: HTMLの全要素について同じパスごとに出現回数を数える。 tags: Ruby HTML Nokogiri author: jun68ykt slide: false --- # はじめに   今いま、取り組んでいる仕事で、頻繁にHTMLをパースすることがあって、そこで直面した課題と、それを楽に切り抜けたという小話。パーサーはNokogiriです。 # お題   あるHTMLの、とりあえず``の中で、同じパスの位置に出現する要素別に、その出現回数を数えなければならないという、ちょっと面倒なタスクが発生した。ここでいうパスとは、ざっくりいうと、XPathによる表記から n-th を表す `[n]`を除去したもの、ぐらいの意味。   たとえば、こういうHTML ```html Test

あいうえお

なにぬねの

``` があったとして、クラスやid、および n-thは無視して、同じパスに出現する要素をカウントして、できれば多い順に ``` /html/body/ul/li: 4回 /html/body/ul: 2回 /html/body/p: 1回 /html/body/div/p: 1回 /html/body/div: 1回 /html/body: 1回 ``` という結果が欲しい。 # 解法   出現順にソートするのは後でやるとしても、まずは、パスごとに出現回数を数えるようなものを作る必要がある。そのためには、要素のすべてを走査するようなロジックを書かないといけない。しかし、全要素をたどるメソッドを(再帰を使ったりして)自分で書くのは、車輪の再開発以外の何ものでもないなと思って調べたら、ありました。 [Nokogiri::XML::Node#traverse]( http://www.rubydoc.info/gems/nokogiri/Nokogiri%2FXML%2FNode%3Atraverse)   この`traverse`にブロックを渡してやれば、あとは面倒をみてくれそう。これは便利。ということで、以下で切り抜けました。 ```ruby:count_element_paths.rb # coding: utf-8 require 'nokogiri' body = Nokogiri::HTML.parse(<<-TARGET_HTML).css('body')[0] Test

あいうえお

なにぬねの

TARGET_HTML # カウント結果を保存する、パス => 回数のハッシュ(回数の初期値:0) counters = Hash.new(0) # traverseメソッドで、全ノードについて、そのパスをカウント body.traverse do |node| next if node.name == 'text' # テキストは数えない path_ignored_nth = node.path.gsub(/\[\d+\]/,'') # パス文字列から n-th を表す部分を削除 counters[path_ignored_nth] += 1 # カウンターを更新 end # 結果を出力 counters.each do |path, counter| puts "#{path}: #{counter}" end ``` 上記を実行して、以下が得られました。 ```shell-session $ ruby count_element_paths.rb /html/body/p: 1 /html/body/ul/li: 4 /html/body/ul: 2 /html/body/div/p: 1 /html/body/div: 1 /html/body: 1 ```   このあとは、対象のHTMLをファイルなりURL経由のものにしたり、結果をソートしたりなどの作業を淡々とこなして完成。   何か作ろうとしたときに、それのある部分について 「これはきっと、すでに他のデキる人が作ってくれてるハズ」 っていう予感みたいなの大事ですねと、改めて感じた次第。 (まあ、今回の`traverse`は自分で書いても数行の話ではありますけれども。)