python で、xpath の simplify

XPath クエリの結果として返るはずのリーフノードの名前を「XPathから」知る必要に迫られ…。

理由は聞かないでくれ。あまりに馬鹿げてるので説明する気にもなれない。

というわけで、XPath のパーサ的なもの、が必要になり。

ちょっと探したらすぐに見つかった。なぜかドキュメントされていないのだが、標準添付ライブラリの xml.etreeの中に、こっそり「ElementPath」というモジュールがいて、これがちょっとした XPath 処理を担っている。(モジュール名は、Python 3.x でも ElementPath のまま。)

そもそも Python 標準添付には「XMLのフルスペック」を扱えるだけのものはないので、同じく XPath にもそれは言えて、当然 ElementPath も「完全」では全くなくて。ただ、使ってみたら案外良い:

 1 # -*- coding: utf-8 -*-
 2 from xml.etree.ElementPath import xpath_tokenizer
 3 
 4 def xpath_simplify_tokenize(xpath, nsmaps={}):
 5     depth = 0
 6     for tok in xpath_tokenizer(xpath, nsmaps):
 7         if tok[0] == '[':
 8             depth += 1
 9         elif tok[0] == ']':
10             depth -= 1
11         elif depth == 0:
12             yield tok
13 
14 print(list(
15         xpath_simplify_tokenize(
16             '/books/author[last-name [position()=1]= "Bob"]')))
17 
18 # ancestor::は無論「namespace」ではないが、xpath_tokenizer は「馬鹿」なので。
19 # そもそも xpath フルスペックを扱おうとするものではないので…。
20 # 戻りには {ancestor}: のように、python 文字列の format が扱える形("{a}".format(a=1)という
21 # 具合)で返るので、nsmaps の工夫で多分なんとかなる。
22 print(list(
23         xpath_simplify_tokenize(
24             'ancestor::author[last-name [position()=1]= "Bob"]',
25             {'ancestor': 'ancestor'})))

パーザではなくトークナイザなんだけれど、逆にトークナイザであることが功を奏しているというか。

何をしたいかと言えば、XPath の述語部分(predicate)を取り除きたい。クエリ結果の末端ノードを知りたいだけの目的に、述語だけがとてつもなく邪魔なのだわ。

実行結果はこんなね:

1 me@host: ~$ python aaa.py
2 [('/', ''), ('', 'books'), ('/', ''), ('', 'author')]
3 [('', '{ancestor}:author')]
4 me@host: ~$

当たり前ですが、例えば「/*」なんて XPath はどうしようもないよ。これはクエリ叩いてみねばわからんとです。などなど、根本的には制約だらけ、ですよ。だからさ、XPath から結果のノードを知るなんぞ、発想が間違ってるのです。