Webサービスを開発していると、XML(Extensible Markup Language)形式のデータを扱うことがよくあります。
設定ファイル、データ交換、Webサービスの応答など、XMLは様々な場面で使われています。
でも、こんな経験はありませんか?
- 数百MBの巨大なXMLファイルを開こうとしたら、メモリ不足で失敗した
- XMLを全部読み込むのに時間がかかりすぎる
- 必要な情報は一部なのに、全体を読み込まないといけない
そんな問題を解決するのがSAX(Simple API for XML)です。
SAXは、XMLファイルを少しずつ読み進めながら処理する方式で、メモリを節約しながら高速に動作します。この記事では、SAXの仕組みや使い方について、初心者の方にも分かるように詳しく解説していきます。
XML解析の基礎知識
XMLとは?
まず、XMLの基本をおさらいしましょう。
XML(Extensible Markup Language)は、データを構造化して記述するためのマークアップ言語です。
XMLの例:書籍リスト
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book id="001">
<title>プログラミング入門</title>
<author>山田太郎</author>
<price>2500</price>
<year>2024</year>
</book>
<book id="002">
<title>データベース設計</title>
<author>佐藤花子</author>
<price>3200</price>
<year>2023</year>
</book>
</bookstore>
特徴:
- タグで囲まれた階層構造
- 人間にも機械にも読みやすい
- 自由に構造を定義できる
なぜXML解析が必要?
プログラムでXMLを扱うには、解析(パース)が必要です。
解析の目的:
- XMLの構造を理解する
- 必要なデータを取り出す
- データを加工・変換する
- エラーをチェックする
XML解析の2つのアプローチ
XML解析には、大きく分けて2つの方法があります。
1. DOM(Document Object Model)
- XML全体をメモリに読み込む
- ツリー構造を構築
- どこからでもアクセス可能
2. SAX(Simple API for XML)
- 順番に読み進める
- イベント駆動で処理
- メモリ効率が良い
本記事のテーマはSAXです。
SAXとは?基本を理解しよう
SAXの定義
SAX(Simple API for XML)は、XMLファイルをイベント駆動方式で解析するAPIです。
開発の歴史:
- 1998年に登場
- David Megginson氏が中心となって開発
- Java向けに設計されたが、多くの言語に実装された
「Simple」の意味:
名前に「Simple」とありますが、これは「シンプルな実装」という意味で、実際には概念的にはDOMより理解しにくいかもしれません。
イベント駆動方式とは?
SAXの最大の特徴がイベント駆動です。
イメージ:本を読む人
XMLファイルを本に例えると:
DOM方式:
本を全部読んでから、内容を思い出しながら作業する
→ 全ページを記憶する必要がある(メモリ大量消費)
SAX方式:
本を1ページずつ読みながら、気になる部分をメモする
→ 今読んでいるページだけ覚えていればOK(メモリ節約)
イベントの流れ
SAXは、XMLを読み進めながら、様々な「イベント」を発生させます。
例:簡単なXML
<book>
<title>プログラミング入門</title>
</book>
発生するイベント:
1. ドキュメント開始
2. 要素開始: <book>
3. 要素開始: <title>
4. テキスト: "プログラミング入門"
5. 要素終了: </title>
6. 要素終了: </book>
7. ドキュメント終了
プログラムは、これらのイベントに反応して処理を行います。
料理に例えると
分かりやすく料理で例えてみましょう。
DOM方式:
1. レシピ全体を読む
2. すべての材料を準備
3. 全工程を頭に入れてから調理開始
SAX方式:
1. レシピを1行読む → その作業をする
2. 次の行を読む → その作業をする
3. 繰り返し
SAXは「読みながら作業する」スタイルなんです。
DOMとSAXの違い
2つの解析方法を詳しく比較してみましょう。
DOMの仕組み
DOM(Document Object Model)は、XML全体をツリー構造としてメモリに読み込みます。
動作:
XMLファイル
↓ 全体を読み込む
メモリ内のツリー構造
↓ 自由にアクセス
プログラムで処理
イメージ:
bookstore
├── book (id="001")
│ ├── title: "プログラミング入門"
│ ├── author: "山田太郎"
│ ├── price: "2500"
│ └── year: "2024"
└── book (id="002")
├── title: "データベース設計"
├── author: "佐藤花子"
├── price: "3200"
└── year: "2023"
このツリー全体がメモリに載ります。
SAXの仕組み
SAXは、XMLを先頭から順番に読み、イベントを発生させます。
動作:
XMLファイル
↓ 少しずつ読む
イベント発生
↓ イベントごとに処理
プログラムで処理
特徴:
- ファイル全体はメモリに載らない
- 読んだ部分だけ処理して、すぐに忘れる
- 一方通行(戻れない)
比較表
| 項目 | DOM | SAX |
|---|---|---|
| メモリ使用量 | 大きい(全体を保持) | 小さい(少しずつ処理) |
| 処理速度 | 遅い(全体を読み込むまで待つ) | 速い(すぐに処理開始) |
| アクセス方式 | ランダムアクセス可能 | 順次アクセスのみ |
| 使いやすさ | 簡単(ツリー構造が直感的) | やや難しい(イベント処理) |
| XML生成 | 可能 | 不可能(読み込み専用) |
| 大きなファイル | 不向き(メモリ不足の危険) | 向いている |
| 複雑な処理 | 向いている(何度でもアクセス) | 不向き(一度しか読めない) |
それぞれが適している場面
DOMが向いている:
- 小〜中サイズのXML(数MB以下)
- XMLの複数箇所にアクセスする必要がある
- XMLを編集・生成する
- 構造が複雑で、親子関係を辿る必要がある
SAXが向いている:
- 大きなXMLファイル(数十MB〜数GB)
- 必要な情報だけを抽出する
- 順番に処理すればOK
- メモリが限られている環境
- ストリーミング処理
SAXのイベント一覧
SAXで発生する主なイベントを見ていきましょう。
ドキュメントレベルのイベント
startDocument(ドキュメント開始)
- XML解析の開始時に1回だけ発生
- 初期化処理などに使う
endDocument(ドキュメント終了)
- XML解析の終了時に1回だけ発生
- 後処理、結果の出力などに使う
要素レベルのイベント
startElement(要素開始)
<tag>のような開始タグを読んだとき- 要素名と属性情報が渡される
endElement(要素終了)
</tag>のような終了タグを読んだとき- 要素名が渡される
テキストレベルのイベント
characters(文字データ)
- タグとタグの間のテキスト
- 複数回に分けて呼ばれることもある
その他のイベント
ignorableWhitespace(無視できる空白)
- 改行やインデントなどの空白文字
processingInstruction(処理命令)
<?xml ... ?>のような処理命令
skippedEntity(スキップされたエンティティ)
- エンティティ参照がスキップされた
warning、error、fatalError
- 警告やエラーが発生したとき
SAXのメリット
SAXを使う利点を詳しく見ていきましょう。
1. メモリ効率が非常に良い
最大のメリット
SAXは、XMLファイル全体をメモリに読み込みません。
例:1GBのXMLファイル
DOM:
- 1GB以上のメモリを消費(ツリー構造のオーバーヘッド)
- メモリ不足でクラッシュの危険
SAX:
- 数MB程度のメモリで処理可能
- どんなに大きなファイルでも安定
2. 処理が速い
すぐに処理を開始できる
DOMは全体を読み込んでから処理しますが、SAXは読みながら処理します。
例:10万行のXML
DOM:
読み込み完了まで待つ(10秒)
↓
処理開始
SAX:
読み込み開始 → すぐに処理開始
並行して実行
3. ストリーミング処理が可能
ネットワーク越しのXMLにも対応
ファイル全体をダウンロードする前に、処理を開始できます。
ネットワーク → 受信しながら解析 → 処理
リアルタイム性が求められる場合に有効です。
4. 単純な処理に最適
必要な情報だけを抽出
複雑な操作は不要で、特定のデータを取り出すだけなら、SAXが最適です。
例:書籍タイトルだけを抽出
if (elementName.equals("title")) {
// タイトルのテキストを保存
}
5. メモリリークのリスクが低い
構造をメモリに保持しない
DOMではツリー全体がメモリに残りますが、SAXは読んだらすぐに忘れるため、メモリリークのリスクが低いです。
SAXのデメリット
良いことばかりではありません。いくつかの制約があります。
1. 複雑な処理が難しい
一方通行の制約
一度読んだ部分には戻れません。
できないこと:
- 親要素の情報を後から参照
- 前後の要素を比較
- ランダムアクセス
対策:
必要な情報は、変数に保存しておく必要があります。
2. 状態管理が必要
どこを読んでいるか追跡が必要
イベントは順番に来るだけなので、「今どの要素の中にいるか」を自分で管理する必要があります。
// 階層を追跡する例
Stack<String> elementStack = new Stack<>();
public void startElement(...) {
elementStack.push(elementName); // スタックに追加
}
public void endElement(...) {
elementStack.pop(); // スタックから削除
}
3. コードが複雑になりがち
イベント駆動のコードは分かりにくい
DOMのような直感的なツリー操作ではないため、コードが複雑になることがあります。
DOM(簡潔):
NodeList books = doc.getElementsByTagName("book");
for (int i = 0; i < books.getLength(); i++) {
Element book = (Element) books.item(i);
String title = book.getElementsByTagName("title").item(0).getTextContent();
}
SAX(複雑):
boolean inTitle = false;
StringBuilder titleText = new StringBuilder();
public void startElement(...) {
if (localName.equals("title")) {
inTitle = true;
}
}
public void characters(...) {
if (inTitle) {
titleText.append(new String(ch, start, length));
}
}
public void endElement(...) {
if (localName.equals("title")) {
System.out.println("Title: " + titleText.toString());
titleText.setLength(0);
inTitle = false;
}
}
4. XMLの編集・生成ができない
読み取り専用
SAXは解析(読み込み)専用で、XMLを編集したり、新しく生成したりすることはできません。
XMLを生成したい場合:
別のAPIを使う必要があります(StAXやDOMなど)。
5. エラーからの回復が難しい
途中でエラーが起きたら
XML全体を把握していないため、エラーが起きた場合の対処が難しいです。
SAXの実装:プログラミング例
実際にSAXを使ってプログラムを書いてみましょう。
Javaでの実装
JavaのSAXは、org.xml.saxパッケージで提供されています。
基本的な使い方:
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
// SAXハンドラーを作成
class BookHandler extends DefaultHandler {
private boolean inTitle = false;
private boolean inAuthor = false;
private boolean inPrice = false;
private StringBuilder currentText = new StringBuilder();
// ドキュメント開始
@Override
public void startDocument() throws SAXException {
System.out.println("=== XML解析開始 ===");
}
// 要素開始
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
currentText.setLength(0); // テキストをクリア
if (qName.equals("book")) {
String id = attributes.getValue("id");
System.out.println("\n書籍ID: " + id);
} else if (qName.equals("title")) {
inTitle = true;
} else if (qName.equals("author")) {
inAuthor = true;
} else if (qName.equals("price")) {
inPrice = true;
}
}
// テキストデータ
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
currentText.append(new String(ch, start, length));
}
// 要素終了
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (qName.equals("title") && inTitle) {
System.out.println(" タイトル: " + currentText.toString().trim());
inTitle = false;
} else if (qName.equals("author") && inAuthor) {
System.out.println(" 著者: " + currentText.toString().trim());
inAuthor = false;
} else if (qName.equals("price") && inPrice) {
System.out.println(" 価格: " + currentText.toString().trim() + "円");
inPrice = false;
}
}
// ドキュメント終了
@Override
public void endDocument() throws SAXException {
System.out.println("\n=== XML解析終了 ===");
}
// エラー処理
@Override
public void error(SAXParseException e) throws SAXException {
System.err.println("エラー: " + e.getMessage());
}
}
// メインクラス
public class SAXExample {
public static void main(String[] args) {
try {
// SAXパーサーを作成
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
// ハンドラーを作成
BookHandler handler = new BookHandler();
// XMLファイルを解析
parser.parse(new File("books.xml"), handler);
} catch (Exception e) {
e.printStackTrace();
}
}
}
出力例:
=== XML解析開始 ===
書籍ID: 001
タイトル: プログラミング入門
著者: 山田太郎
価格: 2500円
書籍ID: 002
タイトル: データベース設計
著者: 佐藤花子
価格: 3200円
=== XML解析終了 ===
Pythonでの実装
Pythonでは、xml.saxモジュールを使います。
import xml.sax
class BookHandler(xml.sax.ContentHandler):
"""SAXハンドラークラス"""
def __init__(self):
self.current_element = ""
self.current_text = ""
self.in_title = False
self.in_author = False
self.in_price = False
def startDocument(self):
"""ドキュメント開始"""
print("=== XML解析開始 ===")
def startElement(self, name, attrs):
"""要素開始"""
self.current_element = name
self.current_text = ""
if name == "book":
book_id = attrs.get("id", "")
print(f"\n書籍ID: {book_id}")
elif name == "title":
self.in_title = True
elif name == "author":
self.in_author = True
elif name == "price":
self.in_price = True
def characters(self, content):
"""テキストデータ"""
self.current_text += content
def endElement(self, name):
"""要素終了"""
text = self.current_text.strip()
if name == "title" and self.in_title:
print(f" タイトル: {text}")
self.in_title = False
elif name == "author" and self.in_author:
print(f" 著者: {text}")
self.in_author = False
elif name == "price" and self.in_price:
print(f" 価格: {text}円")
self.in_price = False
self.current_text = ""
def endDocument(self):
"""ドキュメント終了"""
print("\n=== XML解析終了 ===")
# メイン処理
if __name__ == "__main__":
# パーサーを作成
parser = xml.sax.make_parser()
# ハンドラーを設定
handler = BookHandler()
parser.setContentHandler(handler)
# XMLファイルを解析
try:
parser.parse("books.xml")
except Exception as e:
print(f"エラー: {e}")
C#での実装
C#では、System.Xml.XmlReaderを使うのが一般的です(厳密にはSAXではないですが、同じイベント駆動方式です)。
using System;
using System.Xml;
class SAXExample
{
static void Main()
{
Console.WriteLine("=== XML解析開始 ===");
try
{
using (XmlReader reader = XmlReader.Create("books.xml"))
{
string currentElement = "";
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
currentElement = reader.Name;
if (currentElement == "book")
{
string id = reader.GetAttribute("id");
Console.WriteLine($"\n書籍ID: {id}");
}
break;
case XmlNodeType.Text:
string text = reader.Value.Trim();
if (currentElement == "title")
{
Console.WriteLine($" タイトル: {text}");
}
else if (currentElement == "author")
{
Console.WriteLine($" 著者: {text}");
}
else if (currentElement == "price")
{
Console.WriteLine($" 価格: {text}円");
}
break;
case XmlNodeType.EndElement:
currentElement = "";
break;
}
}
}
Console.WriteLine("\n=== XML解析終了 ===");
}
catch (Exception e)
{
Console.WriteLine($"エラー: {e.Message}");
}
}
}
実践例:大きなXMLファイルの処理
実際の使用例を見てみましょう。
例1:ログファイルから特定情報を抽出
シナリオ:
1GBの巨大なログXMLから、エラーメッセージだけを抽出する
XMLの構造:
<logs>
<entry level="INFO">
<timestamp>2024-01-01 10:00:00</timestamp>
<message>システム起動</message>
</entry>
<entry level="ERROR">
<timestamp>2024-01-01 10:05:00</timestamp>
<message>データベース接続エラー</message>
</entry>
<!-- ... 数百万件のエントリ ... -->
</logs>
SAXでの処理:
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.*;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
class LogEntry {
String timestamp;
String message;
public LogEntry(String timestamp, String message) {
this.timestamp = timestamp;
this.message = message;
}
@Override
public String toString() {
return timestamp + " - " + message;
}
}
class ErrorLogHandler extends DefaultHandler {
private List<LogEntry> errorLogs = new ArrayList<>();
private boolean isError = false;
private boolean inTimestamp = false;
private boolean inMessage = false;
private StringBuilder currentText = new StringBuilder();
private String currentTimestamp = "";
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
currentText.setLength(0);
if (qName.equals("entry")) {
String level = attributes.getValue("level");
isError = "ERROR".equals(level);
} else if (qName.equals("timestamp")) {
inTimestamp = true;
} else if (qName.equals("message")) {
inMessage = true;
}
}
@Override
public void characters(char[] ch, int start, int length) {
currentText.append(new String(ch, start, length));
}
@Override
public void endElement(String uri, String localName, String qName) {
String text = currentText.toString().trim();
if (qName.equals("timestamp") && inTimestamp) {
currentTimestamp = text;
inTimestamp = false;
} else if (qName.equals("message") && inMessage) {
if (isError) {
errorLogs.add(new LogEntry(currentTimestamp, text));
}
inMessage = false;
} else if (qName.equals("entry")) {
isError = false;
}
}
public List<LogEntry> getErrorLogs() {
return errorLogs;
}
}
public class LogAnalyzer {
public static void main(String[] args) {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
ErrorLogHandler handler = new ErrorLogHandler();
long startTime = System.currentTimeMillis();
// 大きなログファイルを解析
parser.parse(new File("large_log.xml"), handler);
long endTime = System.currentTimeMillis();
// 結果を表示
List<LogEntry> errors = handler.getErrorLogs();
System.out.println("=== エラーログ ===");
for (LogEntry log : errors) {
System.out.println(log);
}
System.out.println("\n総エラー数: " + errors.size());
System.out.println("処理時間: " + (endTime - startTime) + "ms");
} catch (Exception e) {
e.printStackTrace();
}
}
}
出力例:
=== エラーログ ===
2024-01-01 10:05:00 - データベース接続エラー
2024-01-01 11:30:15 - ファイル読み込みエラー
2024-01-01 14:22:33 - メモリ不足
総エラー数: 3
処理時間: 2500ms
DOM方式では数十秒かかる処理が、SAXなら数秒で完了します。
例2:XMLを別形式に変換
シナリオ:
XMLデータをCSVに変換する
Pythonでの実装:
import xml.sax
import csv
class XMLtoCSVHandler(xml.sax.ContentHandler):
"""XMLをCSVに変換するハンドラー"""
def __init__(self, csv_writer):
self.csv_writer = csv_writer
self.current_book = {}
self.current_element = ""
self.current_text = ""
def startElement(self, name, attrs):
self.current_element = name
self.current_text = ""
if name == "book":
self.current_book = {"id": attrs.get("id", "")}
def characters(self, content):
self.current_text += content
def endElement(self, name):
text = self.current_text.strip()
if name in ["title", "author", "price", "year"]:
self.current_book[name] = text
elif name == "book":
# 書籍情報をCSVに書き込む
self.csv_writer.writerow([
self.current_book.get("id", ""),
self.current_book.get("title", ""),
self.current_book.get("author", ""),
self.current_book.get("price", ""),
self.current_book.get("year", "")
])
self.current_book = {}
self.current_text = ""
# メイン処理
if __name__ == "__main__":
# CSVファイルを開く
with open("books.csv", "w", newline="", encoding="utf-8") as csvfile:
csv_writer = csv.writer(csvfile)
# ヘッダーを書き込む
csv_writer.writerow(["ID", "タイトル", "著者", "価格", "年"])
# SAXパーサーを作成
parser = xml.sax.make_parser()
handler = XMLtoCSVHandler(csv_writer)
parser.setContentHandler(handler)
# XMLを解析してCSVに変換
try:
parser.parse("books.xml")
print("CSVファイルの生成が完了しました")
except Exception as e:
print(f"エラー: {e}")
生成されるCSV:
ID,タイトル,著者,価格,年
001,プログラミング入門,山田太郎,2500,2024
002,データベース設計,佐藤花子,3200,2023
トラブルシューティング
SAXを使う際によくある問題と解決策です。
問題1:文字データが分割される
症状:characters()メソッドが複数回呼ばれ、テキストが分割される
原因:
SAXパーサーは、パフォーマンスのために文字データを分割して渡すことがあります。
解決策:
StringBuilderなどで文字を蓄積する
private StringBuilder currentText = new StringBuilder();
public void startElement(...) {
currentText.setLength(0); // クリア
}
public void characters(char[] ch, int start, int length) {
currentText.append(new String(ch, start, length)); // 蓄積
}
public void endElement(...) {
String text = currentText.toString().trim(); // 使用
}
問題2:階層構造の追跡が難しい
症状:
今どの要素の中にいるか分からなくなる
解決策:
スタックを使って階層を追跡
import java.util.Stack;
class MyHandler extends DefaultHandler {
private Stack<String> elementStack = new Stack<>();
public void startElement(...) {
elementStack.push(qName);
// 現在の階層を確認
if (elementStack.size() >= 2 &&
elementStack.get(elementStack.size() - 2).equals("book") &&
qName.equals("title")) {
// bookの直下のtitleの場合
}
}
public void endElement(...) {
elementStack.pop();
}
}
問題3:エンコーディングの問題
症状:
日本語が文字化けする
解決策:
XML宣言でエンコーディングを指定
<?xml version="1.0" encoding="UTF-8"?>
Javaコードでも指定:
InputSource source = new InputSource(new FileInputStream("books.xml"));
source.setEncoding("UTF-8");
parser.parse(source, handler);
問題4:メモリ使用量が増える
症状:
SAXを使っているのにメモリが増え続ける
原因:
ハンドラー内で大量のデータを保持している
解決策:
必要なデータだけを保持し、不要になったらすぐに破棄
public void endElement(...) {
// データを処理
processData(currentData);
// すぐに破棄
currentData = null;
}
問題5:エラー処理が不十分
症状:
XMLの形式エラーでプログラムが停止する
解決策:
エラーハンドリングを実装
@Override
public void error(SAXParseException e) throws SAXException {
System.err.println("エラー(行" + e.getLineNumber() + "): " + e.getMessage());
}
@Override
public void fatalError(SAXParseException e) throws SAXException {
System.err.println("致命的エラー(行" + e.getLineNumber() + "): " + e.getMessage());
throw e;
}
@Override
public void warning(SAXParseException e) throws SAXException {
System.err.println("警告(行" + e.getLineNumber() + "): " + e.getMessage());
}
まとめ:SAXは大規模XML処理の強い味方
SAX(Simple API for XML)は、XML解析のための効率的なイベント駆動型APIです。
この記事のポイント:
- SAXの特徴:イベント駆動方式でXMLを順次処理
- メリット:メモリ効率が良い、高速、大きなファイルに対応
- デメリット:一方通行、状態管理が必要、コードが複雑
- DOMとの違い:DOMは全体をメモリに読み込む、SAXは少しずつ処理
- 主なイベント:startDocument、startElement、characters、endElement、endDocument
- 適している場面:大きなXMLファイル、メモリが限られた環境、順次処理で十分な場合
- 実装:Java、Python、C#など多くの言語で利用可能
- 実践例:ログ解析、データ変換、情報抽出
- 注意点:文字データの分割、階層追跡、エンコーディング
数MB程度の小さなXMLなら、DOMの方が簡単で使いやすいです。
しかし、数十MB〜数GBの大きなファイルや、メモリが限られた環境では、SAXが威力を発揮します。
最初はイベント駆動の概念に戸惑うかもしれませんが、慣れれば非常に強力なツールになります。特に、ビッグデータやログ解析の分野では、SAXの知識が役立つでしょう。
XMLを扱う機会があったら、ファイルのサイズや処理内容に応じて、DOMとSAXを使い分けてみてください。適切なツールを選ぶことで、効率的なプログラムが作れるようになります。

コメント