2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonでXMLスキーマ処理とシリアライズ・デシリアライズをやってみた

Posted at

はじめに

image.png

本記事では、PythonによるXML処理の基本から応用までを、Google Colab上で実際に動作確認できるコードとともに紹介します。C#でXMLスキーマ(XSD)の検証やシリアライズ/デシリアライズを行った経験をもとに、同様の処理がPythonでも可能かどうかを検証してみました。

環境準備

!pip install lxml xmltodict

1. XMLの基本操作

xml_basic_operations.png

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))

image.png

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}")

image.png

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)

image.png

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}")

image.png

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)

image.png

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}")

image.png

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("❌ 必須要素が不足しています")

{2042141F-E33F-48A4-9F44-FD1CCB2AF929}.png

まとめ

image.png

PythonでのXML処理は、用途に応じて適切なライブラリを使い分けることで効率的に開発できます。
シンプルな読み書きには標準のElementTreeが向いており、XPath検索やスキーマ検証などの高度な処理にはlxmlが適しています。
辞書形式で手軽に扱いたい場合はxmltodictが便利です。

また、型変換処理を行いたい場合には、dataclassと組み合わせた専用ライブラリ(例:pydantic-xml、xsdata)を利用することで、構造化されたデータ処理や型チェック、補完機能による開発支援が可能になります。

参考情報

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?