飽きる前にそれなりに形になったのでリリースしておきます
Python 2.5*とBeautifulSoup 3.0.7* or 3.1.0*の環境でとりあえず動くXPathEvaluatorです。
アーカイブファイル(ZIP):BSXPath.py: XPathEvaluator Extension for BeautifulSoup
上記ファイル(BSXPath.py)を使ったサンプルはこちら
【2009/04/05追記】
BSXPath.pyを使ったサービスを公開しました。
任意のサイトのフィードパターンを作成・共用できるサービス
使い方
from BSXPath import BSXPathEvaluator,XPathResult #*** 準備 document = BSXPathEvaluator(<html>) # html: HTMLテキスト # ※BSXPathEvaluator は BeautifulSoup のサブクラスです。 # 得られたオブジェクト(document)で BeafutifulSoup のメソッドも使えます。 #*** 基本操作 result = document.evaluate(<expression>,<node>,None,<type>,None) # expression: XPath表現 # node : 基準となるコンテキストノード(BSXPathEvaluatorの戻り値(document)がROOTとなります) # type : XPathResult.<name> (結果として取得したい形式) # name : ANY_TYPE(0), NUMBER_TYPE(1), STRING_TYPE(2), BOOLEAN_TYPE(3) # UNORDERED_NODE_ITERATOR_TYPE(4), ORDERED_NODE_ITERATOR_TYPE(5) # UNORDERED_NODE_SNAPSHOT_TYPE(6), ORDERED_NODE_SNAPSHOT_TYPE(7) # ANY_UNORDERED_NODE_TYPE(8), FIRST_ORDERED_NODE_TYPE(9) # ※第3引数(resolver)と第5引数(result)はNone固定です(未実装) # --- XPathResult.ANY_TYPE(0) 指定時 type = result.nodeType # XPathResult.NUMBER_TYPE(1)/STRING_TYPE(2)/BOOLEAN_TYPE(3)/UNORDERED_NODE_ITERATOR_TYPE(4)のいずれかが # 返るので、これに応じて処理を実施 # --- XPathResult.NUMBER_TYPE(1) 指定時 value = result.numberValue # --- XPathResult.STRING_TYPE(2) 指定時 value = result.stringValue # --- XPathResult.STRING_TYPE(3) 指定時 value = result.booleanValue # --- XPathResult.ANY_UNORDERED_NODE_TYPE(8) or type==XPathResult.FIRST_ORDERED_NODE_TYPE(9) 指定時 value = result.singleNodeValue # --- XPathResult.UNORDERED_NODE_ITERATOR_TYPE(4)/ORDERED_NODE_ITERATOR_TYPE(5) # /UNORDERED_NODE_SNAPSHOT_TYPE(6)/ORDERED_NODE_SNAPSHOT_TYPE(7)のいずれか指定時 length = result.snapshotLength node = result.snapshotItem(<number>) for i in range(length): value = result.snapshotItem(i) #*** WRAPPER関数 nodes = document.getItemList(<expression>[,<node>]) # ノードのリストを返す first = document.getFirstItem(<expression>[,<node>]) # 先頭ノードのみを返す # expression: XPath表現 # node : 基準となるコンテキストノード(デフォルトはBSXPathEvaluatorの戻り値(document))
サンプル
from BSXPath import BSXPathEvaluator,XPathResult html = """ <html><head><title>Hello, DOM 3 XPath!</title></head> <body><h1>Hello, DOM 3 XPath!</h1><p>This is XPathEvaluator Extension for BeautifulSoup.</p> <p>This is based on JavaScript-XPath!</p></body> """ document = BSXPathEvaluator(html) result = document.evaluate('//h1/text()[1]',document,None,XPathResult.STRING_TYPE,None) print result.stringValue # Hello, DOM 3 XPath! result = document.evaluate('//h1',document,None,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,None) print result.snapshotLength # 1 print result.snapshotItem(0) # <h1>Hello, DOM 3 XPath!</h1> nodes = document.getItemList('//p') print len(nodes) # 2 print nodes # [<p>This is XPathEvaluator Extension for BeautifulSoup.</p>, <p>This is based on JavaScript-XPath!</p>] first = document.getFirstItem('//p') print first # <p>This is XPathEvaluator Extension for BeautifulSoup.</p>
謝辞
- XPath解析のロジックはid:amachangさんのJavaScript-XPathのものをほとんどそのまま使わせていただきました。移植するだけでも相当大変だったのに、いちから作成されたamachangさんはほんとにすごい!
- BeautifulSoupを提供して下さっているLeonard Richardsonさんにも感謝!
覚書など
- いまだにXPathもついでにDOMもよく把握していないので、きっと動作は怪しいと思います(をい)*1。
- 一応、
http://svn.coderepos.org/share/lang/javascript/javascript-xpath/trunk/test/functional/data/
を使った試験はしています。
2009/3/24現在のデータ(0000〜0012)において、0002のうちの2つがNG、あとはOKとなっています。
0002でNGなのは、'.//blockquote/text()'と'.//blockquote/node()'。
BeautifulSoupの特性なのか、'<...>\n <...>'のようなHTMLがあった場合、テキストノードとして後ろのタグ前のスペースが無視されてしまう模様。根が深そうなので対応困難っぽいです…。 - アーカイブファイルには試験用スクリプト(TEST_BSXPath.py)と、まとめて試験する用のWindowsコマンドプロンプト用バッチファイル(testbsx.cmd)(とそのテスト結果)を同梱しています。
バッチファイルを実行すると".\testbsxresult"フォルダを作ってその中に結果を保存します。 - BeautifulSoupは3.1.0*よりも3.0.7*の方が、Parseエラーが出にくいようです。
Currently the 3.0.x series is better at parsing bad HTML than the 3.1 series.
- 速度的な面は期待しないで下さい。結構遅いかもです。速くする方法があったら教えて下さい。
- Pythonも初心者なので、かなりおかしな書き方をしていると思われます。こうした方がよいというアドバイスは歓迎です。
*1:こういうの作るならW3Cの仕様を読込まにゃならんのだろうけれども…。