【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' とします)
さて、どうしましょう?
よろしくお願いします。
私の元にこんな内容のファイルが送られてきました (ファイル名は '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
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
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())
# δοκω μοι περι ων πυνθανεσθε ουκ αμελετητος ειναι. και γαρ ετυγχανον
# πρωην εις αστυ οικοθεν ανιων Φαληροθεν· των ουν γνωριμων τις
# οπισθεν κατιδων με πορρωθεν εκαλεσε, και παιζων αμα τη κλησει, “ω
# Φαληρευς,” εφη, “ουτος Απολλοδωρος, ου περιμενεις;” καγω επιστας
# περιεμεινα. και ος, “Απολλοδωρε,” εφη, “και μην και εναγχος σε
# εζητουν βουλομενος διαπυθεσθαι την Αγαθωνος συνουσιαν και Σωκρατους
# και Αλκιβιαδου και των αλλων των τοτε εν τω συνδειπνω
# παραγενομενων, περι των ερωτικων λογων τινες ησαν· αλλος γαρ τις
# μοι διηγειτο ακηκοως Φοινικος του Φιλιππου, εφη δε και σε ειδεναι.
# αλλα γαρ ουδεν ειχε σαφες λεγειν. συ ουν μοι διηγησαι· δικαιοτατος
# γαρ ει τους του εταιρου λογους απαγγελλειν. προτερον δε μοι,” η δ’
# ος, “ειπε, συ αυτος παρεγενου τη συνουσια ταυτη η ου;”
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
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 ... -