目次
環境
- Python 3.11.2
- macOS Ventura 13.2.1
XMLtoDictライブラリを使う方法
XMLを読み込んでPythonのDictionary型へ変換してくれる便利なライブラリ(Githubページ)。pipから簡単にインストールできる。
pip install xmltodict
サンプルコード
XMLtoDictのparse()
import xmltodict
xml = '''<?xml version="1.0"?>
<data>
<country name="Liechtenstein">
<rank>1</rank>
<year>2008</year>
<gdppc>141100</gdppc>
<neighbor name="Austria" direction="E"/>
<neighbor name="Switzerland" direction="W"/>
</country>
<country name="Singapore">
<rank>4</rank>
<year>2011</year>
<gdppc>59900</gdppc>
<neighbor name="Malaysia" direction="N"/>
</country>
<country name="Panama">
<rank>68</rank>
<year>2011</year>
<gdppc>13600</gdppc>
<neighbor name="Costa Rica" direction="W"/>
<neighbor name="Colombia" direction="E"/>
</country>
</data>'''
root = xmltodict.parse(xml)
first_country = root['data']['country'][0]
print(first_country['@name']) # Liechtenstein
print(first_country['year']) # 2008
例ではXML文字列をparse()メソッドに渡しているが、ファイルパスでも良い。するともうDictionary型(辞書型)が返ってくるので、XML構造をDictionaryとして扱うことが出来る。ちなみに属性値はインデックスの頭に"@"をつけることで指定できる。
DictionaryをXMLへ
XMLtoDictの良い点はDictionary型とXMLが簡単に相互変換できること。
Dictionary型からXMLへ戻す
print(xmltodict.unparse(root, pretty=True)
これでXMLとして出力(print)される。引数としてpretty=Trueも一緒に渡すことできれいにフォーマットされたXMLを受け取ることが出来る。デフォルトはFalse。
標準ライブラリを使う方法
Python標準ライブラリのAPI“xml.etree.ElementTree”(公式ドキュメント)。簡易的なXPath(HTMLで言うCSSセレクタ)も使えるので簡単なXML解析/スクレイピングにちょうどよい。
ElementTreeとElement型
このAPIにはElementTree型とElement型がある。ElementTreeは“XML構造全体”を持った型で、これには階層構造となったXML要素も含んでいる。このXMLの“要素”情報自体をこのAPIではElement型と呼ぶ。
よく使うパースメソッドたち
- ファイルを読み込むparse()メソッド → ElementTree型を返す
- 文字列を読み込めるstringfrom()メソッド → Element型を返す
ファイルパスを文字列として渡すことで、そのXMLファイルをElementTree型として返してくれるメソッド parse()、XML文字列を渡すとElement型として返してくれるメソッドfromstring()の2種類をご紹介。
サンプルコード
parse()メソッド
# sample - from XML files
import xml.etree.ElementTree as ET
tree = ET.parse('data.xml') # return ElementTree
root = tree.getroot() # return Element
print(root.tag) # "data"
fromstring()メソッド
# sample.py - from String variables
import xml.etree.ElementTree as ET
xml = '''<?xml version="1.0"?>
<data>
<country name="Liechtenstein">
<rank>1</rank>
<year>2008</year>
<gdppc>141100</gdppc>
<neighbor name="Austria" direction="E"/>
<neighbor name="Switzerland" direction="W"/>
</country>
<country name="Singapore">
<rank>4</rank>
<year>2011</year>
<gdppc>59900</gdppc>
<neighbor name="Malaysia" direction="N"/>
</country>
<country name="Panama">
<rank>68</rank>
<year>2011</year>
<gdppc>13600</gdppc>
<neighbor name="Costa Rica" direction="W"/>
<neighbor name="Colombia" direction="E"/>
</country>
</data>'''
root = ET.fromstring(xml) # return Element
print(root.tag) # "data"
Element型のプロパティやメソッドたち
ファイルから読み込む場合、返ってきたElementTree型にgetroot()することでElement型のルート要素(例では<data>)を取り出せる。
.tagプロパティで要素のタグ名を、.textで要素内の文字列を抽出できる。.attribプロパティにはDictionary型で属性値(例では{'name': 'Liechtenstein'}など)が入っている。
iter()メソッドを使うことで要素を順番に探っていくことができる。for inとの組み合わせはよく使う。
iter()メソッドでイテレート
for year in root.iter('year'):
print(year.text) # "2008", "2011", "2011"
find()メソッド、findall()メソッドを使うことで条件に一致する子要素を見つけ出すことができる。find()メソッドは条件に一致した“最初”の要素を返し、findall()は一致したすべての要素をList型(リスト型)として返してくれる。条件として渡す文字列は簡易的なXPathとなる。
XPathによる要素の取得
first_country = root.find('country')
print(first_country.get('name')) # "Liechtenstein"
print(first_country.find('year').text) # "2008"
neighbors = root.findall('.//neighbor')
print(neighbors[0].get('direction')) # "E"
first_west = root.find('.//*[@direction="W"]')
print(first_west.get('name')) # "Switzerland"
get()メソッドで属性の値のみを取得できる。なお、使えるXPathの記法は通常のものと若干異なる点に注意。詳しくは公式ドキュメントまで。
Element型を編集する
Element型を編集することも、もちろんできる。.textプロパティに文字列を代入することで要素文字列を上書き出来たり、.set()メソッドを使えば要素の属性値を編集することも出来る。
要素の編集/変更
first_year = root.find('.//year')
first_year.text = '2009' # "2008" -> "2009"
first_year.set('modified', 'yes') # adding "modified" attribute
print(first_year.text) # "2009"
print(first_year.attrib) # {'modified': 'yes'}
もちろん、ElementTreeやElementをXMLへ戻すことも出来る。文字列として戻す場合はtostring()メソッド、XMLファイルとして書き出す場合はwrite()メソッドが使える。tostring()はElement型のみ、write()メソッドはElementTree型のみ対応している。これはパースするときと同じ。
ElementTree/Element型からXMLへ戻す
xml_str = ET.tostring(root, encoding='utf-8').decode(encoding='utf-8') # return XML as String
ET.ElementTree(root).write('country_data.xml', encoding='utf-8', xml_declaration=True)) # write to file
.tostring()や.write()のデフォルトエンコードは"US-ASCII"なので例のように"UTF-8"や"Unicode"などを指定したい場合は例のようにする。ちなみに、エンコーディングが"Unicode"ならString(文字列型)で返ってくるが、それ以外(ASCIIやUTF-8も含まれる)だとBytes(バイト型)で返ってくるので、例のように.decode()でString型へデコードする必要がある。.write()のxml_declaration引数はXML宣言(XMLファイル最初の行にある<?xml version='1.0'…)を書き出すか否かを選べるが、デフォルトだとFalse(書き出さない)なので注意が必要。
Element要素の有無
せっかくなので最後に、Element型を扱う上での注意点を。それは要素が存在するか否かif文などでチェックするときの条件文の書き方。
存在する要素かチェックする方法
first_year = root.find('.//year')
if not first_year:
print('`first_year` does not exist OR `first_year` has no children!')
if first_year is None:
print('`first_year` does not exist.')
if len(first_year):
print('`first_year` has children.')
公式ドキュメントにも注意書きがあるように、not first_yearでチェックすると、first_yearが存在しないあるいは子要素を持っていない、どちらでもTrueとなってしまう。もし、first_yearが存在しないことだけをチェックしたいならばfirst_year is Noneとしよう。同様に、子要素の有無をチェックする場合はlen()を使うと良い。len(first_year)とするとその要素の持つ子要素の数がInt(整数型)で返ってくる。
最後に、架空の天気予報APIから返ってくるXMLデータをパースする、XMLtoDictとRequestsを組み合わせた簡単なサンプルコードを載せてお別れを。
明日の天気
import requests
import xmltodict
response = requests.get('https://weather.api', params={"location": "Land-of-Ooo"})
data = xmltodict.parse(response.content)
unit = data["data"]["unit"]
print(unit["temperature"]) # "Celsius"
print(unit["speed"]) # "Meter-per-second"
tomorrow = data["data"]["weather"]["tomorrow"]
print(tomorrow["condition"]) # "Sunny"
print(tomorrow["feelslike"]) # "24.5"
print(tomorrow["wind"]["speed"]) # "2.1"
print(tomorrow["wind"]["direction"]) # "North-east"
print(xmltodict.unparse(data, pretty=True))
# <?xml version='1.0' encoding='utf-8'?>
# <data>
# <unit>
# <temperature>Celsius</temperature>
# <speed>Meter-per-second</speed>
# ...
# </unit>
# <weather>
# <yesterday date="2023-03-27">...</yesterday>
# <today date="2023-03-28">...</today>
# <tomorrow date="2023-03-29">
# <condition>Sunny</condition>
# <feelslike>24.5</feelslike>
# <wind>
# <speed>2.1</speed>
# <direction>North-east</direction>
# </wind>
# ...
# </tomorrow>
# </weather>
# </data>