Pythonでデータ処理を行う場面は、外部システムやAPIからのデータ取得など、さまざまな文脈で頻繁に発生します。データ形式としてよく使われるのがXMLであり、標準モジュールを活用するとファイルからのデータ読み込みを簡単に実現できます。
この記事では、PythonでXMLデータを読み込み、構造を解析するための基本的な手順を解説していきます。巨大なファイルの扱い方や文字化け対策など、サンプルコード付きで解説していきますので、ぜひ参考にしてください。
PythonでXMLデータを読み込む方法
Pythonの標準モジュールであるxml.etree.ElementTreeを活用すると、木構造のデータを一般的な用途では十分な性能で処理できます。外部ライブラリをインストールする手間がなく、すぐに使い始められるのが特徴です。
公式ドキュメントでは、このモジュールの役割を次のように説明しています。
xml.etree.ElementTree モジュールは、XML データを解析および作成するシンプルかつ効率的な API を実装しています。
出典:Python公式ドキュメント xml.etree.ElementTree モジュール
外部APIから取得した情報や複雑な設定ファイルをプログラム側で扱う際にも、この標準モジュールが活躍します。なお、信頼できないソースや外部から取得したXMLをパースする場合は、セキュリティ上のリスク(XXE攻撃やBillion Laughs攻撃など)に注意が必要です。
詳細はPython公式ドキュメント XML の脆弱性を参照し、信頼性の低いデータを扱う場合はdefusedxmlパッケージの使用を検討してください。以下では、基本的な読み込み方法を順に解説します。
ファイルから読み込む
既存のXMLファイルからデータを取得するには、parse()関数を使用します。ファイルパスを指定すると、通常はドキュメント全体をツリー構造としてメモリに展開します(XMLParser設定や後述のiterparse()との組み合わせによって、挙動は変化します)。
ファイルサイズが大きい場合はメモリ消費も比例して増加するため、巨大ファイルの処理には後述のiterparse()を使用してください。
以下のコードは、カレントディレクトリにあるファイルからデータを読み込む例です。
import xml.etree.ElementTree as ET
tree = ET.parse('sample.xml')
root = tree.getroot()
print(root.tag)
上記のコードでは、parse()でツリー全体を取得した後に、getroot()でルート要素を取り出しています。このルート要素を起点として、階層を辿りながら必要な情報を探します。
指定したファイルのパスが間違っているとエラーが発生するため、実行前にファイルの配置場所を確認しておくと安心です。
文字列から読み込む
Webスクレイピングなどで取得したXML形式の文字列を処理する場合は、fromstring()関数を利用します。ファイルを介さずにメモリ上の変数を直接パースできるため、一時データの扱いに便利です。
文字列からルート要素を取得する使い方は、以下の通りです。
import xml.etree.ElementTree as ET
xml_data = '<data><item>Sample</item></data>'
root = ET.fromstring(xml_data)
print(root.tag)
上記のコードでは、fromstring()に文字列を渡すだけで、即座にルート要素であるElementオブジェクトが生成されます。parse()のようにツリー全体を格納したオブジェクトを返すわけではない点に注意が必要です。
XML宣言より前に不正な文字が含まれている場合はパースエラーとなります。XML仕様では先頭の空白や改行自体は許容されますが、バイトオーダーマーク(BOM)が含まれている場合、環境やエンコーディング指定によっては、問題になる場合があるため注意してください。
数GBの巨大なファイルを読み込む
ファイルサイズが数GBに及ぶ場合、全体を一度に読み込むとメモリ不足に陥るおそれがあります。このようなケースでは、データを少しずつ解析するiterparse()関数の出番です。
巨大なファイルをストリーム処理するコードは、以下の通りです。
import xml.etree.ElementTree as ET
# events=('start', 'end') でイテレータを回し、ルート参照を取得してからclear()する
context = ET.iterparse('huge_data.xml', events=('start', 'end'))
context = iter(context)
event, root = next(context) # ルート要素の参照を取得
for event, elem in context:
if event == 'end' and elem.tag == 'target_element':
print(elem.text)
elem.clear()
# ルートが処理済み要素への参照を蓄積しないよう、ルート配下も解放する
root.clear()
上記のコードでは、events=('start', 'end')でイテレータを回してルート要素の参照を最初に取得し、処理済みの要素に対してroot.clear()を実行することでメモリの蓄積を防いでいます。
iterparse()はルート要素への参照をイテレータ内部で保持し続けるため、処理済みの子要素をelem.clear()しても、ルートへの参照経由でメモリが解放されない場合があります。
この構造的な問題を回避するには、上記のようにroot.clear()を併用してルートへの参照を断ち切ることが推奨されます。さらに大規模な処理ではlxmlライブラリへの移行も選択肢の一つです。
文字化けエラーに対処する
Windows環境などでShift-JISやCP932エンコーディングのファイルを扱うと、文字化けによるパースエラーが起こりがちです。XML宣言内のエンコーディング指定と、実際に読み込む際の文字コードが一致していないことが主な原因です。
エンコーディングを明示的に指定してファイルを読み込む方法は、以下の通りです。
import xml.etree.ElementTree as ET
with open('data_sjis.xml', 'r', encoding='cp932') as f:
xml_string = f.read()
root = ET.fromstring(xml_string)
print(root.tag)
上記のコードでは、組み込みのopen()関数で正しい文字コードを指定してから、文字列としてデータを読み込んでいます。この手順を踏むことによって、エンコーディングの不一致を回避できます。
なお、XMLファイルに<?xml version="1.0" encoding="cp932"?>のようなXML宣言が含まれている場合、fromstring()はPythonのUnicode文字列を受け取るため、XML宣言のエンコーディング属性と矛盾が生じてパースエラーとなる場合があります。このケースでは、XML宣言を除去してから渡すか、xml_string.encode('cp932')でバイト列に戻してからfromstring()に渡す方法が確実です。
データ提供元の仕様を確認し、適切な文字コードを選択することがエラー対策として確実な方法です。XML宣言と実際のエンコーディングが一致していることも合わせて確認してください。
Pythonで読み込んだXMLを解析する方法
PythonでXMLの読み込みが完了したら、次は目的のデータを抽出するために要素を解析するステップに進みます。標準モジュールのxml.etree.ElementTreeや用途によっては外部ライブラリを活用するのが適した選択肢となります。
本章で解説する主な解析アプローチは、以下の通りです。
- find()で最初の要素を取得する
- findall()で全ての要素を取得する
- XPathを使って検索する
- 名前空間のある要素に対応する
- pandasでデータフレームに変換する
それぞれのメソッドやライブラリは、用途に合わせて使い分けるのがポイントです。階層構造が複雑なデータでも、適切なアプローチを選ぶことによって、効率的に情報を抽出できます。
find()で最初の要素を取得する
find()メソッドは、指定したタグ名に一致する最初の要素を検索して返すメソッドです。タグ名のみを渡した場合は直下の子要素のみが検索対象となる仕様です。
孫要素以降も対象にする場合は、XPath記法(例:.//item)を用いることによって、深い階層にある要素にもアクセスできます。
find()メソッドの使い方は、以下の通りです。
import xml.etree.ElementTree as ET
tree = ET.parse("sample.xml")
root = tree.getroot()
# 最初に見つかったitem要素を取得(直下の子要素のみが対象)
first_item = root.find("item")
if first_item is not None:
print(first_item.text)
# 孫要素以降も検索したい場合はXPath記法を使用する
first_item_deep = root.find(".//item")
if first_item_deep is not None:
print(first_item_deep.text)
上記のコードでは、タグ名のみを渡したroot.find("item")は直下の子要素のみを検索しています。.//itemのXPath記法を用いた場合は、深さを問わず最初に見つかった要素を返します。
要素が見つかった場合はそのElementオブジェクトを返し、見つからない場合はNoneを返す仕様です。Noneチェックを挟むことによって、要素が存在しないケースでもエラーを回避できます。
findall()で全ての要素を取得する
複数の同名要素をすべて抽出したい場合は、findall()メソッドを使用します。find()と同様に、タグ名のみを渡した場合は直下の子要素のみが検索対象となり、戻り値はリスト型です。
以下のコードは、特定のタグを持つ要素をリストとして一括取得する例です。
import xml.etree.ElementTree as ET
tree = ET.parse("sample.xml")
root = tree.getroot()
# すべてのitem要素を取得(直下の子要素のみが対象)
all_items = root.findall("item")
for item in all_items:
print(item.text)
上記のコードでは、findall()で取得したリストをfor文でループ処理し、各要素のtextプロパティからテキストデータを表示しています。
孫要素以降も検索したい場合は、XPath記法(.//タグ名)を利用することによって、深いネスト構造にある同名要素もまとめて取得できます。
XPathを使って検索する
階層が深い要素や特定の属性を持つ要素を検索する際は、XPath形式の指定が役立ちます。ただし、xml.etree.ElementTreeモジュールが対応するのはXPathのサブセット(簡易XPath)のみであり、完全なXPath仕様のすべての機能は利用できません。
完全なW3C XPath仕様を利用したい場合は、lxmlライブラリの使用を検討してください。lxmlはC言語ベースのlibxml2を内部で使用しており、高速かつ完全なXPath評価に対応しています。
XPathを活用した要素の検索方法は、以下の通りです。
import xml.etree.ElementTree as ET
tree = ET.parse("sample.xml")
root = tree.getroot()
# XPathを使って孫要素を検索
target_elements = root.findall(".//category/item")
for elem in target_elements:
print(elem.attrib)
上記のコードでは、.//を用いて階層の深さを問わず目的の要素を検索しています。条件を絞り込むことによって、より複雑なデータ構造からもピンポイントで必要な情報を抽出できる仕組みです。
名前空間のある要素に対応する
XMLに名前空間(xmlns)が設定されていると、単純なタグ名だけでは、要素を検索できない場合があります。名前空間とは、要素名の衝突を防ぐためにURI(Uniform Resource Identifier)で識別する仕組みです。
名前空間を含むXMLをパースする手順は、以下の通りです。
import xml.etree.ElementTree as ET
tree = ET.parse("sample.xml")
root = tree.getroot()
# 名前空間の辞書を定義
namespaces = {"ns": "http://example.com/ns"}
# 名前空間を指定して検索
items = root.findall("ns:item", namespaces)
for item in items:
print(item.text)
上記のコードでは、プレフィックスとURIのマッピング辞書を作成し、findall()メソッドの第2引数に渡しています。これにより、名前空間付きの要素もプレフィックス指定で正確に取得できるという仕組みです。
pandasでデータフレームに変換する
データ分析用途では、pandasライブラリを用いてXMLをデータフレームに変換すると便利です。ただし、pd.read_xml()はpandas 1.3.0以降で追加された関数であり、それ以前のバージョンでは使用できません。
デフォルトのパーサーがlxmlであるため、pip install lxmlでのインストールが必要です。lxmlが未インストールの場合は、parser='etree'を明示することによって、追加インストール不要の標準モジュールで動作させることもできます。
pandasのread_xml()関数を使った変換方法は、以下の通りです。
import pandas as pd
# XMLファイルをデータフレームに読み込む(lxml未インストール時はparser='etree'を指定)
df = pd.read_xml("sample.xml")
# df = pd.read_xml("sample.xml", parser='etree') # lxmlなしで動かす場合
# 最初の5行を表示
print(df.head())
上記のコードでは、XMLの構造を解析して表形式に変換し、行と列のデータとして扱っています。なお、read_xml()はフラット(浅い)なXML構造に最適化されており、深くネストされたXMLでは意図した変換にならない場合があります。
複雑な階層構造を持つXMLを変換する際は、xpath引数で取得対象を絞り込むか、ElementTreeで手動解析した結果をデータフレームに変換するなどの追加処理を検討してください。
前提条件を満たした環境であれば、データ分析や機械学習の前処理にそのまま活用できる強力な手法です。詳細はpandas公式ドキュメント read_xml()を参照してください。
PythonのXML読み込みに関するよくある質問
PythonでXMLを扱うのにおすすめのライブラリはありますか?
用途によって適したライブラリは異なります。標準のxml.etree.ElementTreeは追加インストール不要で手軽に利用でき、基本操作には十分です。
より高速な処理や完全なXPathサポートが必要な場合はlxmlが、スクレイピングに特化した柔軟な解析を行いたい場合はBeautifulSoupが選ばれる傾向にあります。
読み込みだけではなくXMLの新規作成もできますか?
はい、ElementTreeなどのモジュールを使用すれば、新しいXMLデータを作成可能です。要素を一つずつ構築し、木構造を組み立てていく手順を踏みます。
ルート要素を作成した後に子要素を追加していくことによって、柔軟に任意の構造を持ったデータを生成する仕組みです。
編集したXMLデータをファイルへ保存できますか?
解析や編集を行った後のデータは、ファイルとして出力して保存できます。標準モジュールでは木構造オブジェクトの書き出し用メソッドが用意されているのが特徴です。
出力時にエンコーディングやXML宣言の有無などを指定して保存を完了させます。これによって、別のシステムへ連携する際にも適切な形式でデータを受け渡せます。
※上記コンテンツの内容やソースコードはAIで確認・デバッグしておりますが、間違いやエラー、脆弱性などがある場合は、コメントよりご報告いただけますと幸いです。
ITやプログラミングに関するコラム
PythonをWebで実行する方法
共通テスト「情報Ⅰ」2年目で変わる、日本の教育と学び方
gitでブランチ(branch)を切り替える方法
git cloneでブランチを指定する方法
64GBのメモリが必要な人・不要な人の特徴
PCを再起動するコマンド一覧
CapsLock以外で大文字になる原因【Windows編】
パソコンで大文字になるのを解除する方法
面白いAIの活用事例を業界別に紹介
Gitでcommit(コミット)を取り消す方法
ITやプログラミングに関するニュース
株式会社HODL1がAI経営管理エージェントを提供開始、少人数での経営管理効率化を支援
株式会社サードスコープがKanataを発表、法人向けAI業務支援SaaSを展開
株式会社ナレッジセンスがChatSenseのPPTX出力機能を強化、資料修正負荷の軽減を支援
株式会社すららネットが「すらら」同時導入を公表、夜間中学で日本語学習と教科学習を支援
DUAL株式会社が空室通電DXと賃貸革命の連携を開始、空室時の通電管理効率化を推進
株式会社ITLINEがAWS生成AIシステム開発・運用を提供開始、企業管理型AI基盤の構築を支援
合同会社ウノマスがAI対応スコアを正式公開、LLMOとSEOの横断診断を支援
Markefan株式会社がmarke.aiを提供開始、BtoBマーケティングの属人化抑制を支援
株式会社FinatextがAIロールプレイング提供開始、金融機関の営業育成基盤構築を支援
株式会社アンドパッドがANDPAD ナレッジAIを提供開始、建設現場の情報検索と共有を支援
