Perl で Unicode Character について触る

Python の unicodedata モジュール - bkブログ を読んで,それ Perl だとどうよと思ったんで対応をまとめてみました。utf8 flag や Encode モジュールについては特に解説しませんので。

文字の名前を取得する

charnames モジュール(Perl 5.6 より付属)の viacode 関数を使うと文字の名前を取得することができます。引数として文字列ではなく文字コードを渡すところが要注意です。

use utf8;
use charnames qw( :full );

print charnames::viacode(ord 'A')),  "\n";
# 'LATIN CAPITAL LETTER A'

print charnames::viacode(ord 'あ')), "\n";
# 'HIRAGANA LETTER A'

文字列リテラルとして標準で Wide character を認識するように,use utf8 を使用しています。

文字の名前から文字を取得する

さきほどの charnames モジュールを use すると,文字列のエスケープ表記 \N{...} で文字名を利用することができるようになります。

use utf8;
use charnames qw( :full );

binmode \*STDOUT, ':utf8';

print "\N{LATIN CAPITAL LETTER A}", "\n";
# 'A'

print "\N{HIRAGANA LETTER A}",      "\n";
# 'あ'

Wide character を標準出力に出力できるよう,STDOUT ハンドルの binmode として :utf8 を指定しています。

文字の幅を調べる

CPAN モジュールの Unicode::EastAsianWidth を使うと,regex syntax のカスタムパターン(\p{...})に InEastAsianAmbiguous, InEastAsianFullwidth, InEastAsianHalfwidth, InEastAsianNarrow, InEastAsianNeutral, InEastAsianWide というパターンが追加されます。

また,convenience pattern として InFullwidth, InHalfwidth も追加されます。

わかりにくいので実例をあげます。

use utf8;

# 全角のA
if ('A' =~ m/ \p{InEastAsianFullwidth} /xmso) {
    print "East Asian Full-width\n";
}

Pythoneast_asian_width っぽくないので,ラッパ関数を書いてみました。

use utf8;
use Unicode::EastAsianWidth;
sub say { print join(q{}, @_), "\n"; }

sub east_asian_width {
    my ($chr) = @_;
    my @eaw = (
        qr/\p{InEastAsianHalfwidth}/ => 'H',
        qr/\p{InEastAsianWide}/      => 'W',
        qr/\p{InEastAsianFullwidth}/ => 'F',
        qr/\p{InEastAsianNarrow}/    => 'Na',
        qr/\p{InEastAsianAmbiguous}/ => 'A',
        qr/\p{InEastAsianNeutral}/   => 'N',
    );

    while (@eaw) {
        my ($pat, $cat) = splice @eaw, 0, 2;
        if ($chr =~ m/$pat/) {
            return $cat;
        }
    }

    return 'N';
}

say east_asian_width("A");          # 全角のA
# 'F'

say east_asian_width("ア");          # 半角のア
# 'H'

say east_asian_width("あ");
# 'W'

say east_asian_width("A");          # ASCIIのA
# 'Na'

say east_asian_width("\x{2163}");   # IV
# 'A'

say east_asian_width("\x{0e20}");   # タイ語の文字
# 'N'

また,InFullwidth, InHalfwidth を使うと簡単に文字列幅を計算することができます。具体例として http://d.hatena.ne.jp/tokuhirom/20070514/1179108961 をあげておきます。


ところで,実は,Unicode::EastAsianWidth の 1.10 には致命的なバグがあり,上記のような結果にはなりません*1Bug #31862 for Unicode-EastAsianWidth: Makefile.PL generates invalid EastAsianWidth table のパッチをあて,自力で EastAsianWidth.txt から生成する必要があります。という部分がちょっと難しいのでわからない方は古いリビジョンを使用するか,オードリー先生が修正するのを待ちましょう。んーテスト大切。

2008/02/12 追記: 1.30 で直ったお。私は未検証ですが。

文字の数値を調べる

この機能,一見 PerlCPAN モジュールにはなさそうに見えます。

String-To-Number Conversions

Unicode does define several other decimal--and numeric--characters besides the familiar 0 to 9, such as the Arabic and Indic digits. Perl does not support string-to-number conversion for digits other than ASCII 0 to 9 (and ASCII a to f for hexadecimal).

perluniintro - Perl Unicode introduction - metacpan.org

そもそもこのプロパティ(数値・数字へのマッピング)はどこに定義されているのかというと,Unicode Character Database にて定義されています。

(6) If the character has the decimal digit property, as specified in Chapter 4 of the Unicode Standard, then the value of that digit is represented with an integer value in fields 6, 7, and 8.
(7) If the character has the digit property, but is not a decimal digit, then the value of that digit is represented with an integer value in fields 7 and 8. This covers digits that need special handling, such as the compatibility superscript digits.
(8)

If the character has the numeric property, as specified in Chapter 4 of the Unicode Standard, the value of that character is represented with an positive or negative integer or rational number in this field. This includes fractions as, e.g., "1/5" for U+2155 VULGAR FRACTION ONE FIFTH.

Some characters have these properties based on values from the Unihan data file. See Numeric_Type, Han.

UCD Documentation File Replaced

で,この UnicodeData.txt データベースを扱うモジュールが,Unicode::UCD モジュール(Perl 5.8 より付属*2)です。

UCD の decimal, digit, numeric プロパティに格納されているので charinfo 関数にて取得します*3

use utf8;
use Unicode::UCD qw( charinfo );
sub say { print join(q{}, @_), "\n"; }

say charinfo(ord '7')->{decimal};           # decimal
# '7'

say charinfo(ord "\x{2466}")->{digit};      # circle 7 digit
# '7'

say charinfo(ord "\x{215b}")->{numeric};    # 1/8 numeric
# '1/8'

say charinfo(ord "\x{2166}")->{numeric};     # VII numeric
# '7'

say charinfo(ord "七")->{numeric};           # 漢数字はない
# ''

say charinfo(ord "\x{3226}")->{numeric};    # でも括弧つき漢数字はある
# '7'

Python の関数と異なり,numeric が文字列として返されることに注意が必要です。

文字の種類を調べる

同じく Unicode::UCD のデータベースから取得することができます。

use utf8;
use Unicode::UCD qw( charinfo );
sub say { print join(q{}, @_), "\n"; }

say charinfo(ord 'a')->{category};
# 'Ll' - Letter, Lowercase

say charinfo(ord 'A')->{category};
# 'Lu' - Letter, Uppercase

say charinfo(ord 'A')->{category};  # 全角も同様
# 'Lu' - Letter, Uppercase

say charinfo(ord 'あ')->{category};
# 'Lo' - Letter, Other

その他(正規化・合成・分解)

Unicode::Normalize モジュール(Perl 5.8 より付属)を使うとテキストの正規化や合成,分解を行うことができます。拙著ですが Unicode::Normalize で遊ぶ - daily dayflower をご参照下さい。

HTML エンティティの変換

CPAN モジュールの HTML::Entities モジュール*4を使うと,utf8 flagged string を escape / unescape することができます。

use utf8;
use HTML::Entities;

binmode \*STDOUT, ':utf8';

print encode_entities("\x{20ac}"), "\n";
# '€'

print HTML::Entities::encode_entities_numeric("\x{20ac}"), "\n";
# '€'

printf "%04x\n", ord decode_entities('€');
# '20ac'

printf "%04x\n", ord decode_entities("&#x20ac")
# '20ac'

*1:テーブル構築等いろいろバグっているのです

*2:厳密にいうと Perl 5.7.3 からです

*3:ハッシュで得られます

*4:標準モジュールではないですが,LWP をインストールすると要求される HTML-Parser に含まれるので大抵の場合に存在するでしょう