[Python] 結合文字を使用した濁点や半濁点を直前の仮名と結合させる方法(ウ゛→ ヴ)


まえがき

人名検索が行えるデータベースを作る際、提供されたデータにこんな読み仮名が混在していた。

  • ヤマグチ
  • ヤマク゛チ

ヤマグチと検索してもヤマク゛チさんがヒットしないので、「ク゛」を「」に寄せる形で正規化することにした。
適当なライブラリが見当たらなかったので、自前で実装することにした奮闘記。

実装方法

  1. 文字列内の合成文字を基底文字と結合文字へ分解
  2. 1で分解した文字列をバイト列に変換
  3. 2で変換したバイト列の濁点、半濁点をUnicode結合文字の濁点、半濁点に置換
  4. 3で置換されたバイト列を文字列型へ戻す
  5. (必要に応じて)合成文字に変換

Unicodeの合成文字と結合文字

日本語の「」「」「」のように符号の有無・種類で音を表現する文字に対し、Unicodeでは合成文字結合文字を使用する二通りの表現方法がある。

「ば」の例では以下の二種類がある。

  • U+3070
  • + 濁点 U+306F + U+3099

合成文字基底文字、濁点が結合文字である。

濁点や半濁点結合文字へ置換してやればUnicode上、結合された1文字となる

Wikipediaにわかりやすい例があるので引用する。

例: â は U+00E2 (latin small letter a with circumflex) でも、U+0061 U+0302 (latin small letter a + combining circumflex accent) でも表すことができる。
結合文字 - Wikipedia

UTF-8の合成文字と結合文字

Unicode上での合成文字と結合文字の動作がわかったところで、Unicodeに対応した文字符号化方式で一番メジャーであろうUTF-8で合成文字と結合文字の動きを見る。

合成文字

import unicodedata

print(unicodedata.normalize("NFC", "ヤ マ グ チ").encode())
> b'\xe3\x83\xa4 \xe3\x83\x9e \xe3\x82\xb0 \xe3\x83\x81'

"ヤマグチ"のが、\xe3\x82\xb0符号化されている

結合文字

import unicodedata

print(unicodedata.normalize("NFD", "ヤ マ グ チ").encode())
> b'\xe3\x83\xa4 \xe3\x83\x9e \xe3\x82\xaf\xe3\x82\x99 \xe3\x83\x81'

"ヤマグチ"のが、\xe3\x82\xaf\xe3\x82\x99の組み合わせで符号化されている

濁点・半濁点の文字符号

UnicodeとUTF-8でそれぞれ対応する符号を知る必要があるが、Wikipediaにあった。

濁点 - Wikipedia
半濁点 - Wikipedia

記号 Unicode UTF-8符号 備考
U+309B \xe3\x82\x9b 全角濁点
- U+3099 \xe3\x82\x99 濁点(結合文字)
U+FF9E \xef\xbe\x9e 半角濁点
U+309C \xe3\x82\x9c 全角半濁点
- U+309A \xe3\x82\x9a 半濁点(結合文字)
U+FF9F \xef\xbe\x9f 半角半濁点

全角濁点半角濁点濁点(結合文字)へ置換、
全角半濁点半角半濁点半濁点(結合文字)へ置換する

コード

#!/usr/bin/env python3

import re
import unicodedata

def join_diacritic(text, mode="NFC"):
    """
    基底文字と濁点・半濁点を結合
    """
    # str -> bytes
    bytes_text = text.encode()

    # 濁点Unicode結合文字置換
    bytes_text = re.sub(b"\xe3\x82\x9b", b'\xe3\x82\x99', bytes_text)
    bytes_text = re.sub(b"\xef\xbe\x9e", b'\xe3\x82\x99', bytes_text)

    # 半濁点Unicode結合文字置換
    bytes_text = re.sub(b"\xe3\x82\x9c", b'\xe3\x82\x9a', bytes_text)
    bytes_text = re.sub(b"\xef\xbe\x9f", b'\xe3\x82\x9a', bytes_text)

    # bytet -> str
    text = bytes_text.decode()

    # 正規化
    text = unicodedata.normalize(mode, text)

    return text

実行
join_diacritic("ルイズ・フランソワーズ・ル・ブラン・ド・ラ・ウ゛ァリエール")
> ルイズフランソワーズブランヴァリエール

応用

ヴァリエールちゃんをウ゛ァリエールちゃんにしたい。

    # 一度結合文字へ変換して
    text = unicodedata.normalize("NFD", text)

    # 結合文字を全角濁点・半濁点へ変換すればできる
    bytes_text = re.sub(b'\xe3\x82\x99', b"\xe3\x82\x9b", bytes_text)
    bytes_text = re.sub(b"\xe3\x82\x9a", b'\xe3\x82\x9c', bytes_text)

UTF-8以外の文字符号化方式に対応する

    # 例えばこの様に実装にすれば、UTF-8以外にも対応できる
    bytes_text = re.sub("\u309B".encode(charset), "\u3099".encode(charset), bytes_text)
    bytes_text = re.sub("\uFF9E".encode(charset), "\u3099".encode(charset), bytes_text)

おわりに

Unicode結合文字の仕組み使うので、Shift_JISとかのお友達は一度Unicodeに準じた文字符号化方式にエンコードすることで使えるようになる(たぶん)