はじめに
本記事では、PythonによるXML処理の基本から応用までを、Google Colab上で実際に動作確認できるコードとともに紹介します。C#でXMLスキーマ(XSD)の検証やシリアライズ/デシリアライズを行った経験をもとに、同様の処理がPythonでも可能かどうかを検証してみました。
環境準備
!pip install lxml xmltodict
1. XMLの基本操作
1.1 標準ライブラリでの読み書き
import xml.etree.ElementTree as ET
# XMLデータの作成
xml_data = """<Recipe>
<Name>スパイスカレー</Name>
<CookingTime>60</CookingTime>
<Ingredients>
<Ingredient>
<Name>玉ねぎ</Name>
<Amount>2個</Amount>
</Ingredient>
<Ingredient>
<Name>鶏肉</Name>
<Amount>300g</Amount>
</Ingredient>
</Ingredients>
</Recipe>"""
# XMLの読み込み
try:
root = ET.fromstring(xml_data)
print(f"レシピ名: {root.find('Name').text}")
print(f"調理時間: {root.find('CookingTime').text}分")
# 材料の取得
print("材料:")
for ingredient in root.findall('Ingredients/Ingredient'):
name = ingredient.find('Name').text
amount = ingredient.find('Amount').text
print(f"- {name}: {amount}")
except ET.ParseError as e:
print(f"XML解析エラー: {e}")
# 新しいレシピの作成
new_root = ET.Element("Recipe")
name = ET.SubElement(new_root, "Name")
name.text = "バターチキンカレー"
time = ET.SubElement(new_root, "CookingTime")
time.text = "45"
# インデントを追加して見やすくする
def prettify_xml(element):
"""XMLをきれいに整形する関数"""
from xml.dom import minidom
rough_string = ET.tostring(element, encoding='unicode')
reparsed = minidom.parseString(rough_string)
return reparsed.toprettyxml(indent=" ")
print("\n新しいレシピXML:")
print(prettify_xml(new_root))
1.2 lxmlでのXPath検索
from lxml import etree
# カレーメニューのXML
xml_string = """<CurryMenu>
<Curry spice-level="3">
<Name>インドカレー</Name>
<Price>1200</Price>
</Curry>
<Curry spice-level="2">
<Name>タイカレー</Name>
<Price>980</Price>
</Curry>
<Curry spice-level="1">
<Name>日本風カレー</Name>
<Price>800</Price>
</Curry>
</CurryMenu>"""
try:
# XPath検索
root = etree.fromstring(xml_string)
# 辛さレベル3のカレーを検索
spicy_curries = root.xpath("//Curry[@spice-level='3']")
if spicy_curries:
print(f"辛さレベル3: {spicy_curries[0].find('Name').text}")
# 価格1000円以上のカレーを検索
expensive_curries = root.xpath("//Curry[Price >= 1000]")
print("高価格カレー:")
for curry in expensive_curries:
name = curry.find('Name').text
price = curry.find('Price').text
print(f"- {name}: {price}円")
except etree.XMLSyntaxError as e:
print(f"XML構文エラー: {e}")
2. XMLスキーマ(XSD)検証
2.1 XSDスキーマの定義と検証
from lxml import etree
# XSDスキーマ
xsd_schema = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Recipe">
<xs:complexType>
<xs:sequence>
<xs:element name="Name" type="xs:string"/>
<xs:element name="CookingTime" type="xs:int"/>
<xs:element name="Ingredients">
<xs:complexType>
<xs:sequence>
<xs:element name="Ingredient" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="Name" type="xs:string"/>
<xs:element name="Amount" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>"""
# スキーマの作成
try:
schema_root = etree.fromstring(xsd_schema)
schema = etree.XMLSchema(schema_root)
print("✅ XSDスキーマを正常に作成しました")
except Exception as e:
print(f"❌ スキーマ作成エラー: {e}")
schema = None
# 有効なXML
valid_xml = """<Recipe>
<Name>キーマカレー</Name>
<CookingTime>40</CookingTime>
<Ingredients>
<Ingredient>
<Name>ひき肉</Name>
<Amount>300g</Amount>
</Ingredient>
<Ingredient>
<Name>玉ねぎ</Name>
<Amount>1個</Amount>
</Ingredient>
</Ingredients>
</Recipe>"""
# 無効なXML(CookingTimeが文字列)
invalid_xml = """<Recipe>
<Name>ビーフカレー</Name>
<CookingTime>とても長い時間</CookingTime>
<Ingredients>
<Ingredient>
<Name>牛肉</Name>
<Amount>500g</Amount>
</Ingredient>
</Ingredients>
</Recipe>"""
# 検証関数
def validate_xml(xml_string, schema):
"""XMLをスキーマに対して検証する関数"""
if schema is None:
print("❌ スキーマが利用できません")
return False
try:
xml_doc = etree.fromstring(xml_string)
if schema.validate(xml_doc):
print("✅ XMLは有効です")
return True
else:
print("❌ XMLは無効です")
for error in schema.error_log:
print(f" エラー: {error}")
return False
except Exception as e:
print(f"❌ 検証エラー: {e}")
return False
print("\n=== 有効なXMLのテスト ===")
validate_xml(valid_xml, schema)
print("\n=== 無効なXMLのテスト ===")
validate_xml(invalid_xml, schema)
3. シリアライズ・デシリアライズ
3.1 xmltodictによる辞書変換
import xmltodict
import json
# XMLから辞書への変換
xml_string = """<CurryOrder>
<CustomerName>田中さん</CustomerName>
<Items>
<Item>
<CurryName>チキンカレー</CurryName>
<Quantity>2</Quantity>
<Price>1000</Price>
</Item>
<Item>
<CurryName>野菜カレー</CurryName>
<Quantity>1</Quantity>
<Price>800</Price>
</Item>
</Items>
<TotalAmount>2800</TotalAmount>
</CurryOrder>"""
try:
# デシリアライズ
order_dict = xmltodict.parse(xml_string)
print("=== 辞書型変換結果 ===")
print(json.dumps(order_dict, indent=2, ensure_ascii=False))
# 辞書からデータを取得
customer_name = order_dict['CurryOrder']['CustomerName']
total_amount = order_dict['CurryOrder']['TotalAmount']
print(f"\n顧客名: {customer_name}")
print(f"合計金額: {total_amount}円")
except Exception as e:
print(f"変換エラー: {e}")
# シリアライズ
order_data = {
"CurryOrder": {
"CustomerName": "佐藤さん",
"Items": {
"Item": [
{
"CurryName": "マトンカレー",
"Quantity": "1",
"Price": "1200"
},
{
"CurryName": "シーフードカレー",
"Quantity": "1",
"Price": "1400"
}
]
},
"TotalAmount": "2600"
}
}
try:
xml_output = xmltodict.unparse(order_data, pretty=True)
print("\n=== XML変換結果 ===")
print(xml_output)
except Exception as e:
print(f"XML変換エラー: {e}")
3.2 データクラスによるオブジェクト指向アプローチ
from dataclasses import dataclass
from typing import List, Optional
import xml.etree.ElementTree as ET
@dataclass
class Ingredient:
name: str
amount: str
@dataclass
class CurryRecipe:
name: str
cooking_time: int
ingredients: List[Ingredient]
# XMLからオブジェクトへ
def xml_to_recipe(xml_string: str) -> Optional[CurryRecipe]:
"""XMLをCurryRecipeオブジェクトに変換"""
try:
root = ET.fromstring(xml_string)
# 基本情報の取得
name_elem = root.find('Name')
time_elem = root.find('CookingTime')
if name_elem is None or time_elem is None:
raise ValueError("必須要素(Name または CookingTime)が見つかりません")
name = name_elem.text
cooking_time = int(time_elem.text)
# 材料の取得
ingredients = []
for ing_elem in root.findall('Ingredients/Ingredient'):
name_elem = ing_elem.find('Name')
amount_elem = ing_elem.find('Amount')
if name_elem is not None and amount_elem is not None:
ingredient = Ingredient(
name=name_elem.text,
amount=amount_elem.text
)
ingredients.append(ingredient)
return CurryRecipe(
name=name,
cooking_time=cooking_time,
ingredients=ingredients
)
except (ET.ParseError, ValueError) as e:
print(f"XML解析エラー: {e}")
return None
# オブジェクトからXMLへ
def recipe_to_xml(recipe: CurryRecipe) -> str:
"""CurryRecipeオブジェクトをXMLに変換"""
root = ET.Element('Recipe')
# 基本情報の設定
name_elem = ET.SubElement(root, 'Name')
name_elem.text = recipe.name
time_elem = ET.SubElement(root, 'CookingTime')
time_elem.text = str(recipe.cooking_time)
# 材料の設定
ingredients_elem = ET.SubElement(root, 'Ingredients')
for ingredient in recipe.ingredients:
ing_elem = ET.SubElement(ingredients_elem, 'Ingredient')
name_elem = ET.SubElement(ing_elem, 'Name')
name_elem.text = ingredient.name
amount_elem = ET.SubElement(ing_elem, 'Amount')
amount_elem.text = ingredient.amount
return prettify_xml(root)
# テストデータ
test_xml = """<Recipe>
<Name>欧風カレー</Name>
<CookingTime>90</CookingTime>
<Ingredients>
<Ingredient>
<Name>牛肉</Name>
<Amount>400g</Amount>
</Ingredient>
<Ingredient>
<Name>玉ねぎ</Name>
<Amount>3個</Amount>
</Ingredient>
<Ingredient>
<Name>人参</Name>
<Amount>2本</Amount>
</Ingredient>
</Ingredients>
</Recipe>"""
# デシリアライズのテスト
print("=== デシリアライズテスト ===")
recipe = xml_to_recipe(test_xml)
if recipe:
print(f"レシピ名: {recipe.name}")
print(f"調理時間: {recipe.cooking_time}分")
print("材料:")
for ingredient in recipe.ingredients:
print(f"- {ingredient.name}: {ingredient.amount}")
# シリアライズのテスト
print("\n=== シリアライズテスト ===")
new_recipe = CurryRecipe(
name="グリーンカレー",
cooking_time=30,
ingredients=[
Ingredient("ココナッツミルク", "400ml"),
Ingredient("グリーンカレーペースト", "大さじ2"),
Ingredient("鶏肉", "250g"),
Ingredient("茄子", "2本")
]
)
xml_result = recipe_to_xml(new_recipe)
print(xml_result)
4. 例: カレー注文システム
from dataclasses import dataclass
from typing import List, Dict, Optional
import xml.etree.ElementTree as ET
from datetime import datetime
@dataclass
class MenuItem:
id: str
name: str
price: int
spice_level: int
@dataclass
class OrderItem:
menu_item: MenuItem
quantity: int
subtotal: int
@dataclass
class Order:
order_id: str
customer_name: str
items: List[OrderItem]
total_amount: int
order_time: str
class CurryOrderSystem:
def __init__(self):
self.menu: Dict[str, MenuItem] = {
"001": MenuItem("001", "チキンカレー", 1000, 2),
"002": MenuItem("002", "ビーフカレー", 1200, 3),
"003": MenuItem("003", "野菜カレー", 800, 1),
"004": MenuItem("004", "シーフードカレー", 1400, 2),
"005": MenuItem("005", "マトンカレー", 1300, 4)
}
self.order_counter = 1
def create_order(self, customer_name: str, items: List[tuple]) -> Optional[Order]:
"""注文を作成"""
try:
order_items = []
total = 0
for item_id, quantity in items:
if item_id not in self.menu:
print(f"警告: メニューID {item_id} が見つかりません")
continue
menu_item = self.menu[item_id]
subtotal = menu_item.price * quantity
order_item = OrderItem(
menu_item=menu_item,
quantity=quantity,
subtotal=subtotal
)
order_items.append(order_item)
total += subtotal
if not order_items:
print("エラー: 有効な注文アイテムがありません")
return None
order = Order(
order_id=f"ORD{self.order_counter:04d}",
customer_name=customer_name,
items=order_items,
total_amount=total,
order_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
self.order_counter += 1
return order
except Exception as e:
print(f"注文作成エラー: {e}")
return None
def order_to_xml(self, order: Order) -> str:
"""注文をXMLに変換"""
root = ET.Element('Order')
root.set('id', order.order_id)
root.set('timestamp', order.order_time)
# 顧客情報
customer = ET.SubElement(root, 'Customer')
customer.text = order.customer_name
# 注文アイテム
items_elem = ET.SubElement(root, 'Items')
for item in order.items:
item_elem = ET.SubElement(items_elem, 'Item')
# メニュー情報
menu_elem = ET.SubElement(item_elem, 'Menu')
menu_elem.set('id', item.menu_item.id)
menu_elem.set('spice-level', str(item.menu_item.spice_level))
name_elem = ET.SubElement(menu_elem, 'Name')
name_elem.text = item.menu_item.name
price_elem = ET.SubElement(menu_elem, 'Price')
price_elem.text = str(item.menu_item.price)
# 注文詳細
quantity_elem = ET.SubElement(item_elem, 'Quantity')
quantity_elem.text = str(item.quantity)
subtotal_elem = ET.SubElement(item_elem, 'Subtotal')
subtotal_elem.text = str(item.subtotal)
# 合計金額
total_elem = ET.SubElement(root, 'Total')
total_elem.text = str(order.total_amount)
return prettify_xml(root)
def xml_to_order(self, xml_string: str) -> Optional[Order]:
"""XMLから注文を復元"""
try:
root = ET.fromstring(xml_string)
order_id = root.get('id')
order_time = root.get('timestamp')
customer_name = root.find('Customer').text
items = []
for item_elem in root.findall('Items/Item'):
menu_elem = item_elem.find('Menu')
menu_id = menu_elem.get('id')
spice_level = int(menu_elem.get('spice-level'))
menu_name = menu_elem.find('Name').text
menu_price = int(menu_elem.find('Price').text)
quantity = int(item_elem.find('Quantity').text)
subtotal = int(item_elem.find('Subtotal').text)
menu_item = MenuItem(menu_id, menu_name, menu_price, spice_level)
order_item = OrderItem(menu_item, quantity, subtotal)
items.append(order_item)
total_amount = int(root.find('Total').text)
return Order(order_id, customer_name, items, total_amount, order_time)
except Exception as e:
print(f"XML解析エラー: {e}")
return None
# 使用例
system = CurryOrderSystem()
# 注文の作成
print("=== 注文作成 ===")
order = system.create_order("山田さん", [("001", 2), ("003", 1), ("005", 1)])
if order:
print(f"注文ID: {order.order_id}")
print(f"顧客: {order.customer_name}")
print(f"注文時間: {order.order_time}")
print("注文内容:")
for item in order.items:
print(f"- {item.menu_item.name} x{item.quantity} = {item.subtotal}円")
print(f"合計: {order.total_amount}円")
# XMLへの変換
print("\n=== XML変換 ===")
xml_output = system.order_to_xml(order)
print(xml_output)
# XMLからの復元
print("=== XML復元テスト ===")
restored_order = system.xml_to_order(xml_output)
if restored_order:
print(f"復元成功: {restored_order.customer_name}の注文")
print(f"合計金額: {restored_order.total_amount}円")
5. エラーハンドリングとベストプラクティス
import logging
from contextlib import contextmanager
# ログ設定
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@contextmanager
def xml_processing_context(description: str):
"""XML処理のコンテキストマネージャー"""
logger.info(f"開始: {description}")
try:
yield
logger.info(f"完了: {description}")
except Exception as e:
logger.error(f"エラー in {description}: {e}")
raise
def safe_xml_parse(xml_string: str) -> Optional[ET.Element]:
"""安全なXML解析"""
try:
with xml_processing_context("XML解析"):
parser = ET.XMLParser()
return ET.fromstring(xml_string, parser)
except ET.ParseError as e:
logger.error(f"XML構文エラー: {e}")
return None
except Exception as e:
logger.error(f"予期しないエラー: {e}")
return None
def validate_required_elements(root: ET.Element, required_elements: List[str]) -> bool:
"""必須要素の存在確認"""
for element_name in required_elements:
if root.find(element_name) is None:
logger.error(f"必須要素が見つかりません: {element_name}")
return False
return True
# 使用例
xml_test = """<Recipe>
<Name>テストカレー</Name>
<CookingTime>30</CookingTime>
</Recipe>"""
root = safe_xml_parse(xml_test)
if root is not None:
required = ['Name', 'CookingTime']
if validate_required_elements(root, required):
print("✅ XMLは有効です")
else:
print("❌ 必須要素が不足しています")
まとめ
PythonでのXML処理は、用途に応じて適切なライブラリを使い分けることで効率的に開発できます。
シンプルな読み書きには標準のElementTreeが向いており、XPath検索やスキーマ検証などの高度な処理にはlxmlが適しています。
辞書形式で手軽に扱いたい場合はxmltodictが便利です。
また、型変換処理を行いたい場合には、dataclassと組み合わせた専用ライブラリ(例:pydantic-xml、xsdata)を利用することで、構造化されたデータ処理や型チェック、補完機能による開発支援が可能になります。
参考情報