解決済み

RubyのHTMLパーサーで複数のタグを抽出したい

  • 困ってます
  • 質問No.7681702
  • 閲覧数1539
  • ありがとう数1
  • 気になる数0
  • 回答数1
  • コメント数0

お礼率 95% (19/20)

現在、Rubyを用いて
あるWebページから特定のタグの要素を抽出して
テキストデータに出力したいと思い、プログラムを書いているのですが、
途中で行き詰ってしまいました。
ちなみにHpricotやNokogiriといったHTMLパーサーを試してみました。
パーサーを使うのは初めてです。

例えば以下のようにHTML文書に抽出したい部分がDIVタグとPタグに囲まれていた場合、
=====================================
・・・
<div class="content">
<div class="header">不要部分</div>
<div class="title">抽出したい文字列1</div>
<div class="subtitle">抽出したい文字列2</div>
<div class="subtitle">抽出したい文字列3</div>
<p class="paragraph">抽出したい文字列4</p>
<p class="paragraph">抽出したい文字列5</p>
<div class="comment">抽出したい文字列6</div>
<div class="footer">不要部分</div>
</div>
・・・
=====================================

このHTMLからまずは
抽出したい文字列1
抽出したい文字列2
抽出したい文字列3
抽出したい文字列4
抽出したい文字列5
抽出したい文字列6
といった出力が得たいのです。

プログラムも書いてみました。

=====================================
#ドキュメント全体を取得(dataにはHTMLの文字列が入っている)
html = Hpricot(data)
#内容部分(contentクラスのdiv)を取得(CSSセレクタで記述)
content = html/"div.content"

=====================================

ここまでは良いのですが、
そのあと、div要素を取り出すだけであれば
headerクラスと、footerクラスを抽出しないように

=====================================
(content/"div:not(.header):not(.footer)").each{ |line|
puts line.inner_html
}
=====================================

とすることで、

抽出したい文字列1
抽出したい文字列2
抽出したい文字列3
抽出したい文字列6

が得られますが、pタグをカンマでdivの前に追加して

=====================================
(content/"p,div:not(.header):not(.footer)").each{ |line|
puts line.inner_html
}
=====================================

のようにすると、

抽出したい文字列4
抽出したい文字列5
抽出したい文字列1
抽出したい文字列2
抽出したい文字列3
抽出したい文字列6

のように出力されます。

同様にdivの後にpを追加し

=====================================
(content/"div:not(.header):not(.footer),p").each{ |line|
puts line.inner_text
}
=====================================

のようにすると


抽出したい文字列1
抽出したい文字列2
抽出したい文字列3
抽出したい文字列6
抽出したい文字列4
抽出したい文字列5

のようになってしまいます。
つまり、複数のタグを指定すると、指定した順序で抽出されるようです。
代わりに子供すべてを列挙するchildのようなものがあるかと、調べてみたのですが、
どうやらそのような書き方はないようです。

複数のタグを含む場合にはHTMLパーサーでは解析できないのでしょうか。
パーサーは抽出時に順番を保証はしてくれないのでしょうか。

あきらめて、正規表現で抽出しようと思いましたが、
=====================================
<div class="comment">
<div class="comment_header">ごちゃごちゃ</div>
<div class="comment_body">抽出したい要素6</div>
</div>
=====================================
などDIVが入れ子となっている場合に、
非常にややこしく感じたので
お手上げ状態です。

どのように、解決できるでしょうか。
よろしくお願いします。

質問者が選んだベストアンサー

  • 回答No.1

ベストアンサー率 48% (4416/9106)

CSSであまり複雑な指定をしたこと無いのですが、おそらくXPathのほうが細かい指定が出来ます。
Nokogiriでしか確認していませんが、おそらくHpricotでもXPath指定が出来ると思います。

content / "./*[@class!='header' and @class!='footer']"

./* が直下の任意のノードで、[ ] 内がその選択条件です。
あるいはまとめて、htmlから直接、

html / "//div[@class='content']/*[@class!='header' and @class!='footer']"

任意のノードでなく、div と p だけに限るなら、

content / "./*[((name()='p')or(name()='div')) and @class!='header' and @class!='footer']"
お礼コメント
Rupyjp

お礼率 95% (19/20)

どうもありがとうございます。
結論から言いますとHpricotからはできませんでしたが
Nokogiriで成功しました!!

Hpricotで一生懸命あれやこれややってみましたが、
結局できませんでした。
なぜか、ワイルドカード指定*がパス直下ではなく
すべての子孫の要素に適応されるようです。
バグなのか、仕様なのかわかりませんが。
何がおかしいんでしょう。

検証に使用したのは以下のコードです↓
==========================================
#coding: utf-8

require 'rubygems'
require 'hpricot'

data = <<"END"
<html>
<body>
<div class="content">
<div class="header">不要部分</div>
<div class="title"><b>抽出</b>したい文字列1</div>
<div class="subtitle">抽出したい文字列2</div>
<div class="subtitle">抽出したい文字列3</div>
<p class="paragraph">抽出したい文字列4</p>
<p class="paragraph">抽出したい文字列5</p>
<div class="comment">抽出したい文字列6</div>
<div class="footer">不要部分</div>
</div>
</body>
<html>
END

html = Hpricot(data)
content = html/"div.content"

(content/"./*[((name()='p')or(name()='div')) and @class!='header' and @class!='footer']").each_with_index{ |line,i|
print i
print ":"
puts line.inner_html
}
==========================================
結果は
==========================================
1:
2:不要部分
3:
4:
5:<b>抽出</b>したい文字列1
6:抽出
7:
8:
9:
10:抽出したい文字列2
11:
12:
13:抽出したい文字列3
14:
15:
16:抽出したい文字列4
17:
18:
19:抽出したい文字列5
20:
21:
22:抽出したい文字列6
23:
24:
25:不要部分
26:
27:
==========================================
というおかしなものになってしまいました。
さらに最後のputs line.inner_htmlをpp lineにすると
いろいろとんでもないことになっているようです。

しかしNokogiriを使うと結果は
==========================================
0:<b>抽出</b>したい文字列1
1:抽出したい文字列2
2:抽出したい文字列3
3:抽出したい文字列4
4:抽出したい文字列5
5:抽出したい文字列6
==========================================
として正しく抽出できました。

とりあえず、Nokogiriでうまくできたので満足しています。
質問して良かったです。
ありがとうございました。
また機会がありましたらよろしくお願いします。
投稿日時 - 2012-09-06 14:40:03
結果を報告する
このQ&Aにはまだコメントがありません。
あなたの思ったこと、知っていることをここにコメントしてみましょう。
関連するQ&A
AIエージェント「あい」

こんにちは。AIエージェントの「あい」です。
あなたの悩みに、OKWAVE 3,600万件のQ&Aを分析して最適な回答をご提案します。

その他の関連するQ&A、テーマをキーワードで探す

キーワードでQ&A、テーマを検索する

ピックアップ

ページ先頭へ