Unicodeでは濁点や半濁点を別扱いしてることがあるので結合した

PDFをテキストに変換して使うことがときどきあります。

今日処理してたPDFな電子書籍の中に、テキストデータは持っているのに、なんかしらんけど検索がうまくかからないことが多い、という変なファイルがありました。ぜんぜん検索できないならまだわかるんだけど、できる検索語とできない検索語があるかんじ。

pdftotextでテキストファイルにしてみたところ、なんとこのテキストファイルが同じように検索できたりできなかったりする。さすがにちょっと不思議。

で、「が」という文字が入ってると検索がかからないのに気がついたので、「が」だけ切り出したテキストファイルを作り、ほかに普通のエディタで「が」だけ入力したテキストファイルを作って、PythonUnicodeコードポイントを見てみました。ga.txtが検索のかからないもの、ga2.txtがかかるものです。

>>> for line in open('ga.txt'):
...  line.decode('utf-8')
... 
u'\u304b\u3099'
>>> for line in open('ga2.txt'):
...  line.decode('utf-8')
... 
u'\u304c'

2文字です。

これ、普通にcat ga2.txtとやると1文字の「が」しか出てこないし、テキストエディタで見ても1文字だし、コピペしようと選択するときも1文字としてしか選べない。でも2文字のコードポイントを持っています。

ちょっと調べてみると、Unicodeには合成文字というシステムがあり、濁点付き、半濁点付きの文字(たとえば「が」)を表現するのに「が」1文字のコードポイントで表現してもいいし(U+304C)、「か(U+304B)」+「濁点(U+3099)」で表現してもいい、ということになっているのです。

検索置換してくれよう。と思ったんですが、普通にキーボードから日本語入力システムで入力できる「濁点」や「半濁点」では、こうした合成文字は入力できません。

それではこの合成文字を生成するのはどうしたらいいでしょう。Pythonではユニコードのコードポイントをそのまま入力してやれば文字列が生成できるので、これを使います。

また、「chr(コードポイントの数値表現)」で文字を生成できます。1文字で表現する濁点つきの文字は元の文字のコード+1、や半濁点つきの文字は+2のコードポイントに存在するので、人間は最初の「かきくけこ…」だけ入力し、あとはできるだけコンピュータまかせで生成してやれば間違いが少ないので、そんな感じで変換辞書を作ってやります。

Pythonのコマンドインタープリタを起動して辞書を初期化してから、先に半濁点付きの部分を作ってやります。

$ python3.4
Python 3.4.2 (v3.4.2:ab2c023a9432, Oct  5 2014, 20:42:22) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 
>>> repdict=dict()
>>> for tap in [(c +'\u309a' , chr(ord(c)+2)) for c in u'はひふへほハヒフヘホ']:
...   repdict.update({tap[0]:tap[1]})
... 
>>> repdict
{'パ': 'パ', 'プ': 'プ', 'ぴ': 'ぴ', 'ポ': 'ポ', 'ピ': 'ピ', 'ぷ': 'ぷ', 'ぽ': 'ぽ', 'ぱ': 'ぱ', 'ぺ': 'ぺ', 'は': '゚', 'ペ': 'ペ'}
>>> 

最初にrepdict=dict()で空の辞書を作ります。

for tap in [(c +'\u309a' , chr(ord(c)+2)) for c in u'はひふへほハヒフヘホ']:
の部分はうしろから読みます。まず
「はひふへほハヒフヘホ」
という文字列を1文字ずつ取ってcに代入します。

このcを使って2種類の文字を生成し、カッコでまとめておきます。
2つの文字とは「c +'\u309a'」と「chr(ord(c)+2)」です。
「c +'\u309a'」はcに入ってる文字そのものと半濁点('\u309a')を足した合成文字です。
「chr(ord(c)+2)」はord(c)でcのコードポイントを数値にして、それに2を足すことで半濁点付きの文字を1文字で表現したものを指定してます。

この処理を各文字についておこなってリスト(でまとめた形式)にしているので、この行の内の部分は

[('ぱ', 'ぱ'), ('ぴ', 'ぴ'), ('ぷ', 'ぷ'), ('ぺ', 'ぺ'), ('ぽ', 'ぽ'), ('パ', 'パ'), ('ピ', 'ピ'), ('プ', 'プ'), ('ペ', 'ペ'), ('ポ', 'ポ')]

というデータを作ります。

このリストの各タプルをまたループで処理します。「for (変数) in (シーケンス型):」でシーケンス型データ(リストやタプル)の1要素ずつを「変数」に取って処理するので、まず変数tapに最初のタプル「('ぱ', 'ぱ')」を取り、ようやく2行目に行きます。

repdict.update({tap[0]:tap[1]})

これは変換辞書repdictに新しい項目を追加するものです。
tapの1番目の要素(tap[0])をキーに、2番目の要素(tap[1])を値とした項目を追加しています。
for tap in []:のループなので、この処理をリストの各タプルについておこなっています。
最後にrepdictと入力して、中味を確かめてるわけです。

同様の処理を「かきくけこ…」と濁点についてもおこないます。

>>> for tap in [(chr(ord(c)) +'\u3099' , chr(ord(c)+1)) for c in u'かきくけこさしすせそたちつてとはひふへほカキクケコサシスセソタチツテトハヒフヘホ']:
...   repdict.update({tap[0]:tap[1]})
... 
>>> repdict
{'ヂ': 'ヂ', 'グ': 'グ', 'ボ': 'ボ', 'ぎ': 'ぎ', 'ず': 'ず', 'プ': 'プ', 'デ': 'デ', 'パ': 'パ', 'ゼ': 'ゼ', 'ぴ': 'ぴ', 'ぞ': 'ぞ', 'ブ': 'ブ', 'ギ': 'ギ', 'だ': 'だ', 'バ': 'バ', 'ぽ': 'ぽ', 'ズ': 'ズ', 'ぷ': 'ぷ', 'ポ': 'ポ', 'じ': 'じ', 'ぢ': 'ぢ', 'べ': 'べ', 'ぱ': 'ぱ', 'ジ': 'ジ', 'ザ': 'ザ', 'び': 'び', 'げ': 'げ', 'が': 'が', 'ビ': 'ビ', 'ベ': 'ベ', 'ぶ': 'ぶ', 'ば': 'ば', 'ざ': 'ざ', 'ペ': 'ペ', 'ぼ': 'ぼ', 'ヅ': 'ヅ', 'ゲ': 'ゲ', 'ぺ': 'ぺ', 'ガ': 'ガ', 'ゴ': 'ゴ', 'ゾ': 'ゾ', 'ピ': 'ピ', 'で': 'で', 'ぜ': 'ぜ', 'ぐ': 'ぐ', 'ド': 'ド', 'ど': 'ど', 'ダ': 'ダ', 'づ': 'づ', 'ご': 'ご'}

だいぶゴチャゴチャして、ほんとにちゃんと全部の要素が入ってるか心配なので確かめてみます。

>>> sorted(repdict.keys())
['が', 'ぎ', 'ぐ', 'げ', 'ご', 'ざ', 'じ', 'ず', 'ぜ', 'ぞ', 'だ', 'ぢ', 'づ', 'で', 'ど', 'ば', 'ぱ', 'び', 'ぴ', 'ぶ', 'ぷ', 'べ', 'ぺ', 'ぼ', 'ぽ', 'ガ', 'ギ', 'グ', 'ゲ', 'ゴ', 'ザ', 'ジ', 'ズ', 'ゼ', 'ゾ', 'ダ', 'ヂ', 'ヅ', 'デ', 'ド', 'バ', 'パ', 'ビ', 'ピ', 'ブ', 'プ', 'ベ', 'ペ', 'ボ', 'ポ']
>>> 

sorted(シーケンス型)は、ソート可能なシーケンス型をソートした結果を別のリストに入れて返す関数です。
これに食わせてるrepdict.keys()は、ディクショナリ型(辞書型)がソート不能なので、キーだけ取り出したリストを作っている、ということです。

              • -


まあこんな感じで用意しておいた変換辞書を使って、実際に2文字な「がぎぐ…」を1文字の「がぎぐ…」に置換するとしましょう。
普通こういう処理をするには文字列string型のtranslate()というメソッドを使うと便利なんですが、translate()は1文字を1文字に置き換える処理しかしてくれないので、元が2文字の今回は不適です。

まずはファイルを読み込みます。

>>> f=open('対象テキストファイル名')
>>> contents=f.read()
>>> f.close()

これで元ファイルの内容はcontentsに格納されました。
さて変換。ちょっと遅い処理になるけど、変換辞書のキーごとに元テキストをスキャンして置き換えていきます。
とか書くと長そうだけど、ループさせるので2行で書けます。

>>> for key in repdict.keys():
...   contents=contents.replace(key, repdict.get(key))
... 
    
「文字列.replace(元, 新)」は「文字列」の中の「元」を「新」に置き換えるメソッドです。 「辞書.get(キー)」はキーの項目の内容を呼び出すメソッドですから、repdictのキー(たとえば2文字版の「が」)ごとに検索をかけて、見つかったらそのキーの値(1文字版の「が」)に置き換える処理です。これを「がぎぐげご…」について繰り返したら終了です。人間がやったら死んじゃうけど、コンピュータなら一瞬です。 最後に置き換え済の内容をファイルに書き出します。本当にちゃんと変換できてるか心配だし、わけのわからない処理をしちゃっていても後戻りできるように別のファイルに書き出します。
>>> w=open('書きだすファイル名', 'w')
>>> w.write(contents)
>>> w.close()
これで変換前のファイルと変換後のファイルが存在してる状態になっててるはず。 Pythonを終了し、
$ diff 対象テキストファイル 書きだすファイル名
とか、
$ grep 'が' 書きだすファイル名
とかやって心ゆくまで確かめたら作業は終了です。 (コードの解説はPythonをやりはじめたばかりの人、およびリハビリ中の人向けに書きました) (もっと標準的な方法も存在してるようです http://tech.albert2005.co.jp/blog/2014/11/21/mco-normalize/