風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

Google App EngineでもXPathが使える!XPathEvaluator Extension for BeautifulSoup(BSXPath)をリリース

飽きる前にそれなりに形になったのでリリースしておきます

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の仕様を読込まにゃならんのだろうけれども…。