Claude Media
Claude CodeでONIXの書誌情報を点検する — 必須項目の欠落と表記ゆれ

Claude CodeでONIXの書誌情報を点検する — 必須項目の欠落と表記ゆれ

ONIXの書誌データから、必須項目の欠落・ISBNや日付や価格の表記ゆれ・コードリスト外の値を洗うスクリプトをClaude Codeに書かせる手順です。実行結果つき。

出版社や取次、書店のあいだで書誌データをやり取りするとき、世界的に使われている形式がONIX for Books(以下ONIX)です。XMLで書かれ、機械同士が読むことを前提にしているため、人が目で追って点検するには向きません。数百件のProductを前に、ISBNの桁、価格の書き方、判型の単位が揃っているかを確かめる作業は、スクリプトに任せるのが現実的です。

Claude Codeには、そのスクリプトを書かせて実行させ、指摘を一覧にまとめさせられます。判定そのものはスクリプトが決まった規則で行うので、同じファイルなら何度流しても同じ結果です。この記事では、点検の規則をどう決めるか、スクリプトの完成形、実行結果の読み方、依頼文の書き方を順に示します。例のXMLは手順の確認用に作った合成データで、実在の書籍ではありません。

ONIXの点検は「形」と「中身」の2層に分ける

ONIXの点検には、性質の違う2つの層があります。分けて考えると、Claudeに頼む範囲がはっきりします。

くらべる

ONIXの点検は2層に分かれる

スキーマ検証

形の点検

要素の順序、必須の入れ子、空要素の有無など、XML構造そのものを見ます。XSDまたはRNGのスキーマで機械的に判定でき、独自のスクリプトは要りません。

独自スクリプト

中身の点検

ISBNのチェックディジット、日付の書式、価格の整数性、単位の統一、題名への版表示の混入など、スキーマを通っても誤りになりうる内容を見ます。Claudeに書かせる対象はこちらです。

EDItEUR(ONIXを管理する団体)のFAQには、この分け方の根拠になる記述があります。XMLとして正しく、検証も通るのに、データの中身が誤っていることは簡単に起こる、という指摘です。例として、書籍業界のシステムでは題名の欄に版や製本の別を足す慣行があるが、ONIXでは題名の要素に題名の構成部分だけを入れ、版数や製本は別の要素に置く、と説明されています。つまり中身の点検は、自社の社内ルールをONIXの流儀に合わせられているかを見る作業でもあります。

形の点検には注意点があります。FAQによれば、データのない要素は要素ごと省略するのが正しく、<Tag></Tag>や<Tag/>のような空要素を送ってはいけません。ところが、DTDで検証すると、必須要素が空でもそのまま通ってしまう危険があります。検証にはXSDかRNGを使うよう、FAQは勧めています。DTDしか通していない入稿フローがあれば、そこが最初の見直し箇所です。

必須項目は受け手ごとに違うので、先に文章で決める

「必須項目の欠落を洗う」と頼む前に、何を必須とみなすかを決めておく必要があります。ここが曖昧だと、Claudeは自分の推測で必須項目を選び、実行のたびに結果が変わります。

ONIXの仕様上の必須要素は、ごくわずかです。FAQは、実際に使われる要素はもっと限られ、全要素を使う者はいないと述べたうえで、スキーマが暗黙に許す最小構成は実用にならないと書いています。実際の最小構成は、自社の事業要件と取引先の要件で決まります。一般書の出版社と学術書の出版社では、必要な項目が少しずつ違うという説明です。

つまり「必須項目リスト」は、取引先(取次、書店、データ配信先)と合意したものを使います。仕様書から機械的に拾うものではありません。受け手が求める要素を一覧にして、CLAUDE.mdに書いておきます。CLAUDE.mdは、プロジェクトの直下に置くmarkdownファイルで、Claude Codeがセッションの開始時に毎回読みます。

# ONIX点検のルール
 
- 入力: ONIX for Books 3.x の XML(Reference name 形式)
- 点検スクリプトは `onix_check.py`。判定ルールはここに書いたものだけ
- 必須項目(取引先と合意済み。Product からの相対パス):
  RecordReference / NotificationType / ProductIdentifier/IDValue /
  DescriptiveDetail/ProductForm / .../TitleText / .../PersonName /
  .../Measure/Measurement / .../PublisherName / .../PublishingDate/Date /
  .../Price/PriceAmount
- ISBN-13: ハイフンなしの13桁、チェックディジットを検算する
- 日付: YYYYMMDD の8桁
- 寸法: 単位は mm に統一する
- 価格: 円は整数。価格種別(税込・税抜)は1ファイル内で統一する
- コード値は EDItEUR のコードリスト(CSV)に存在するものだけ
- 判定ルールを変えるときは、変更理由を報告してから直す

最後の一行は、Claudeが「通らないから」という理由で基準のほうを緩めるのを防ぎます。ルールの変更は人が決め、Claudeには報告までさせる運用にします。

判定の根拠をEDItEURの資料に置く

規則ごとに、ONIXの公式資料に根拠があります。下の表は、点検項目と根拠、スクリプトでの扱いをまとめたものです。

点検項目ONIX側の定めスクリプトの扱い
ISBN-13の表記ONIX側の定めコードリスト5の15は、13桁でハイフンや空白なしスクリプトの扱いハイフン入りは不正として指摘
製本の種別ONIX側の定めコードリスト150のBBはハードカバー、BCはソフトカバースクリプトの扱い一覧にないコードは指摘
寸法の単位ONIX側の定めコードリスト50で、長さの推奨単位はミリメートルスクリプトの扱いmm以外は指摘
価格の種別ONIX側の定めコードリスト58の01は税抜、02は税込の推奨小売価格スクリプトの扱い1ファイル内の混在を指摘
円の価格ONIX側の定めコードリスト96のJPYは、通常は整数で表記スクリプトの扱い整数以外は指摘
役割コードONIX側の定めコードリスト17のA01は著者スクリプトの扱い日本語の「著」などは指摘

コードリストは、公開された値が後から削除されることはなく、非推奨になるだけだと説明されています。四半期ごとに新しい号が出ますが、既存の値の意味は保たれます。そのため、点検用に落としたCSVが少し古くても、既存コードの判定が突然変わることはありません。新しい号で足されたコードが「一覧にない」と出た場合は、CSVを取り直します。

コードリストはHTMLのほか、CSV、JSON、XMLなどで配布されています。スクリプトが読むには、CSVが扱いやすい形式です。

スクリプトの完成形

次が、上のルールを実装したスクリプトです。Pythonの標準ライブラリだけで動きます。Claudeに頼むときは、この形を目標にして、CLAUDE.mdのルールを渡して書かせます。

onix_check.py の全文
#!/usr/bin/env python3
"""ONIX for Books 3.x の Product を1件ずつ点検し、指摘を CSV 風に出力する。"""
import csv, re, sys
import xml.etree.ElementTree as ET
 
# 受け手と合意した必須項目(Product からの相対パス)。取引先の要件に合わせて書き換える
REQUIRED = [
    "RecordReference", "NotificationType",
    "ProductIdentifier/IDValue",
    "DescriptiveDetail/ProductForm",
    "DescriptiveDetail/TitleDetail/TitleElement/TitleText",
    "DescriptiveDetail/Contributor/PersonName",
    "DescriptiveDetail/Measure/Measurement",
    "PublishingDetail/Publisher/PublisherName",
    "PublishingDetail/PublishingDate/Date",
    "ProductSupply/SupplyDetail/Price/PriceAmount",
]
# 要素名 -> コードリスト番号
CODED = {"NotificationType": "1", "ProductIDType": "5", "ProductForm": "150",
         "ContributorRole": "17", "MeasureType": "48", "MeasureUnitCode": "50",
         "PriceType": "58", "CurrencyCode": "96"}
 
def local(tag):
    return tag.rsplit("}", 1)[-1]
 
def strip_ns(root):
    for e in root.iter():
        e.tag = local(e.tag)
 
def load_codes(path):
    codes = {}
    with open(path, encoding="utf-8", errors="replace", newline="") as f:
        for row in csv.reader(f):
            if len(row) >= 2:
                codes.setdefault(row[0], set()).add(row[1])
    return codes
 
def isbn13_ok(v):
    if not re.fullmatch(r"\d{13}", v):
        return False
    s = sum(int(c) * (1 if i % 2 == 0 else 3) for i, c in enumerate(v[:12]))
    return (10 - s % 10) % 10 == int(v[12])
 
def check(product, codes):
    out = []
    def add(path, msg):
        out.append((product.findtext("RecordReference", "?"), path, msg))
    for p in REQUIRED:
        node = product.find(p)
        if node is None:
            add(p, "必須項目がない")
        elif not (node.text or "").strip():
            add(p, "空要素(要素ごと省略するのが正)")
    for e in product.iter():
        if e.tag in CODED and (e.text or "").strip():
            if e.text.strip() not in codes.get(CODED[e.tag], set()):
                add(e.tag, f"コードリスト{CODED[e.tag]}にない値: {e.text.strip()}")
    for pid in product.findall("ProductIdentifier"):
        if pid.findtext("ProductIDType") == "15" and not isbn13_ok((pid.findtext("IDValue") or "").strip()):
            add("ProductIdentifier/IDValue", f"ISBN-13の形式かチェックディジットが不正: {pid.findtext('IDValue')}")
    for d in product.iter("Date"):
        if not re.fullmatch(r"\d{8}", (d.text or "").strip()):
            add("PublishingDate/Date", f"YYYYMMDD形式でない: {d.text}")
    for m in product.iter("Measure"):
        if not re.fullmatch(r"\d+(\.\d+)?", (m.findtext("Measurement") or "").strip()):
            add("Measure/Measurement", "数値でない")
        if m.findtext("MeasureUnitCode") not in (None, "mm"):
            add("Measure/MeasureUnitCode", f"mm以外: {m.findtext('MeasureUnitCode')}")
    for a in product.iter("PriceAmount"):
        if not re.fullmatch(r"\d+", (a.text or "").strip()) and product.findtext(".//CurrencyCode") == "JPY":
            add("Price/PriceAmount", f"円の価格が整数でない: {a.text}")
    for t in product.iter("TitleText"):
        if re.search(r"第\d+版|[上並]製|文庫|新書|\(.*版.*\)", t.text or ""):
            add("TitleText", f"版・製本が題名に混入していないか要確認: {t.text}")
    return out
 
def main(xml_path, codelist_csv):
    root = ET.parse(xml_path).getroot()
    strip_ns(root)
    if any(re.fullmatch(r"[a-z]\d{3}", e.tag) for e in root.iter()):
        print("short tag が使われている(Reference name との混在は不可)")
    codes = load_codes(codelist_csv)
    products = root.findall("Product")
    rows = [r for p in products for r in check(p, codes)]
    # 全件横断: 同じファイル内で価格種別が揺れていないか
    kinds = {p.findtext(".//PriceType") for p in products}
    if len(kinds) > 1:
        rows.append(("(全体)", "Price/PriceType", f"税込・税抜の種別が混在: {sorted(k or '' for k in kinds)}"))
    w = csv.writer(sys.stdout)
    w.writerow(["record", "path", "message"])
    w.writerows(rows)
    print(f"# {len(products)} 件中 {len({r[0] for r in rows if r[0] != '(全体)'})} 件に指摘", file=sys.stderr)
 
if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

作りで押さえた点が3つあります。

  1. 名前空間を最初に取り除き、Product以下を単純なパスで検索する。ONIXの名前空間付きのままだと、パスの指定が冗長になる
  2. 必須項目はREQUIREDの一覧で持ち、取引先ごとに書き換える。コードに直接埋めない
  3. 指摘は「どの記録の、どのパスに、何が問題か」の3列にそろえる。後で表計算ソフトに貼れる

要素の順序の誤りは、このスクリプトでは見ません。順序はスキーマ検証の領分で、FAQにもスキーマで定義された順序に従わないと検証を通らないとあります。Reference nameと短いタグ(<b037>のような形式)の混在も許されません。こちらは安価に見られるので、短いタグが1つでもあれば冒頭で知らせる作りにしています。

実行して何が出るかを見る

例として、4件のProductを持つ合成データを用意しました。1件目は問題がなく、2〜4件目に、ありがちな誤りを仕込んであります。仕込んだ誤りは、次のとおりです。

  • 2件目: ISBNがハイフン入り、寸法がcm、価格が1,980
  • 3件目: 製本が日本語、題名が空要素、出版社の要素がない
  • 4件目: ISBNのチェックディジット誤り、役割が「著」、日付が2026/12/01、題名に版と製本、価格種別が他と違う

2件目のProductの主要部分は、次のような形です。

<ProductIdentifier>
  <ProductIDType>15</ProductIDType>
  <IDValue>978-4-00-000002-4</IDValue>
</ProductIdentifier>
...
<Measure>
  <MeasureType>01</MeasureType>
  <Measurement>21.0</Measurement>
  <MeasureUnitCode>cm</MeasureUnitCode>
</Measure>
...
<PriceAmount>1,980</PriceAmount>

実行すると、次の一覧が出ました。EDItEURから取得したコードリストの第75号(CSV)を使っています。

python3 onix_check.py sample.xml codelists75.csv > report.csv
# 4 件中 3 件に指摘
record,path,message
sample-0002,ProductIdentifier/IDValue,ISBN-13の形式かチェックディジットが不正: 978-4-00-000002-4
sample-0002,Measure/MeasureUnitCode,mm以外: cm
sample-0002,Price/PriceAmount,"円の価格が整数でない: 1,980"
sample-0003,.../TitleText,空要素(要素ごと省略するのが正)
sample-0003,PublishingDetail/Publisher/PublisherName,必須項目がない
sample-0003,ProductForm,コードリスト150にない値: ハードカバー
sample-0004,ContributorRole,コードリスト17にない値: 著
sample-0004,ProductIdentifier/IDValue,ISBN-13の形式かチェックディジットが不正: 9784000000049
sample-0004,PublishingDate/Date,YYYYMMDD形式でない: 2026/12/01
sample-0004,TitleText,版・製本が題名に混入していないか要確認: やさしい会計 第2版(並製)
(全体),Price/PriceType,"税込・税抜の種別が混在: ['01', '02']"

仕込んだ誤りがすべて拾われ、問題のない1件目は一覧に出ていません。TitleTextの空要素の行は、紙面の都合でパスを省略して載せています。実際の出力ではパス全体が入ります。

指摘の読み方と、直すときの順番

指摘は、直し方の性質で3つに分かれます。

読み方

指摘の種類と対応

  • 機械的に直せるもの

    ISBNのハイフン、日付の区切り、価格のカンマ、単位のcmからmmへの換算が当てはまります。規則が一意なので、変換スクリプトにしてよい部類です。

  • 元データを見に行くもの

    チェックディジットの誤りや、必須項目の欠落です。変換で埋めると誤ったデータを作ることになるため、編集担当に戻します。

  • 人が判断するもの

    題名への版表示の混入と、価格種別の混在です。前者は文字列の癖から疑っているだけで、誤検知がありえます。後者はどちらの価格が正しいかの判断が要ります。

「機械的に直せるもの」でも、Claudeに元のXMLを直接書き換えさせないほうが安全です。元ファイルは残し、修正版を別名で出力させて、差分を目で確かめる運用にします。出力ファイル名をCLAUDE.mdに「元ファイルは上書きしない」と書いておくと、毎回の依頼で繰り返さずに済みます。

直す順番は、まず機械的に直せるものを変換してから点検をもう一度流します。それで残った指摘が、人が見るべき本来の件数です。件数が多いと、重要な欠落が表記ゆれの指摘に埋もれます。

Claude Codeに頼む手順

依頼は、スクリプトを書かせる回と、そのスクリプトを運用する回で分けます。

最初の依頼は、CLAUDE.mdを読ませたうえで、スクリプトの作成と、手元のファイルへの試し実行までを求めます。

CLAUDE.md の点検ルールに従って onix_check.py を作ってください。
sample.xml で動かし、指摘を CSV で出してください。
各指摘が CLAUDE.md のどのルールに対応するか、一覧にして報告してください。

対応表を出させる理由は、ルールにない判定をClaudeが勝手に足していないかを見るためです。たとえば、「題名に全角スペースがあれば指摘」のような独自の規則が入っていたら、ルールの外です。

スクリプトができたあとの運用では、Claude Codeを対話なしで呼び出す形が使えます。公式の概要には、ログやコマンド出力をパイプで渡し、claude -pで処理させる例があります。点検の結果をClaudeに読ませて、編集担当向けの連絡文にまとめる使い方です。

python3 onix_check.py new_titles.xml codelists75.csv 2>&1 \
  | claude -p "この指摘を、直す担当者ごとの依頼文にまとめてください"

ここでClaudeが担うのは、既に決まった指摘の文章化です。判定は上流のスクリプトが済ませています。指摘の件数や内容を、Claudeの文章から数え直すことはしません。件数は、CSVの行数が正です。

点検結果を信用しすぎないための注意

スクリプトが「指摘なし」と返しても、ONIXとして完全に正しいとは限りません。見ていない項目があるためです。

落とし穴

スクリプトの「指摘なし」は、書いたルールの範囲でしか保証しない

REQUIREDに入れなかった要素は、欠けていても出ません。ISBNの桁は見ても、そのISBNが実際に発番されたものかは見ません。受け手が突き返す理由は、スクリプトの外にもあります。取引先のガイドラインが公開されていれば、その要求をルールに足してください。

もう一つ、受け手が求める要素のうち「あっても使わないもの」は、送って構いません。FAQは、受け手は有効なメッセージを、使わない任意の内容が含まれているという理由で拒否してはならないとしています。この点から、任意項目が多いこと自体は、点検で指摘する対象になりません。見るのは「必須が足りているか」と「入っている値が正しいか」の2点です。

バージョンについても触れておきます。ONIXは2.1、3.0、3.1が併存しており、3.1は2023年の初めに公開されました。日本向けに限定した2.1の改訂(rev.04)が2011年の初めに出ている点も、FAQに書かれています。点検対象が2.1のファイルなら、要素名もパスも3.0系とは違うので、このスクリプトはそのまま使えません。入稿ファイルのバージョンは、先頭のrelease属性で見分けます。

日本の出版流通では、ISBNとは別に、電子出版物のe-code(コードリスト5の27)のような識別子も使われます。e-codeは20文字の英数字で、コードリストに桁数と形式が書かれています。取り扱う識別子が増えるなら、CODEDと検算の規則を足していくことになります。

判断のしどころ

ONIXの点検にClaude Codeを使う利点は、スクリプトを書く手間が減ることよりも、点検のルールを文章で固定できる点にあります。ルールがCLAUDE.mdに残っていれば、担当者が替わっても、取引先が増えても、同じ基準で点検が回ります。温度ログの逸脱判定をCLAUDE.mdのルールで固定する考え方は、温度ログの逸脱区間を洗い出す手順と同じです。公開前のメタデータを点検する例はZenodoへの研究データ公開前の点検にもあります。

逆に言えば、ルールを決めるところは人が持つ必要があります。

最初は数十件の小さなファイルで、指摘の内訳を眺めるところから始めるのが無難です。誤検知が多いルールは、その場で精度を直せます。慣れてから、実際の入稿ファイルに広げます。表記ゆれを外部の正規データと突き合わせる発想は、国土地理院の住所検索APIによる住所の検証で扱っています。実データを扱うときは、未発売タイトルの情報が外に出ないか、自社の情報管理の規則と照らしてください。

この記事を共有:XはてブLinkedIn