lxml で XML で子要素のテキストをフラットにする。

 下のような XML があったとする。

<document>
    out outer1 <outer> in outer1 <inner> in inner </inner> in outer2 </outer> out outer2
</document>

 ここで、 document 以下のテキストを一気に書き換えたい。
 期待するのは以下の様な動作。(上記 XML を sample.xml とする)

>>> import lxml.html
>>> xml = open('sample.xml', 'rb').read()
>>> root = lxml.html.fromstring(xml)
>>> root.text_content()
'\r\n    out outer1  in outer1  in inner  in outer2  out outer2\r\n'
>>> root.text = "new text"
>>> root.text_content()
'new text' #こうならない

 実際はこうなる

>>> root.text_content()
'new text in outer1  in inner  in outer2  out outer2\r\n'
>>> root.text
'new text'

 Element#text は子要素までのテキストを取ってくる。 text_content() 全部を書き換えたい時はどうすればいいのか?
ということで、タグを外す drop_tag() を使うことにした。

要素のタグを外す Element#drop_tag()

 要素のタグだけを外し、テキストはそのまま。
 これを子要素に適用していく。

>>> root[0]
<Element outer at 0x1e65f60> # 子要素は outer
>>> root[0].drop_tag()
>>> root.text_content()
'new text in outer1  in inner  in outer2  out outer2\r\n' #テキスト全体は変化なし
>>> root.text
'new text in outer1 ' # outer が持っていたテキストを保持
>>> root[0]
<Element inner at 0x1e65f60> # 子要素が inner になっている

 子要素が無くなるまで drop_tag() を実行すれば、全テキストを親要素が保持することになる。

>>> root[0].drop_tag()
>>> root[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "lxml.etree.pyx", line 1069, in lxml.etree._Element.__getitem__ (src/lxml
/lxml.etree.c:37264)
IndexError: list index out of range # 子要素がなくなった
>>> root.text
'new text in outer1  in inner  in outer2  out outer2\r\n'
>>> root.text = "new text 2"
>>> root.text
'new text 2'
>>> root.text_content()
'new text 2' # テキスト全体が置換されている
子要素のリストは list() 関数を HTML Element に使うと得られる。
>>> list(root)
[<Element outer at 0x1f68b10>]
>>> for e in list(root):
...     print e.text
...
 in outer1
>>> root

 ここだと直接の子要素が1つ(outer)しかないので分かりづらいかも

HTML Element を引数に取り、子要素のタグをすべて外す関数 flatten_text()

 例の root みたいな Element を渡したら、子要素をテキストだけ残してフラットにしてくれる。テキストごと消したければ、drop_tag() の代わりに Element#drop_tree() を使えばいい。

"""
flatten children of HTML Element 
element: HTML Element, not list
"""
def flatten_text(element):
    while list(element):
        children = list(element)
        for child in children:
            child.drop_tag()


 実行例:

>>> import lxml.html
>>> root = lxml.html.fromstring(open('sample.xml', 'rb').read())
>>> root.text
'\r\n    out outer1 '
>>> root.text_content()
'\r\n    out outer1  in outer1  in inner  in outer2  out outer2\r\n'
>>> flatten_text(root)
>>> root.text
'\r\n    out outer1  in outer1  in inner  in outer2  out outer2\r\n' #テキストが取り出されている
>>> root.text_content()
'\r\n    out outer1  in outer1  in inner  in outer2  out outer2\r\n'
>>> root.text = 'new text'
>>> root.text
'new text'
>>> root.text_content()
'new text'        # すべてのテキストを置き換えることができた