😅

アイヌ語仮名「ㇷ゚」に対する正規表現の罠

2023/08/31に公開
4

導入

アイヌ語は日本語と異なり、閉音節(子音で終わる音節)も存在するので、表記の際音素文字であるラテン文字なら、そのまま p, t, k, m, n, s, r などの子音文字を後ろの付ければ良いわけなので、アイヌ語ローマ字表記では、何も問題が生じない。しかし、元々開音節言語である日本語に特化したカタカナのような仮名文字で表記する際、鼻音 n は「ン」でなんとかなる(実はそれでもまずい事になっているけどここでは割愛する)が、p, t, k, m, n, s, r, h はどうしようもないので、特殊の捨て仮名(小書き仮名文字)を利用することになっている。

具体的には以下のような特殊仮名文字(通称 アイヌ語仮名)である。

  • ㇷ゚ -p
  • ッ -t
  • ㇰ -k
  • ㇺ -m
  • ㇱ -s
  • ㇻ -(a)r, ㇼ -(i)r, ㇽ -(u)r, ㇾ -(e)r, ㇿ -(o)r

お分かり頂けただろうか…

問題

r がいっぱいあるのは、まあちょっとめんどくさいけど r が 5 種類あると処理すれば良いけど

p くんに着目してください

「ㇷ゚」は「プ」が小ちゃくなっているが、その肩に乗ってある丸っこい「゜」にお気づきでしょうか

そう、あれは、U+309A COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK という結合文字である。

  • ㇷ゚ -p U+31F7 U+309A
  • ッ -t U+30C3
  • ㇰ -k U+31F0
    (以下略)

以上のように、アイヌ語で使われる仮名文字の中でも「ト゚」や「ツ゚」同様、仮名文字と結合文字の2つのコードポイントの結合文字列である。

実験

何が問題かというと、正規表現エンジンによっては、U+309A を Unicode Property 的なカタカナと見なす実装とそうでない実装があるということである。例えば、「カㇷ゚」(U+30AB, U+31F7, U+309A)という文字列に対し、以下の正規表現エンジンによって、マッチングした結果を一覧した。

エンジン 正規表現 結果
Perl (PCRE 2) /\p{Katakana}/g ["カ", "ㇷ", U+309A]
Python (re) 未サポート 未サポート
Python (regex) re.findall(r'\p{Katakana}', 'カㇷ゚') ["カ", "ㇷ"]
JavaScript 'カㇷ゚'.match(/\p{Script=Katakana}/gu) ["カ", "ㇷ"]
Golang (regexp) regexp.MustCompile(`\p{Katakana}`).FindAllString("カㇷ゚", -1) ["カ", "ㇷ"]
C# Regex.Matches("カㇷ゚", @"\p{IsKatakana}"); ["カ"]
Java (java.util.regex) Pattern.compile("\\p{IsKatakana}").matcher("カㇷ゚").find(); ["カ", "ㇷ"]
Ruby 'カㇷ゚'.scan(/\p{Katakana}/) ["カ", "ㇷ"]
Rust (regex) Regex::new(r"\p{Katakana}").unwrap().find_iter("カㇷ゚") ["カ", "ㇷ"]

以上の表のように、言語によってかなりバラバラで、概ね ["カ"]["カ", "ㇷ"]["カ", "ㇷ", U+309A] の三種類の結果が出る。

対策

マッチング時に、U+309A を後ろに入れて ? でマークしする(一文字扱いする)か、カタカナと文字グループに一緒に入れる(PCREの結果に統一する)か、のが穏当かな。

Python

# pip install regex
import regex as re
re.findall(r'\p{Katakana}\u309a?', 'カㇷ゚') # ['カ', 'ㇷ゚']
re.findall(r'[\p{Katakana}\u309a]', 'カㇷ゚') # ['カ', 'ㇷ', '゚']

追記

下の追記も参照。

# pip install regex
import regex as re
re.findall(r'\p{Katakana}\p{Mn}*', 'カㇷ゚') # ['カ', 'ㇷ゚']

# pip install regex
# pip install uniseg
import regex as re
import uniseg.graphemecluster as gc
[char for char in gc.grapheme_clusters('カㇷ゚') if re.match(r'\p{Katakana}', char)] # ['カ', 'ㇷ゚']

JavaScript

'カㇷ゚'.match(/\p{Script=Katakana}\u309a?/gu) // ['カ', 'ㇷ゚']
'カㇷ゚'.match(/[\p{Script=Katakana}\u309a]/gu) // ['カ', 'ㇷ', '゚']

追記

とのことらしいので

[...new Intl.Segmenter("mul").segment("カㇷ゚")].filter(({ segment }) => /\p{sc=Katakana}/u.test(segment)).map(({ segment }) => segment); ['カ', 'ㇷ゚']
'カㇷ゚'.match(/\p{sc=Katakana}\p{Mn}*/gu)) // ['カ', 'ㇷ゚']

// npm install @stdlib/string-to-grapheme-cluster-iterator
import graphemeClusters2iterator from '@stdlib/string-to-grapheme-cluster-iterator';
  console.log(
    [...graphemeClusters2iterator('カㇷ゚')].filter((c) => /\p{sc=Katakana}/u.test(c))); // ['カ', 'ㇷ゚']

その他の言語

// TODO...

追記その二

コメント欄には大変ありがたいことに、こうなっている理由を詳しくご解説くださったコメントがございましたので、ご参考のほどお願い申し上げます。

Discussion

ピン留めされたアイテム
nanto_vinanto_vi

各言語での正規表現マッチの結果がPerlとC#だけ異なるように見えますが、Perlに関しては\p{Katakana}\p{Script_Extensions=Katakana}の省略表記とみなされるからです。(Perl 5.24までは\p{Katakana}\p{Script=Katakana}の省略表記でしたが、Perl 5.26から\p{Script_Extensions=Katakana}の省略表記に変更されました。)

Perlでも/\p{Script=Katakana}/gを使えば["カ", "ㇷ"]が返りますし、JavaScriptでも/\p{Script_Extensions=Katakana}/guを使えば["カ", "ㇷ", U+309A]が返ります。

C# (.NET)に関しては、\p{Katakana}がKatakana scriptではなくKatakana blockにマッチするからです。.NETの正規表現における\p{}はGeneral_CategoryまたはBlockプロパティしか指定できず、残念ながらScriptまたはScript_Extensionsプロパティによるマッチには対応していなさそうです。

mkpolimkpoli

詳しく補足情報をご教示いただき、ありがとうございます!大変勉強になりました。

安岡孝一安岡孝一

「U+31F7 U+309A」無茶苦茶ツライですよね。私(安岡孝一)もTransformers(python上の言語処理モジュール)でアイヌ語の言語処理に挑戦して、結局「U+31F7 U+309A」だけは特別扱いせざるを得なかったのです。そのあたり『ローマ字・カタカナ・キリル文字併用アイヌ語RoBERTa・DeBERTaモデルの開発』の脚注d)にも、チラっと書いておいたので、よければどうぞ。

mkpolimkpoli

わざわざありがとうございます!大変勉強になります!色々参考させていただきます。