HN 日本語サマリー

← 一覧へ戻る
プログラミング

Edison: Pythonで構文木を使ったEDIFACTの解析

Parsing EDIFACT in Python with syntax trees (github.com)

16 pointsby alexprengere4 コメント

要約

Edisonは、EDIFACTメッセージを解析するためのPythonライブラリです。メッセージをセグメントの平坦なリストとしてではなく、セグメントが再帰的に子セグメントのグループを定義できる構文木としてモデル化します。これにより、構文木を一度定義すれば、その構造に対してメッセージを解析、検証、または整形印刷できます。

全文翻訳

Edison

[画像: Edison]

実際の構文木を持つEDIFACTパーサー。

[画像: ビルド] [画像: バージョン] [画像: Pythonバージョン] [画像: ライセンス] [画像: ダウンロード数]

Edisonは、EDIFACTメッセージを解析するためのPythonライブラリです。メッセージをセグメントの平坦なリストとしてではなく、セグメントが再帰的に子セグメントのグループを定義できる構文木としてモデル化します。

構文木を一度定義すれば、その構造に対してメッセージを解析、検証、または整形印刷できます。これは、構文木をセグメント間の可能な遷移の状態機械に変換することによって機能します。

ハイライト

  • Pythonで直接EDIFACT構文木を定義できます。
  • セグメントは、条件付き、必須、または繰り返し可能な子セグメントのグループを定義できます。
  • 関心のあるセグメントに解析コールバックをアタッチできます。
  • メッセージバージョンによって構文木を選択できます。
  • エスケープされたEDIFACT区切り文字とEBCDIC制御文字を処理します。
  • 指定された長さを持つセグメント(例: BLB)を正しくトークン化します。
  • pydotで遷移グラフをエクスポートできます。

インストール

EdisonにはPython 3.12以降が必要です。

pip install edison-parser

クイックスタート

BaseParserをサブクラス化し、1つ以上のスキーマを定義し、処理したいスキーマノードにコールバックメソッド名をアタッチします。

from edison import BaseParser


class PIZZAQParser(BaseParser):
    schemas = {
        (0, 0): [
            ["UNH"],
            ["CVR"],
            ["REF"],  # REFセグメントの一種
            [
                "PIZ",
                [
                    ["TYP", [], "parse_typ"],
                    ["REF", [], "parse_ref"],  # REFセグメントの別の種類
                ],
            ],
            ["UNT"],
        ],
    }

    def __init__(self):
        self.summary = {}
        self.pizza = None

    def parse_typ(self, segment: str):
        # パーサーが "TYP" セグメントを検出したときに実行されます。セグメントは文字列として渡されます。
        self.pizza = segment[4:]

    def parse_ref(self, segment: str):
        # パーサーが "REF" セグメントを検出したときに実行されます。セグメントは文字列として渡されます。
        # "REF" セグメントの "2番目" の種類でのみ実行されます!
        refs = segment[4:].split("*")
        self.summary[self.pizza] = len(refs)


edi = """
UNH+1+PIZZAQ:1:1:1A+00012480825035'\nCVR+USD'\nREF+LUIS'\nPIZ+1'\nTYP+MARGARITA'\nREF+1*2'\nPIZ+2'\nTYP+PESTO'\nREF+3'\nUNT+19+1'\n"""

# パーサーを作成し、このEDIFACTメッセージを解析します。
# "summary" 属性はコールバックによって設定されます。
parser = PIZZAQParser()
parser.parse(edi)
parser.summary  # {"MARGARITA": 2, "PESTO": 1}

# メッセージを検証し、整形印刷します。
PIZZAQParser.validate(edi)  # True/False
PIZZAQParser.show(edi)  # 表示するには

PIZZAQParser.show(edi)の出力は以下の通りです。

[画像: Edison]

ツリーベースの構造。

スキーマフォーマット

スキーマは、サポートされる最小バージョンをキーとする辞書です。

schemas = {
    (0, 0): [
        ["UNH"],
        ["BGM"],
        ["UNT"],
    ],
}

各セグメントノードは、これらの位置フィールドを受け入れます。

["TAG", children, action, repetition, flag]
フィールドデフォルト意味
TAG必須3文字のEDIFACTセグメントタグ。
children[]ネストされたセグメントグループ。
actionNone生のセグメントとともに呼び出されるパーサーメソッド名。
repetition999991は単一使用を意味します。それより大きい値は繰り返し可能を意味します。
flag"C"条件付きの場合は"C"、必須の場合は"M"。

メインAPI

API使用法
parser.parse(edi, version=None)EDIメッセージをウォークし、一致するセグメントアクションを呼び出します。
Parser.validate(edi, version=None)すべてのセグメントがスキーマ遷移に従っている場合にTrueを返します。
Parser.show(edi, version=None, colorize=True)EDIメッセージのフォーマットされ、インデントされた表現を返します。
Parser.build_dot_object(version=None, include_up_links=True)pydot.Dot遷移グラフを構築します。
extract_envelope_headers(edi)UNB/UNHエンベロープセグメントを素早く返します。
extract_envelope_footers(edi)UNT/UNZエンベロープセグメントを素早く返します。
escape(data) / unescape(data)EDIFACT区切り文字(+、:、*、')をエスケープ/アンエスケープします。
normalize_ebcdic_separators(edi)EBCDIC EDIFACT区切り文字を安全にASCIIに変換します。

高度な使用法

バージョン化された構文木

同じメッセージがバージョン間で進化する場合、複数の構文木を定義します。パーサーは、version引数以下で最も高いスキーマバージョンを選択し、解析コールバックは同じままでいられます。

from edison import BaseParser


class RouteParser(BaseParser):
    schemas = {
        (0, 0): [
            ["UNH"],
            ["ORG", [], "parse_origin"],
            ["DST", [], "parse_destination"],
            ["UNT"],
        ],
        (2, 0): [
            ["UNH"],
            [
                "LEG",
                [
                    ["ORG", [], "parse_origin"],
                    ["DST", [], "parse_destination"],
                ],
            ],
            ["UNT"],
        ],
    }

    def __init__(self):
        self.route = {}

    def parse_origin(self, segment: str):
        self.route["origin"] = segment[4:]

    def parse_destination(self, segment: str):
        self.route["destination"] = segment[4:]


legacy = RouteParser()
legacy.parse("UNH'ORG+PAR'DST+NCE'UNT'", version=(1, 0))

modern = RouteParser()
modern.parse("UNH'LEG+1'ORG+PAR'DST+NCE'UNT'", version=(2, 0))

assert legacy.route == modern.route == {
    "origin": "PAR",
    "destination": "NCE",
}
assert RouteParser.validate("UNH'ORG+PAR'DST+NCE'UNT'", version=(1, 0))
assert RouteParser.validate("UNH'LEG+1'ORG+PAR'DST+NCE'UNT'", version=(2, 0))

versionが省略されたり、不完全だったり、既知のすべてのスキーマよりも新しい場合、Edisonは最新のスキーマを使用します。要求されたバージョンが最初のスキーマよりも古い場合、find_transitions()はValueErrorを発生させます。

必須および繰り返し可能なセグメント

repetitionは、遷移グラフが同じノードに戻ることができるかどうかを制御します。

  • 1は、ノードが特定のシーケンスで1回使用できることを意味します。
  • 1より大きい値は、ノードが繰り返し可能であることを意味します。遷移グラフは、2と99999のような正確な出現回数の制限を区別するのではなく、1と「繰り返し可能」のみを区別します。

必須ノードは、ツリー内の有効なパスを制約します。必須ノードがシーケンスで次に来ると、パーサーはそれをスキップして、それをバイパスする可能性のある後続の兄弟、親、または子に到達することはできません。必須の子ノード自体が親を必須にするわけではありません。グループ全体が必要な場合は、グループノードにも"M"をマークしてください。

from edison import BaseParser


class OrderParser(BaseParser):
    schemas = {
        (0, 0): [
            ["UNH"],
            [
                "ITM",
                [
                    ["DSC"],
                    ["QTY", [], "parse_quantity", 1, "M"],
                    ["PRI"],
                ],
                None,
                99999,
                "M",
            ],
            ["UNT"],
        ],
    }

    def __init__(self):
        self.quantities = []

    def parse_quantity(self, segment: str):
        self.quantities.append(segment[4:])


assert OrderParser.validate("UNH'ITM'DSC'QTY+2'PRI'ITM'QTY+1'UNT'")
assert not OrderParser.validate("UNH'UNT'")  # ITMは必須です。
assert not OrderParser.validate("UNH'ITM'PRI'UNT'")  # QTYは必須です。
assert not OrderParser.validate("UNH'ITM'QTY+2'QTY+3'UNT'")  # QTYは1回繰り返されます。

共有ヘッダー、フッター、およびパーサーフック

複数のスキーマがエンベロープセグメントを共有する場合にheaderとfooterを使用します。これらは、クラスコンパイル時にすべてのスキーマに自動的に前置および後置されます。

from edison import BaseParser


class EnvelopeParser(BaseParser):
    header = [["UNH", [], "on_segment"]]
    footer = [["UNT", [], "on_segment"]]
    schemas = {(0, 0): [["BGM", [], "on_segment"]]}

    def __init__(self):
        self.segments = []

    def set_up(self):
        self.segments.clear()

    def on_segment(self, segment: str):
        self.segments.append(segment)

    def tear_down(self):
        self.count = len(self.segments)

set_up()は解析開始前に実行され、tear_down()は解析終了後に実行されます。

遷移グラフ

内部的に、Edisonはパーサークラスが作成されるときに、セグメント間の遷移の有向グラフを構築します。各ノードは特定のセグメントであり、各エッジは次のセグメントへの有効な遷移です。これにより、解析はグラフのウォークとなり、検証はパスが存在するかどうかの確認となります。

build_dot_object()を使用して遷移グラフを視覚化できます。これはpydot.Dotオブジェクトを返します。pydotはEdisonとともにインストールされます。PNGファイルを書き込むには、Graphvizがシステムで利用可能であることを確認してください。

from edison import BaseParser


class Parser(BaseParser):
    schemas = {
        (0, 0): [
            ["UNH"],
            [
                "BGM",
                [
                    ["DTM"],
                ],
            ],
            ["UNT"],
        ],
    }


dot = Parser.build_dot_object()
dot.write_png("transitions.png")

上記のスキーマに対応する結果のグラフは以下の通りです。

[画像: pydot]

解析で使用されるのと同じバージョンセレクターを渡したり、よりシンプルなツリーライクなグラフのためにアップリンクを非表示にしたりできます。

dot = Parser.build_dot_object(version=(13, 2), include_up_links=False)
dot.write_png("transitions-v13.png")

build_dot_object()は基になるpydot.Dotオブジェクトを返すため、書き込む前にカスタマイズできます。

エスケープとEBCDIC EDIFACT区切り文字

EdisonはエスケープされたEDIFACT区切り文字を理解し、バイナリペイロードでよく見られるEBCDIC区切り文字を正規化できます。

from edison import normalize_ebcdic_separators, escape, unescape


assert escape("REC+") == r"REC+"
assert unescape(r"REC+") == "REC+"
assert normalize_ebcdic_separators("UNB\x1dSENDER\x1fREC") == "UNB+SENDER:REC"

セグメントトークナイザーはエスケープされた引用符も処理するため、IFT+1+AIRLINE\'SERVER'のようなコンテンツは単一のEDIFACTセグメント内に保持されます。

EDIFACTエンベロープ抽出

EDIFACTエンベロープ境界を読み取る一般的なケースでは、最初のUNB/UNHセグメントを遅延評価で返すextract_envelope_headers()または最後のUNT/UNZセグメントを遅延評価で返すextract_envelope_footers()を使用します。

from edison import extract_envelope_footers, extract_envelope_headers

edi = "UNB+SENDER+RECEIVER'UNH+1+PIZZAQ:1:1:1A'BGM+1'UNT+3+1'UNZ+1+1'"
headers = extract_envelope_headers(edi)
footers = extract_envelope_footers(edi)

assert headers.unb == "UNB+SENDER+RECEIVER"
assert headers.unh == "UNH+1+PIZZAQ:1:1:1A"
assert footers.unt == "UNT+3+1"
assert footers.unz == "UNZ+1+1"

開発

ローカルコマンド

このリポジトリはuv、tox、pytest、ruff、black、ty、およびpre-commitフックを使用しています。

uv tool install tox --with tox-uv
tox -e py

配布物をビルドするには:

uv build --sdist --wheel -o dist/

完全なpre-commitスイートを実行するには:

uv tool install prek==0.4.10
prek run --all-files

CIは、Linux、Windows、macOS上のCPython 3.12および3.14でtoxを介してテストを実行し、その後ソースおよびホイールの配布物をビルドします。