XML ドキュメントを作成する

XML の構文解析に加えて、 xml.etree.ElementTree はアプリケーションで組み立てた Element オブジェクトから一般的な XML ドキュメントを作成する機能も提供します。ドキュメントを解析するときに使用した Element クラスは、そのコンテンツのシリアライズされたフォームの作成方法も知っていて、ファイルまたは他のデータストリームへ書き込みできます。

要素のノードを組み立てる

Element ノードの階層構造を作成するのに便利なヘルパー関数が3つあります。 Element() は標準的なノードを作成します。 SubElement() は親に新たなノードを追加します。 Comment() は XML のコメント構文を使用してシリアライズするノードを作成します。

from xml.etree.ElementTree import Element, SubElement, Comment, tostring

top = Element('top')

comment = Comment('Generated for PyMOTW')
top.append(comment)

child = SubElement(top, 'child')
child.text = 'This child contains text.'

child_with_tail = SubElement(top, 'child_with_tail')
child_with_tail.text = 'This child has regular text.'
child_with_tail.tail = 'And "tail" text.'

child_with_entity_ref = SubElement(top, 'child_with_entity_ref')
child_with_entity_ref.text = 'This & that'

print tostring(top)

この出力はツリーの XML ノードのみを含み、バージョンとエンコーディングをもつ XML の定義ではありません。

$ python ElementTree_create.py
<top><!--Generated for PyMOTW--><child>This child contains text.</ch
ild><child_with_tail>This child has regular text.</child_with_tail>A
nd "tail" text.<child_with_entity_ref>This &amp; that</child_with_en
tity_ref></top>

child_with_entity_ref テキストの & という文字は、自動的に &amp; というエンティティ参照に変換されます。

人が読み易い XML

ElementTree は、余分なスペースを追加することでドキュメントのコンテンツを変更するので、 tostring() が生成する出力を “pretty print” する機能がありません。人が読み易い出力を作成するには、 私がインターネット上で見つけた tipxml.dom.minidom で XML を再解析した後で toprettyxml() メソッドを使用します。

from xml.etree import ElementTree
from xml.dom import minidom

def prettify(elem):
    """Return a pretty-printed XML string for the Element.
    """
    rough_string = ElementTree.tostring(elem, 'utf-8')
    reparsed = minidom.parseString(rough_string)
    return reparsed.toprettyxml(indent="  ")

変更したサンプルは次のようになります。

from xml.etree.ElementTree import Element, SubElement, Comment
from ElementTree_pretty import prettify

top = Element('top')

comment = Comment('Generated for PyMOTW')
top.append(comment)

child = SubElement(top, 'child')
child.text = 'This child contains text.'

child_with_tail = SubElement(top, 'child_with_tail')
child_with_tail.text = 'This child has regular text.'
child_with_tail.tail = 'And "tail" text.'

child_with_entity_ref = SubElement(top, 'child_with_entity_ref')
child_with_entity_ref.text = 'This & that'

print prettify(top)

この出力は以前の出力よりも読み易くなっています。

$ python ElementTree_create_pretty.py
<?xml version="1.0" ?>
<top>
  <!--Generated for PyMOTW-->
  <child>
    This child contains text.
  </child>
  <child_with_tail>
    This child has regular text.
  </child_with_tail>
  And &quot;tail&quot; text.
  <child_with_entity_ref>
    This &amp; that
  </child_with_entity_ref>
</top>

xml.dom.minidom の pretty-printer は、フォーマット時に余分なスペースに加えて XML 定義もこの出力に追加します。

要素のプロパティを設定する

前節のサンプルはタグとテキストのコンテンツをもつノードを作成しましたが、ノードへ任意の属性を設定しませんでした。 XML ドキュメントを解析する にあるサンプルの多くは、podcast を表示する OPML ファイルとそのフィードを処理します。ツリーの outline ノードは、グループ名と podcast のプロパティの属性に使用されます。 ElementTree は、CSV ファイルから同様の XML ファイルを組み立てられます。それはツリーを構築するときに全ての要素の属性を設定します。

import csv
from xml.etree.ElementTree import Element, SubElement, Comment, tostring
import datetime
from ElementTree_pretty import prettify

generated_on = str(datetime.datetime.now())

# set() で属性を1つ設定する
root = Element('opml')
root.set('version', '1.0')

root.append(Comment('Generated by ElementTree_csv_to_xml.py for PyMOTW'))

head = SubElement(root, 'head')
title = SubElement(head, 'title')
title.text = 'My Podcasts'
dc = SubElement(head, 'dateCreated')
dc.text = generated_on
dm = SubElement(head, 'dateModified')
dm.text = generated_on

body = SubElement(root, 'body')

with open('podcasts.csv', 'rt') as f:
    current_group = None
    reader = csv.reader(f)
    for row in reader:
        group_name, podcast_name, xml_url, html_url = row
        if current_group is None or group_name != current_group.text:
            # 新しいグループを開始する
            current_group = SubElement(body, 'outline', {'text':group_name})
        # 一度に全ての属性をセットしてこの podcast をグループに追加する
        podcast = SubElement(current_group, 'outline',
                             {'text':podcast_name,
                              'xmlUrl':xml_url,
                              'htmlUrl':html_url,
                              })

print prettify(root)

属性値は (root ノードのように) set() で1つずつか、(それぞれのグループと podcast ノードのように)ノードファクトリへディクショナリを渡すことでまとめて設定できます。

$ python ElementTree_csv_to_xml.py
<?xml version="1.0" ?>
<opml version="1.0">
  <!--Generated by ElementTree_csv_to_xml.py for PyMOTW-->
  <head>
    <title>
      My Podcasts
    </title>
    <dateCreated>
      2013-02-17 11:34:38.325286
    </dateCreated>
    <dateModified>
      2013-02-17 11:34:38.325286
    </dateModified>
  </head>
  <body>
    <outline text="Science and Tech">
      <outline htmlUrl="http://www.publicradio.org/columns/futureten
se/" text="APM: Future Tense" xmlUrl="http://www.publicradio.org/col
umns/futuretense/podcast.xml"/>
    </outline>
    <outline text="Science and Tech">
      <outline htmlUrl="http://www.uh.edu/engines/engines.htm" text=
"Engines Of Our Ingenuity Podcast" xmlUrl="http://www.npr.org/rss/po
dcast.php?id=510030"/>
    </outline>
    <outline text="Science and Tech">
      <outline htmlUrl="http://www.nyas.org/WhatWeDo/SciencetheCity.
aspx" text="Science &amp; the City" xmlUrl="http://www.nyas.org/Podc
asts/Atom.axd"/>
    </outline>
    <outline text="Books and Fiction">
      <outline htmlUrl="http://www.podiobooks.com/blog" text="Podiob
ooker" xmlUrl="http://feeds.feedburner.com/podiobooks"/>
    </outline>
    <outline text="Books and Fiction">
      <outline htmlUrl="http://web.me.com/normsherman/Site/Podcast/P
odcast.html" text="The Drabblecast" xmlUrl="http://web.me.com/normsh
erman/Site/Podcast/rss.xml"/>
    </outline>
    <outline text="Books and Fiction">
      <outline htmlUrl="http://www.tor.com/" text="tor.com / categor
y / tordotstories" xmlUrl="http://www.tor.com/rss/category/TorDotSto
ries"/>
    </outline>
    <outline text="Computers and Programming">
      <outline htmlUrl="http://twit.tv/mbw" text="MacBreak Weekly" x
mlUrl="http://leo.am/podcasts/mbw"/>
    </outline>
    <outline text="Computers and Programming">
      <outline htmlUrl="http://twit.tv" text="FLOSS Weekly" xmlUrl="
http://leo.am/podcasts/floss"/>
    </outline>
    <outline text="Computers and Programming">
      <outline htmlUrl="http://www.coreint.org/" text="Core Intuitio
n" xmlUrl="http://www.coreint.org/podcast.xml"/>
    </outline>
    <outline text="Python">
      <outline htmlUrl="http://advocacy.python.org/podcasts/" text="
PyCon Podcast" xmlUrl="http://advocacy.python.org/podcasts/pycon.rss
"/>
    </outline>
    <outline text="Python">
      <outline htmlUrl="http://advocacy.python.org/podcasts/" text="
A Little Bit of Python" xmlUrl="http://advocacy.python.org/podcasts/
littlebit.rss"/>
    </outline>
    <outline text="Python">
      <outline htmlUrl="" text="Django Dose Everything Feed" xmlUrl=
"http://djangodose.com/everything/feed/"/>
    </outline>
    <outline text="Miscelaneous">
      <outline htmlUrl="http://www.castsampler.com/users/dhellmann/"
 text="dhellmann's CastSampler Feed" xmlUrl="http://www.castsampler.
com/cast/feed/rss/dhellmann/"/>
    </outline>
  </body>
</opml>

ノードのリストからツリーを構築する

複数の子は extend() メソッドをもつ Element インスタンスへ追加できます。 extend() への引数は、 list または別の Element インスタンスを含む任意のイテレータです。

from xml.etree.ElementTree import Element, tostring
from ElementTree_pretty import prettify

top = Element('top')

children = [
    Element('child', num=str(i))
    for i in xrange(3)
    ]

top.extend(children)

print prettify(top)

list が渡されたとき、そのリストにあるノードは直接的に新たな親へ追加されます。

$ python ElementTree_extend.py
<?xml version="1.0" ?>
<top>
  <child num="0"/>
  <child num="1"/>
  <child num="2"/>
</top>

別の Element インスタンスが渡されたとき、そのノードの子は新たな親へ追加されます。

from xml.etree.ElementTree import Element, SubElement, tostring, XML
from ElementTree_pretty import prettify

top = Element('top')

parent = SubElement(top, 'parent')

children = XML('''<root><child num="0" /><child num="1" /><child num="2" /></root> ''')
parent.extend(children)

print prettify(top)

この場合、3つの子をもつ XML 文字列を解析することで root タグをもつノードが作成されて parent ノードに追加されます。 root ノードは、出力ツリーにはありません。

$ python ElementTree_extend_node.py
<?xml version="1.0" ?>
<top>
  <parent>
    <child num="0"/>
    <child num="1"/>
    <child num="2"/>
  </parent>
</top>

extend() は全ての既存ノードの親子関係を変更しないということを理解することが重要です。その値が既にツリーにある既存の部分を拡張するために渡される場合、そこにも存在して、繰り返して出力されます。

from xml.etree.ElementTree import Element, SubElement, tostring, XML
from ElementTree_pretty import prettify

top = Element('top')

parent_a = SubElement(top, 'parent', id='A')
parent_b = SubElement(top, 'parent', id='B')

# 子を作成する
children = XML('''<root><child num="0" /><child num="1" /><child num="2" /></root> ''')

# 重複を分かり易くするためにノードの Python オブジェクト id に id をセットする
for c in children:
    c.set('id', str(id(c)))

# 最初の親に追加する
parent_a.extend(children)

print 'A:'
print prettify(top)
print

# 2番目の親にノードをコピーする
parent_b.extend(children)

print 'B:'
print prettify(top)
print

Python のユニークなオブジェクト識別子へそういった子の id 属性を設定することは、その出力に同じノードオブジェクトが複数回現れることになります。

$ python ElementTree_extend_node_copy.py
A:
<?xml version="1.0" ?>
<top>
  <parent id="A">
    <child id="4300114320" num="0"/>
    <child id="4300114384" num="1"/>
    <child id="4300114576" num="2"/>
  </parent>
  <parent id="B"/>
</top>


B:
<?xml version="1.0" ?>
<top>
  <parent id="A">
    <child id="4300114320" num="0"/>
    <child id="4300114384" num="1"/>
    <child id="4300114576" num="2"/>
  </parent>
  <parent id="B">
    <child id="4300114320" num="0"/>
    <child id="4300114384" num="1"/>
    <child id="4300114576" num="2"/>
  </parent>
</top>

ストリームへ XML をシリアライズする

tostring() は、ファイルのようなインメモリオブジェクトに書き込み、要素ツリー全体を表す文字列を返すように実装されています。巨大なデータを処理するときは、メモリの消費を少なくして、 ElementTreewrite() メソッドでファイルハンドラへ直接書き込むために I/O ライブラリを効率良く使用します。

import sys
from xml.etree.ElementTree import Element, SubElement, Comment, ElementTree

top = Element('top')

comment = Comment('Generated for PyMOTW')
top.append(comment)

child = SubElement(top, 'child')
child.text = 'This child contains text.'

child_with_tail = SubElement(top, 'child_with_tail')
child_with_tail.text = 'This child has regular text.'
child_with_tail.tail = 'And "tail" text.'

child_with_entity_ref = SubElement(top, 'child_with_entity_ref')
child_with_entity_ref.text = 'This & that'

empty_child = SubElement(top, 'empty_child')

ElementTree(top).write(sys.stdout)

このサンプルは、コンソールへ書き込むために sys.stdout を使用しますが、オープンされたファイルまたはソケットにも書き込めます。

$ python ElementTree_write.py
<top><!--Generated for PyMOTW--><child>This child contains text.</ch
ild><child_with_tail>This child has regular text.</child_with_tail>A
nd "tail" text.<child_with_entity_ref>This &amp; that</child_with_en
tity_ref><empty_child /></top>

ツリーの最後のノードは、テキストまたはサブノードを含まないので、空のタグ <empty_child /> として書き込まれます。 write() は、空のノード処理を制御するために method 引数を受け取ります。

import sys
from xml.etree.ElementTree import Element, SubElement, ElementTree

top = Element('top')

child = SubElement(top, 'child')
child.text = 'This child contains text.'

empty_child = SubElement(top, 'empty_child')

for method in [ 'xml', 'html', 'text' ]:
    print method
    ElementTree(top).write(sys.stdout, method=method)
    print '\n'
    

3つのメソッドがサポートされています。

xml
デフォルトメソッドで <empty_child /> を生成する。
html
HTML で要求されるようなタグのペアを生成する(<empty_child></empty_child>)。
text
ノードのテキストのみを表示して、空のタグを完全に読み飛ばす。
$ python ElementTree_write_method.py
xml
<top><child>This child contains text.</child><empty_child /></top>

html
<top><child>This child contains text.</child><empty_child></empty_child></top>

text
This child contains text.

See also

Outline Processor Markup Language, OPML
Dave Winer の OPML 仕様とドキュメント
Bookmark and Share