テキストの文字種分割の補足
Perl で日本語テキストを簡単に字種かたまりに分割できないかな、
Perl で日本語テキストを字種分割
と思い、perlunicode を読みながらサンプルプログラムを書いてみました。
対象テキストは UTF-8。
たつをさんは,m//
でマッチングさせて分割させてますけど,これだと正規表現で網羅されてないトークンが失われてしまうと思います。
#!/usr/bin/perl use strict; use warnings; use utf8; binmode \*STDOUT, ':utf8'; my $src = <<"END_DATA"; zーあyxルーラでう、う9 10AB.DE「"GH'」★で漢字をカ・ナ食ったー!?MJD39\x{2466}。 END_DATA print $src, "\n"; my @cs = ( $src =~ m/ ( \p{M}+ | \p{N}+ | \p{P}+ | \p{S}+ | \p{Z}+ | \p{C}+ | \p{Latin}+ | \p{Han}+ | \p{Hiragana} [\p{Hiragana}ー]* | \p{Katakana} [\p{Katakana}ー]* ) /gxmso ); print join(", ", @cs), "\n";
ちっと改変しましたが,基本同じコードです。
で,実行結果は(最後の「39?」の「?」は丸数字の7です),
% perl chunker.pl zーあyxルーラでう、う9 10AB.DE「"GH'」★で漢字をカ・ナ食ったー!?MJD39?。 z, あ, yx, ルーラ, でう, 、, う, 9, , 10, AB, ., DE, 「", GH, '」, ★, で, 漢字, を, カ, ・, ナ, 食, ったー, !?, MJD, 39?, 。,
先頭の「z」のあとの「ー」がトラッピングされていないので抜けてしまいます。
なので,split
して空文字列を grep
で抜くほうがベターかと思います。
(2008/02/28: 補足を書きました⇒テキストの文字種分割の補足の補足 - daily dayflower)
#!/usr/bin/perl use strict; use warnings; use utf8; binmode \*STDOUT, ':utf8'; my $src = <<"END_DATA"; zーあyxルーラでう、う9 10AB.DE「"GH'」★で漢字をカ・ナ食ったー!?MJD39\x{2466}。 END_DATA print $src, "\n"; my @cs = grep { $_ ne q{} } split m/ ( \p{M}+ | \p{N}+ | \p{P}+ | \p{S}+ | \p{Z}+ | \p{C}+ | \p{Latin}+ | \p{Han}+ | \p{Hiragana} [\p{Hiragana}ー]* | \p{Katakana} [\p{Katakana}ー]* ) /xmso, $src; print join(", ", @cs), "\n";
結果は,
% perl chunker.pl zーあyxルーラでう、う9 10AB.DE「"GH'」★で漢字をカ・ナ食ったー!?MJD39?。 z, ー, あ, yx, ルーラ, でう, 、, う, 9, , 10, AB, ., DE, 「", GH, '」, ★, で, 漢字, を, カ, ・, ナ, 食, ったー, !?, MJD, 39?, 。,
以後補足
Unicode 文字は,以下の3つの属性をもっています。
- Property (Unicode Character Databases - Property Values)
- Script (UAX #24 Script Names)
- Block (UAX #24 Scripts and Blocks)
Property はさらに General Category, Canonical Combining Class, Bidi Class 等々があります。
ここでひらがなの「は」を例にして,これらの属性を調べてみます。
まず,Property ですが,UnicodeData.txt(長大なテキストファイルなので注意)を見ればわかります。
306F;HIRAGANA LETTER HA;Lo;0;L;;;;;N;;;;;
フォーマットは Unicode Character Database - UCD Property Files - UnicodeData.txt を参照してください。
Code が U+306F,Name が「HIRAGANA LETTER HA」であること,General Category が「Letter, Other (Lo)」であること,BiDi Class が「Left-to-Right (L)」であること,などがわかります。
次に Script ですが,文字が(語弊がありますが)どの言語に属しているかということを示します。これは Scripts.txt を見ればわかります。
3041..3096 ; Hiragana # Lo [86] HIRAGANA LETTER SMALL A..HIRAGANA LETTER SMALL KE
ひらがなの「は」の場合,「Hiragana」という Script のみに属していることがわかります。
最後に Block です。めんどうなので引用します。
1.2 Scripts and Blocks
Unicode characters are also divided into non-overlapping ranges called[http://www.unicode.org/reports/tr41/tr41-1.html#Blocks:title=blocks]
. Many of these blocks have the same name as one of the scripts because characters of that script are primarily encoded in that block. However, blocks and scripts differ in the following ways:
- Blocks are simply ranges, and often contain code points that are unassigned.
- Characters from the same script may be in several different blocks.
- Characters from different scripts may be in the same block.
UAX #24: Script Names - Scripts and Blocks
As a result, for mechanisms such as regular expressions, using script values produces more meaningful results than simple matches based on block names.
Block は Blocks.txt を見ればわかります。
3040..309F; Hiragana
「Hiragana」という code block に属していることがわかります(今回の場合,たまたま Script の名称と一致しましたが,そうではない場合が多いです)。
んで,先ほどの正規表現に戻りますが,
m/ ( \p{M}+ | \p{N}+ | \p{P}+ | \p{S}+ | \p{Z}+ | \p{C}+ | \p{Latin}+ | \p{Han}+ | \p{Hiragana} [\p{Hiragana}ー]* | \p{Katakana} [\p{Katakana}ー]* ) /gxmso;
最初の行の一文字だけで指定されている \p{...}
は,Property の General Category でマッチングをかけています。後半の Latin
, Han
, Hiragana
, Katakana
というのは Script でマッチングをかけています((Script の Hiragana ではなく Block の Hiragana にマッチさせたい場合,\p{InHiragana}
のように明示的に Block で表記すれば OK です))。
具体的にどれがどうマッチするかを先ほどの例で調べてみます。
#!/usr/bin/perl use strict; use warnings; use utf8; binmode \*STDOUT, ':utf8'; my $src = <<"END_DATA"; zーあyxルーラでう、う9 10AB.DE「"GH'」★で漢字をカ・ナ食ったー!?MJD39\x{2466}。 END_DATA print $src, "\n"; my @rexs = ( '\p{Mark}+', '\p{Number}+', '\p{Punctuation}+', '\p{Symbol}+', '\p{Separator}+', '\p{Other}+', '\p{Latin}+', '\p{Han}+', '\p{Hiragana} [ー\p{Hiragana}]* ', '\p{Katakana} [ー\p{Katakana}]* ', '\p{Letter}+', '\p{Alphabetic}+', ); foreach my $rex (@rexs) { print "##### $rex #####", "\n"; my @cs = ($src =~ m/($rex)/gxms); print "'", join(q{', '}, @cs), "'" if @cs; print "\n"; }
実行結果は下記のようになります。
% perl match.pl zーあyxルーラでう、う9 10AB.DE「"GH'」★で漢字をカ・ナ食ったー!?MJD39?。 ##### \p{Mark}+ ##### ##### \p{Number}+ ##### '9', '10', '39?' ##### \p{Punctuation}+ ##### '、', '.', '「"', ''」', '・', '!?', '。' ##### \p{Symbol}+ ##### '★' ##### \p{Separator}+ ##### ' ' ##### \p{Other}+ ##### ' ' ##### \p{Latin}+ ##### 'z', 'yx', 'AB', 'DE', 'GH', 'MJD' ##### \p{Han}+ ##### '漢字', '食' ##### \p{Hiragana} [ー\p{Hiragana}]* ##### 'あ', 'でう', 'う', 'で', 'を', 'ったー' ##### \p{Katakana} [ー\p{Katakana}]* ##### 'ルーラ', 'カ', 'ナ' ##### \p{Letter}+ ##### 'zーあyxルーラでう', 'う', 'AB', 'DE', 'GH', 'で漢字をカ', 'ナ食ったー', 'MJD' ##### \p{Alphabetic}+ ##### 'zーあyxルーラでう', 'う', 'AB', 'DE', 'GH', 'で漢字をカ', 'ナ食ったー', 'MJD'
んで。
なんで長々と書いてきたかというと,以前一ヶ所だけはまったところがありまして。
先ほどの出力の最終行をみていただくとわかりますが,「Alphabetic」という Property があります。これは利便性のためにいくつかの General Category をまとめたもので,ほかにもUnicode Character Database - Properties に一覧があります。
で,Alphabetic の定義ですが,
Characters with the Alphabetic property. For more information, see Chapter 4 in [Unicode].
Unicode Character Database - Alphabetic
Generated from: Other_Alphabetic + Lu + Ll + Lt + Lm + Lo + Nl
つまり,「Letter」+「Number, Letter」+「Other_Alphabetic」なのです。上記を見てわかるとおり,Letter というと日本語の文字も含みますから,Alphabetic という語感とうらはらに,アルファベットに限定しない結果がマッチしてしまいます。
ということで,実際に(全角半角によらず)「アルファベット」にマッチさせるのは,\p{Latin}
のほうがいいようです。