テキストの文字種分割の補足

Perl で日本語テキストを簡単に字種かたまりに分割できないかな、
と思い、perlunicode を読みながらサンプルプログラムを書いてみました。
対象テキストは UTF-8

Perl で日本語テキストを字種分割

たつをさんは,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 はさらに 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.


As a result, for mechanisms such as regular expressions, using script values produces more meaningful results than simple matches based on block names.

UAX #24: Script Names - Scripts and Blocks

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].


Generated from: Other_Alphabetic + Lu + Ll + Lt + Lm + Lo + Nl

Unicode Character Database - Alphabetic

つまり,「Letter」+「Number, Letter」+「Other_Alphabetic」なのです。上記を見てわかるとおり,Letter というと日本語の文字も含みますから,Alphabetic という語感とうらはらに,アルファベットに限定しない結果がマッチしてしまいます。

ということで,実際に(全角半角によらず)「アルファベット」にマッチさせるのは,\p{Latin} のほうがいいようです。