検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>
python_coding_challenge

【Python 雑談・雑学 + coding challenge】Unicode の正規化処理 ( normalization ) を利用して、diacritical marks ( 発音区別符号 ) を取り除こう! テキスト解析の前処理としても重要です! 投稿一覧へ戻る

Published 2020年9月24日21:35 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

問題 ( 制限時間: 45 分 ):


私の元にこんな内容のファイルが送られてきました (ファイル名は 'greek_str.txt' とします)。


δοκῶ μοι περὶ ὧν πυνθάνεσθε οὐκ ἀμελέτητος εἶναι. καὶ γὰρ ἐτύγχανον
πρῴην εἰς ἄστυ οἴκοθεν ἀνιὼν Φαληρόθεν· τῶν οὖν γνωρίμων τις
ὄπισθεν κατιδών με πόρρωθεν ἐκάλεσε, καὶ παίζων ἅμα τῇ κλήσει, “ὦ
Φαληρεύς,” ἔφη, “οὗτος Ἀπολλόδωρος, οὐ περιμένεις;” κἀγὼ ἐπιστὰς
περιέμεινα. καὶ ὅς, “Ἀπολλόδωρε,” ἔφη, “καὶ μὴν καὶ ἔναγχός σε
ἐζήτουν βουλόμενος διαπυθέσθαι τὴν Ἀγάθωνος συνουσίαν καὶ Σωκράτους
καὶ Ἀλκιβιάδου καὶ τῶν ἄλλων τῶν τότε ἐν τῷ συνδείπνῳ
παραγενομένων, περὶ τῶν ἐρωτικῶν λόγων τίνες ἦσαν· ἄλλος γάρ τίς
μοι διηγεῖτο ἀκηκοὼς Φοίνικος τοῦ Φιλίππου, ἔφη δὲ καὶ σὲ εἰδέναι.
ἀλλὰ γὰρ οὐδὲν εἶχε σαφὲς λέγειν. σὺ οὖν μοι διήγησαι· δικαιότατος
γὰρ εἶ τοὺς τοῦ ἑταίρου λόγους ἀπαγγέλλειν. πρότερον δέ μοι,” ἦ δ’
ὅς, “εἰπέ, σὺ αὐτὸς παρεγένου τῇ συνουσίᾳ ταύτῃ ἢ οὔ;”



テキスト解析を施して有用な情報を得たいのですが、そのためにはアクセント記号 ( diacritics; diacritical marks; 発音区別記号) を取り除く必要があります。


こんな感じです... (出力ファイル名は 'out.txt' とします)


δοκω μοι περι ων πυνθανεσθε ουκ αμελετητος ειναι. και γαρ ετυγχανον
πρωην εις αστυ οικοθεν ανιων Φαληροθεν· των ουν γνωριμων τις
οπισθεν κατιδων με πορρωθεν εκαλεσε, και παιζων αμα τη κλησει, “ω
Φαληρευς,” εφη, “ουτος Απολλοδωρος, ου περιμενεις;” καγω επιστας
περιεμεινα. και ος, “Απολλοδωρε,” εφη, “και μην και εναγχος σε
εζητουν βουλομενος διαπυθεσθαι την Αγαθωνος συνουσιαν και Σωκρατους
και Αλκιβιαδου και των αλλων των τοτε εν τω συνδειπνω
παραγενομενων, περι των ερωτικων λογων τινες ησαν· αλλος γαρ τις
μοι διηγειτο ακηκοως Φοινικος του Φιλιππου, εφη δε και σε ειδεναι.
αλλα γαρ ουδεν ειχε σαφες λεγειν. συ ουν μοι διηγησαι· δικαιοτατος
γαρ ει τους του εταιρου λογους απαγγελλειν. προτερον δε μοι,” η δ’
ος, “ειπε, συ αυτος παρεγενου τη συνουσια ταυτη η ου;”



さて、どうしましょう?


よろしくお願いします。



いかがだったでしょうか?


今回の問題は、Unicode の扱いに慣れている方であれば簡単、普段あまり関わっていない方には何が何やら?という両極端なものだと思います。


Unicode では「見かけ」が同じ文字であっても、その文字を表すコードポイントが複数存在しているものがあります。


例としてよく挙げられているのが、ラテン文字の小文字 (e) にアキュートアクセント (´) が付いた文字ですネ。


c = '\u00e9'

print(c) # 1: é

print(len(c)) # 1


d = '\u0065\u0301'

print(d) # 2:

print(len(d)) # 2



「見かけ」上の文字は同じでも、コンピュータが扱っているコードポイントは異なっています。


1: は「合成済み文字」とか「結合文字 ( composite character )」と呼ばれていて、


2: は「非結合文字 ( decomposed character )」とか「分解可能文字」などと呼ばれているようです。


1: と 2: は「見かけ」は同じですが表現方法が異なっていますから、プログラムでは「違うもの」として扱われます。


print(c == d) # False



しかし「テキスト」として扱う上では同じであって欲しいわけですから、ここで登場するのが「正規化 ( normalization )」です。


normalization に関する詳しい説明は省きますが、一般的に行われているのは 'NFC' という正規化形式を用いる方法で、可能であれば一旦「非結合文字」に分解し、再度「結合文字」を構成する、という正規化方法です。


from unicodedata import normalize


print(normalize('NFC', c) == normalize('NFC', d)) # True



'NFC' と対極に位置するのが 'NFD' という正規化方式で、これは全ての文字を「非結合文字」で統一するものです。


それからアクセント記号 ( diacritics ) ですが、Unicode では「結合可能な diacritical marks (combining diacritical marks)」として \u0300 - \u036f にまとめられています。


はい、実はここまでの説明で、大雑把ではありますが、今回の問題を解くための鍵は全て曝け出しました。


1: まず 'NFD' 形式で正規化を行って、分解できる文字に関しては「大元の文字 (基底文字)」と「発音区分符号 ( diacritical marks )」に分けます。


2: 分解した文字の中で、コードポイントが \u0300 - \u036f のものは diacritical marks ですから取り除きます。


3: 最終的に 'NFC' 形式で正規化を行えば、diacritical marks を含まないテキストを取得できます。




では実装してみましょう。


import re


marks_pat = re.compile('[\u0300-\u036f]')


def unmark_regex(text):
s = normalize('NFD', text) # 1:
s = marks_pat.sub('', s) # 2:
return normalize('NFC', s) # 3:


with open('greek_str.txt', encoding='utf-8') as rf, open('out.txt', 'w', encoding='utf-8') as wf:
for line in rf:
wf.write(unmark_regex(line))


with open('out.txt', encoding='utf-8') as rf:
for line in rf:
print(line.strip())

# δοκω μοι περι ων πυνθανεσθε ουκ αμελετητος ειναι. και γαρ ετυγχανον
# πρωην εις αστυ οικοθεν ανιων Φαληροθεν· των ουν γνωριμων τις
# οπισθεν κατιδων με πορρωθεν εκαλεσε, και παιζων αμα τη κλησει, “ω
# Φαληρευς,” εφη, “ουτος Απολλοδωρος, ου περιμενεις;” καγω επιστας
# περιεμεινα. και ος, “Απολλοδωρε,” εφη, “και μην και εναγχος σε
# εζητουν βουλομενος διαπυθεσθαι την Αγαθωνος συνουσιαν και Σωκρατους
# και Αλκιβιαδου και των αλλων των τοτε εν τω συνδειπνω
# παραγενομενων, περι των ερωτικων λογων τινες ησαν· αλλος γαρ τις
# μοι διηγειτο ακηκοως Φοινικος του Φιλιππου, εφη δε και σε ειδεναι.
# αλλα γαρ ουδεν ειχε σαφες λεγειν. συ ουν μοι διηγησαι· δικαιοτατος
# γαρ ει τους του εταιρου λογους απαγγελλειν. προτερον δε μοι,” η δ’
# ος, “ειπε, συ αυτος παρεγενου τη συνουσια ταυτη η ου;”



うまくいきました、ヤッホー!


この例では正規表現を利用しましたが、unicodedata モジュールには combining() が用意されています。


このメソッドは、渡された文字が正規結合文字でない場合、つまり diacritical marks である場合は 0 を返してきます。


from unicodedata import combining


def unmark_combining(text):
s = normalize('NFD', text)
s = ''.join(c for c in s if not combining(c))
return normalize('NFC', s)


with open('greek_str.txt', encoding='utf-8') as rf, open('out_2.txt', 'w', encoding='utf-8') as wf:
for line in rf:
wf.write(unmark_combining(line))


with open('out_2.txt', encoding='utf-8') as rf:
for line in rf:
print(line.strip())

# δοκω μοι περι ων πυνθανεσθε ουκ αμελετητος ειναι. και γαρ ετυγχανον
# πρωην εις αστυ οικοθεν ανιων Φαληροθεν· των ουν γνωριμων τις
# οπισθεν κατιδων με πορρωθεν εκαλεσε, και παιζων αμα τη κλησει, “ω
# Φαληρευς,” εφη, “ουτος Απολλοδωρος, ου περιμενεις;” καγω επιστας
# περιεμεινα. και ος, “Απολλοδωρε,” εφη, “και μην και εναγχος σε
# εζητουν βουλομενος διαπυθεσθαι την Αγαθωνος συνουσιαν και Σωκρατους
# και Αλκιβιαδου και των αλλων των τοτε εν τω συνδειπνω
# παραγενομενων, περι των ερωτικων λογων τινες ησαν· αλλος γαρ τις
# μοι διηγειτο ακηκοως Φοινικος του Φιλιππου, εφη δε και σε ειδεναι.
# αλλα γαρ ουδεν ειχε σαφες λεγειν. συ ουν μοι διηγησαι· δικαιοτατος
# γαρ ει τους του εταιρου λογους απαγγελλειν. προτερον δε μοι,” η δ’
# ος, “ειπε, συ αυτος παρεγενου τη συνουσια ταυτη η ου;”


from filecmp import cmp


print(cmp('out.txt', 'out_2.txt')) # True



ふむ、変換したテキストを含む 2 つのファイルの内容は「同じ」であるとお墨付きをもらいました、満足!


正規表現と unicodedata モジュールの combining() を利用する方法を見てみました。


これらの実装を少し変えるだけで、ある特定の diacritical mark が付いた文字だけは残す、というような処理も簡単に記述できます。
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【Python 雑談・雑学 + coding challenge】文字列中の数字を抜き出して桁区切りをつけよう! 正規表現 (regular expression ) を使うと「えっ!?」っていうくらい簡単ですょ。lookahead と negative lookahead を使います。
【Python 雑談・雑学 + coding challenge】collections モジュールの Counter クラスと most_common メソッドを利用してシーケンス内の最頻出要素を取得しよう!
【Python 雑談・雑学 + coding challenge】シーケンス ( sequence ) における インデックス ( index ) を使った要素 1 つの取り出しと、スライス ( slice ) を利用した場合の取り出しの違いをちゃんと理解していますか?
【Python 雑談・雑学 + coding challenge】sorted 組み込み関数の key パラメータをうまく使って、カスタムオブジェクトを簡単にソートしよう! __getitem__、__len__ 特殊関数 ( special methods, dunder methods ) を実装すれば立派なシーケンス ( sequence ) です
【Python 雑談・雑学 + coding challenge】Python の pprint 機能を自分で実装してみよう! 自分なりの Pretty Print できちゃいます!!
【Python 雑談・雑学 + coding challenge】iterator protocol の実装 --- __iter__ 特殊関数は何を返すべき? イテレータオブジェクト ( iterator object ) なら何でも、そう、generator expression でもOKです!
【Python 雑談・雑学 + coding challenge】Python data structure の1つ、set を活用していますか? 複数のシーケンスの包含関係を調べるには最適です