BibTeX の特殊文字を Python の bibtexparser で LaTeX 形式に変換する例

# はじめに

BibTeX ファイルには、論文のメタデータが含まれていますが、自動生成された BibTeX ファイルにはしばしばウムラウト（ä, ö, ü）やアクセント付き文字（é, è, ñ など）がそのまま含まれており、LaTeX で適切に処理されないことがあります。

この記事では、`BibTeX` の特殊文字を `Python` の `bibtexparser` で `LaTeX` 形式に変換する例について紹介します。

https://bibtexparser.readthedocs.io/en/main/index.html

例えば、次のような BibTeX エントリがあったとします。

```bibtex
@ARTICLE{Holzer1997-cr,
  author    = "Hölzer, G and Fritsch, M",
  title     = "Kα and Kβ x-ray emission lines",
  journal   = "Phys. Rev. A",
  year      = 1997
}
```

このままでは `Hölzer` という名前が正しく表示されません。正しい LaTeX 形式では次のように書くべきです。

```
  author    = "H{"o}lzer, G and Fritsch, M"
```

この変換を手作業で行うのは面倒です。そこで、Python を使って特殊文字を自動変換するスクリプトを作成します。

## 必要なライブラリ

このスクリプトには `bibtexparser` を使用します。

```sh
pip install bibtexparser
```

## 実行方法

1. `input.bib` に変換したい BibTeX ファイルを保存。
2. 上記 `rk_paper_util_bibtexparser.py input.bib` スクリプトを実行。
    ``` bash:
    python rk_paper_util_bibtexparser.py input.bib
    ```
3. `input_cvt.bib` に変換後のデータが保存される。

### 変換の確認

元の `input.bib`:
```bibtex:input.bib
@ARTICLE{Holzer1997-cr,
  author    = "Hölzer, G and Fritsch, M",
  title     = "Kα and Kβ x-ray emission lines",
  journal   = "Phys. Rev. A",
  year      = 1997
}
```

変換後の `input_cvt.bib`:
```bibtex:input_cvt.bib
@ARTICLE{Holzer1997-cr,
  author    = "H{"o}lzer, G and Fritsch, M",
  title     = "Kα and Kβ x-ray emission lines",
  journal   = "Phys. Rev. A",
  year      = 1997
}
```

このように、他のテキストに影響を与えることなく、ウムラウトの部分だけ tex 表記になると思います。

## Python スクリプトの解説

以下の Python スクリプトを実行することで、BibTeX ファイルの特殊文字を適切な LaTeX 形式に変換できます。

下記の github にもコードを置いています。

https://github.com/yamadasuzaku/rksysoft/blob/main/astro/paperutil/rk_paper_util_bibtexparser.py

```python: rk_paper_util_bibtexparser.py
#!/usr/bin/env python 

import argparse
import bibtexparser
import re
import os

# 特殊文字をLaTeXのエスケープ表現に変換するためのマッピング
TEX_REPLACEMENTS = {
    "ä": r"{\"a}", "ö": r"{\"o}", "ü": r"{\"u}", "ß": r"{\ss}",
    "Ä": r"{\"A}", "Ö": r"{\"O}", "Ü": r"{\"U}",
    "é": r"{\'e}", "è": r"{\`e}", "ê": r"{\^e}", "ë": r"{\"e}",
    "É": r"{\'E}", "È": r"{\`E}", "Ê": r"{\^E}", "Ë": r"{\"E}",
    "á": r"{\'a}", "à": r"{\`a}", "â": r"{\^a}", "ã": r"{\~a}",
    "Á": r"{\'A}", "À": r"{\`A}", "Â": r"{\^A}", "Ã": r"{\~A}",
    "í": r"{\'i}", "ì": r"{\`i}", "î": r"{\^i}", "ï": r"{\"i}",
    "Í": r"{\'I}", "Ì": r"{\`I}", "Î": r"{\^I}", "Ï": r"{\"I}",
    "ó": r"{\'o}", "ò": r"{\`o}", "ô": r"{\^o}", "õ": r"{\~o}",
    "Ó": r"{\'O}", "Ò": r"{\`O}", "Ô": r"{\^O}", "Õ": r"{\~O}",
    "ú": r"{\'u}", "ù": r"{\`u}", "û": r"{\^u}",
    "Ú": r"{\'U}", "Ù": r"{\`U}", "Û": r"{\^U}",
    "ñ": r"{\~n}", "Ñ": r"{\~N}", "ç": r"{\c c}", "Ç": r"{\c C}",
    "ń": r"{\'n}", "Ň": r"{\v{N}}", "ň": r"{\v{n}}",
    "š": r"{\v{s}}", "Š": r"{\v{S}}",
    "ž": r"{\v{z}}", "Ž": r"{\v{Z}}",
    "ř": r"{\v{r}}", "Ř": r"{\v{R}}",
    "č": r"{\v{c}}", "Č": r"{\v{C}}",
    "ě": r"{\v{e}}", "Ě": r"{\v{E}}"
}
def texify(text):
    """文字列内の特殊文字をLaTeX形式に変換する関数"""
    return re.sub(
        '|'.join(map(re.escape, TEX_REPLACEMENTS.keys())),  # 変換対象の文字を正規表現でマッチさせる
        lambda match: TEX_REPLACEMENTS[match.group(0)],  # マッチした文字をLaTeX表現に変換
        text
    )

def process_bibtex(input_file, output_file):
    """BibTeXファイルを読み込み、特殊文字をLaTeX形式に変換して保存する関数"""
    with open(input_file, encoding="utf-8") as bib_file:
        bib_database = bibtexparser.load(bib_file)  # BibTeXファイルを読み込む

    # 各エントリのすべてのフィールドを変換
    for entry in bib_database.entries:
        for key, value in entry.items():
            entry[key] = texify(value)  # LaTeX形式に変換

    # 変換後のデータを新しいBibTeXファイルとして保存
    with open(output_file, "w", encoding="utf-8") as bib_file:
        bibtexparser.dump(bib_database, bib_file)

    print(f"変換完了: {output_file}")  # 完了メッセージを表示

def main():
    """コマンドライン引数を処理し、BibTeXファイルを変換するメイン関数"""
    parser = argparse.ArgumentParser(
        description="BibTeXファイル内の特殊文字をLaTeX形式に変換するツール"
    )
    parser.add_argument("input", help="入力するBibTeXファイルのパス")
    parser.add_argument("--output", help="出力ファイルのパス（省略時は自動生成）")
    
    args = parser.parse_args()
    
    # 出力ファイル名を決定（指定がない場合は元のファイル名に "_cvt" を付ける）
    if args.output:
        output_file = args.output
    else:
        base, ext = os.path.splitext(args.input)
        output_file = f"{base}_cvt{ext}"
    
    process_bibtex(args.input, output_file)  # BibTeXファイルを変換

if __name__ == "__main__":
    main()  # メイン関数を実行
```

## **詳しい説明:** 

1. **`re.sub(pattern, replacement, text)` の `replacement` に関数を指定すると、検索結果ごとにその関数が呼ばれる。**
   - これは、通常 `replacement` には固定の文字列を指定することが多いですが、関数を指定すると、`re.sub()` はマッチした部分ごとにその関数を呼び出します。

2. **関数が呼ばれる際、`re.Match` オブジェクトが `replacement` で指定した関数に渡される。**
   - ここで渡される `re.Match` オブジェクトには、マッチした文字列に関する情報が格納されています。
   - `.group(0)` を呼び出すと、マッチした文字列全体が取得できます。

3. **したがって、`lambda match: TEX_REPLACEMENTS[match.group(0)]` の `match` には、`re.Match` オブジェクトが渡されることになる。**
   - `match.group(0)` を使うことで、マッチした文字列を取得し、それを `TEX_REPLACEMENTS` 辞書で LaTeX 形式の文字列に変換する。

### **具体的な動作を確認**
次のコードで `re.sub()` の `replacement` に関数を指定した場合の動作を確認できます。

```python
import re

# 変換マッピング
TEX_REPLACEMENTS = {
    "ä": r"{\"a}", "ö": r"{\"o}", "ü": r"{\"u}", "ß": r"{\ss}",
    "Ä": r"{\"A}", "Ö": r"{\"O}", "Ü": r"{\"U}"
}

def replacement_function(match):
    print(f"マッチした文字列: {match.group(0)}")  # どの文字がマッチしているか確認
    return TEX_REPLACEMENTS[match.group(0)]  # LaTeX 形式に変換

text = "Hölzer, Müller, Weiß"

converted_text = re.sub('|'.join(map(re.escape, TEX_REPLACEMENTS.keys())), replacement_function, text)

print(f"変換後のテキスト: {converted_text}")
```

#### **実行結果**
```
マッチした文字列: ö
マッチした文字列: ü
マッチした文字列: ß
変換後のテキスト: H{\"o}lzer, M{\"u}ller, W{\ss}
```

:::note info

### `re.sub()` の詳細解説

この関数 `re.sub()` は、`text` 内の特定の文字を置換するために使用されています。

- `re.escape(TEX_REPLACEMENTS.keys())`:
  - `TEX_REPLACEMENTS` のキー（特殊文字）を正規表現で安全に扱えるようエスケープします。
  - 例えば `ä` などの特殊文字が正規表現のメタ文字と衝突しないよう処理されます。

- `'|'.join(map(re.escape, TEX_REPLACEMENTS.keys()))`:
  - `TEX_REPLACEMENTS.keys()` は `ä`, `ö`, `ü` などの特殊文字のリストです。
  - それぞれを `re.escape()` でエスケープし、`|`（OR 演算）でつなげて一つの正規表現パターンにします。
  - 例えば、`ä|ö|ü|ß` のようなパターンが生成されます。

- `lambda match: TEX_REPLACEMENTS[match.group(0)]`:
  - `match.group(0)` は `re.sub()` によって見つかった一致部分を取得します。
  - 例えば、`ö` に一致した場合、`match.group(0)` は `ö` になります。
  - その値を `TEX_REPLACEMENTS` の辞書から取得し、対応する LaTeX 形式に変換します。
  - 例えば、`match.group(0) = "ö"` のとき、`TEX_REPLACEMENTS["ö"]` は `{"o}` となり、これに置換されます。

**具体例:**
```python
text = "Hölzer, Müller, naïve"
converted_text = texify(text)
print(converted_text)
```
**出力:**
```
H{"o}lzer, M{"u}ller, na{"i}ve
```

このように、`re.sub()` を用いることで、文字列の中に含まれる特殊文字を LaTeX 形式に適切に変換できます。

:::

## まとめ

- BibTeX の特殊文字を LaTeX 形式に自動変換する Python スクリプトを作成した。
- `bibtexparser` を使って BibTeX をパースし、ウムラウトやアクセント記号を `{"o}` のように変換。
- `re.sub()` と `match.group(0)` を活用し、正規表現を用いた効率的な変換を実現。
- `group(0)` の意味と `group(1)`, `group(2)` との違いを具体例を用いて解説しました。

これを活用し、手作業のミスを防ぎ、LaTeX でのフォーマットエラーを回避して、より正確な BibTeX ファイルを簡単に作成してみましょう。

:::note info

### (補足) `bibtexparser` と `pybtex` の違い

Python で BibTeX を処理するライブラリには [`bibtexparser`](https://bibtexparser.readthedocs.io/en/main/) のほかに [`pybtex`](https://pybtex.org) もあります。それぞれの違いを理解して適切に使い分けることが重要です。

| ライブラリ | 特徴 |
|------------|----------------|
| `bibtexparser` | BibTeX を Python の辞書形式で扱うシンプルなライブラリ。データの編集や JSON との変換が容易。 |
| `pybtex` | BibTeX の解析に加え、LaTeX スタイルのフォーマットや出力をサポート。書誌情報の整形などに適している。 |

`bibtexparser` はデータの読み込みや編集に向いているのに対し、`pybtex` は LaTeX での書誌情報のレンダリングに特化しているため、用途に応じて選択すると良いでしょう。

:::