langcodesでお手軽BCP 47入門(多言語領収書をLLMで賢くOCR)
お久しぶりです、アスエネの加藤です。アポカリプスホテル面白かったですね。
ChatGPTなどのLLM を使って多言語の領収書を OCR する時、「これはどの言語・地域のテキストなのか?」 を正しく扱えると、後処理の翻訳・集計処理がグッと楽になります。そこで本記事では、言語や地域を標準化するための規格であるBCP 47言語タグ[1]の解説から始まり、Pythonでお手軽にBCP 47を扱える langcodes の基本からLLM連携などの応用について紹介します。
1. BCP 47言語タグとは?
1-1. 概要
BCP 47(Best Current Practice 47)は、インターネットで「言語+地域+スクリプト」を表すための標準文字列形式です。ja-JP や zh-Hant-TW といった 言語タグ がこれに該当します。
| 成分 | 例 | 意味 | 
|---|---|---|
| 言語 | ja | 
ISO 639 由来(日本語) | 
| スクリプト | Hant | 
ISO 15924 由来(繁体字漢字) | 
| 地域 | TW | 
ISO 3166-1 alpha-2(台湾) | 
タグは 言語 – スクリプト – 地域 – バリアント … の順で並び、必要な部分だけ付与します。
HTTP ヘッダーや HTML、PNG メタデータ、Windows(Vista以降)など幅広く採用されています。
1-2. 日付フォーマットの違い
例えば領収書の日付を年-月-日形式に統一するためにも言語や地域の情報は重要です。BCP 47を使えば簡単に管理できます。
| 地域 | 一般的な並び順 | 例 | BCP 47 タグ例 | 
|---|---|---|---|
| 🇺🇸 アメリカ | 月 / 日 / 年 | 12/31/2025 | en-US | 
| 🇬🇧 英国・欧州 | 日 / 月 / 年 | 31/12/2025 | en-GB | 
| 🇯🇵 日本 | 年 / 月 / 日(元号も) | 2025/12/31・令和7年12月31日 | ja-JP | 
| 🇹🇭 タイ | 仏暦(西暦+543) | 31/12/2568 | th-TH | 
- 仏暦(タイ)は西暦より 543 年先行 (仏暦 2568 = 西暦 2025)
 - イスラム暦(ヒジュラ暦)やユダヤ暦は月・年が完全に異なる
 - 日本の元号(令和など)は同一年内でも途中で改元する可能性がある
 
このように、タイであれば言語情報だけでも十分ですがアメリカと英国を区別するためには地域情報は必須です。
2. langcodesとは?
2-1. 概要
PythonでBCP 47に準拠した言語タグを扱う場合はlangcodesがおススメです。
pip install langcodes で簡単にインストールできます。
- 
タグから暦・地域を推測できる
Language.get("th-TH").maximize() # -> "TH" - 
言語だけでは取れない地域固有のルールを保持
enでは曖昧でもen-US/en-GBで日付パースを分岐 
- BCP 47に完全対応し、表記ゆれにも対応可能
 - MITライセンス
 - 純粋Python実装で、標準ライブラリで完結するため安全性も高い
 - 2024年11月に最新バージョンの 3.5 がリリースされており、メンテナンス頻度も高い
 
2-2. 代表的 API
以下がlangcodesで良く使う機能です。
| 関数/メソッド | 用途 | 例 | 
|---|---|---|
Language.get(tag).language | 
言語だけ取得 | 
'en_US' → 'en'
 | 
Language.get(tag).territory_name() | 
国だけ取得 | 
'eng_US' → 'United States'
 | 
Language.get(tag).describe('ja') | 
特定言語で表示 | 
'en-US' → {'language': '英語', 'territory': 'アメリカ合衆国'}
 | 
standardize_tag(tag) | 
タグ正規化 | 
'eng_US' → 'en-US'
 | 
tag_is_valid(tag) | 
妥当性チェック | 
tag_is_valid('jp') ➔ False
 | 
best_match(desired, supported) | 
優先順位付きリストから最適マッチ | best_match('pt-BR', ['pt', 'en']) | 
Language.match_score(a, b) | 
距離スコア(0–100) | Language.get('ja').match_score('ja-JP') | 
3.x 系では match-score が「互換 API」として残りつつ、新実装は distance 準拠です(出典)。
As of langcodes version 2.0, the "score" functions (such as Language.match_score, tag_match_score, and best_match) are deprecated. They'll keep using the deprecated language match tables from around CLDR 27.
For a better measure of the closeness of two language codes, use Language.distance, tag_distance, and closest_match.
それ以外にも、READMEに様々な使い方が記載されています。
2-3. langcodesの有利点
このように非常に便利な機能を持つlangcodesですが、他にも同様の機能を持つライブラリは存在しますが、langcodes が頭一つ飛び出ている印象です。
| ライブラリ | 主な利点 | 主な欠点 | おすすめ度 | 
|---|---|---|---|
| langcodes | BCP 47 をフル実装し、旧コードや表記ゆれを自動正規化。2↔3 文字コード変換・近似マッチングなど高機能。依存ゼロで軽量、MIT ライセンス | 言語名データは別パッケージ (language_data) が必要。コミュニティ規模はやや小さめ | 
★★★ | 
| language-tags | IANA サブタグリスト準拠で BCP 47 タグの妥当性検証が正確。シンプル・軽量・MIT | 検証とメタ情報取得に特化し、自動正規化やマッチング等のロジックはなし。更新頻度は年1回程度と少なめ | ★★ | 
| pycountry | ISO 639/3166/15924 など膨大な公式データを網羅。多言語名称付き辞書として便利 | BCP 47 構文を扱えず正規化も不可。パッケージサイズが大きい(十数 MB)。LGPL 2.1(組み込み利用時のライセンス注意) | ★ | 
| Babel | CLDR ベースのロケール実装+日付・数値等の国際化機能が豊富。活発にメンテされ企業利用も多い | “言語コード専用”ではなく BCP 47 を部分的にしか扱えない(例: ハイフン区切り要調整)。過去に CVE あり・パッケージサイズも大きめ | ★ | 
3. langcodesの応用
3-1. BCP 47 + langcodes + Babelで簡単西暦変換
比較表にある Babel はBCP 47を扱うツールとしては不十分ですが、langcodes と組み合わせることで簡単に西暦変換が可能になります。
使い方は、以下に示すように babel.dates.parse_date(date_str, locale='th_TH') へ langcodes で正規化した文字列を渡すだけです。
from langcodes import Language
from babel.dates import parse_date
from datetime import datetime
def parse_with_region(date_str: str, tag: str) -> datetime:
    """BCP 47 タグと文字列を受け取り、西暦 datetime に変換する"""
    locale = Language.get(tag).to_babel()        # 下線区切りへ正規化
    return parse_date(date_str, locale=locale)   # Babel が暦を含めて解釈
print(parse_with_region('31/12/2568', 'th-TH'))  # 2025-12-31 00:00:00
th だけでは暦が未確定で解析エラーになり得る。th-TH と地域を明示すれば、自動で仏暦→西暦変換。同様に ja-JP なら元号入り日付もパース可能。
3-2. OCR × LLM での実践例
アスエネのAI-OCRでは様々な国の領収書データを扱うことが多く、これまで書いてきたBCP 47による日付の標準化は重要です。LLMのプロンプトで処理させることも考えていたのですが、令和→西暦などの変換途中でハルシネーションを完全に避けられないということもあり、langcodes を利用しています[2]。
また、LLMがハルシネーションでBCP 47の規格外の形式で返してきた場合でも、langcodes なら standardize_tag() と LanguageTagError で安全に正規化処理できるというのも利点でした。
from langcodes import standardize_tag, Language
from langcodes.tag_parser import LanguageTagError
# 例: tiktoken など LLM ライブラリ
def detect_lang_tag(text: str) -> str:
    prompt = f'Identify the BCP 47 language tag for the following text:\n{text}'
    tag = call_llm(prompt)  # 実装は省略
    try:
        return standardize_tag(tag)
    except LanguageTagError:
        return 'und'  # undetermined
- LLM に BCP 47 形式で回答させる(プロンプトで制約)
 - 
standardize_tag()で大小文字・ハイフンを正規化 - 翻訳 API へは地域差が重要なら 
'zh-Hant-TW'のように精密タグを渡す 
3-3. ハマりポイント & 対策
| 症状 | 原因 | 対策 | 
|---|---|---|
VSCodeなどのIDE で LanguageTagError を「未公開 API」と警告 | 
例外クラスがトップレベル再エクスポートされていない | 
from langcodes.tag_parser import LanguageTagError と明示的に import | 
en_US が無効扱いになる | 
BCP 47 ではアンダースコア不可 | 
.replace('_', '-') または standardize_tag()
 | 
zh-CN と zh-Hans が混在 | 
地域とスクリプトが混用 | 
Language.get(tag).maximize() で zh-Hans-CN に統一 | 
| 言語名が取れない | 別パッケージ依存 | pip install language_data | 
match_score() が期待通り 100 でない | 
地域差分で減点 | 冗長成分を削って比較 | 
4. まとめ
- BCP 47 は「言語+スクリプト+地域」を一行で表す国際標準
 - Python の 
langcodesがあれば、タグの解析・正規化・類似評価をワンライナーで実装できる - LLM+OCR ワークフローでは 「LLM → langcodes → 翻訳/標準化」 がシンプルかつ堅牢
 
みなさんも是非、BCP 47 + langcodesで安心安全に多言語情報を扱いましょう!
Discussion