読者です 読者をやめる 読者になる 読者になる

本当は怖い情報科学

情報系大学院生の趣味&実益ブログ。

字句解析器生成系Flexの正規表現で、Onigurumaと同じUnicodeプロパティを使えるようにしてみる

ネタ

ところで、最近のさまざまな言語仕様において、「Unicodeプロパティ」や「Unicodeカテゴリ」と呼ばれるものが使われるようになっている。細かい定義は知らないが、例えばECMAScript Language Specificationを例にとると、

UnicodeLetter
any character in the Unicode categories “Uppercase letter (Lu)”, “Lowercase letter (Ll)”, “Titlecase letter (Lt)”,“Modifier letter (Lm)”, “Other letter (Lo)”, or “Letter number (Nl)”.

と書かれている箇所がある。この、"Lu"などがUnicodeカテゴリだ。

さらに、Unicodeプロパティと呼ばれるものは、例えば「Hiragana」というものがある。文字通り「ひらがな」の文字の範囲を表している。Onigurumaという正規表現エンジンにおいては、このUnicodeプロパティ/Unicodeカテゴリをパターンとして指定することが可能だ。\p{property-name}という構文で利用できる。\p{Hiragana}と書けば、ひらがな1文字にマッチさせることが可能だ。

さて、flexレキサージェネレーターと呼ばれて構文解析器を作るときの字句解析のエンジンを生成してくれるソフトウェアだ。だいたいパーサジェネレーターのbisonとペアで使われる。正規表現を用いて文字列を切り出し、状態遷移も使って字句解析を行うことが出来る。

しかし、この正規表現エンジンは取り替えることができないため、Flexに付属の古いものを使わざるを得ない。ECMAScriptR6RSなどの近代的な(?)言語仕様の実装にはUnicodeカテゴリ/Unicodeプロパティの仕様が必須なため、これは不便だ。

というわけで、Flex正規表現内でOniguruma的なUnicodeプロパティ/Unicodeカテゴリの指定ができるように(やや強引に)変換するスクリプトを作成した。

原理は簡単で、プロパティ/カテゴリの正規表現を、文字の羅列に変換している。ちなみに入力はUTF-8を前提としている。

#!/usr/bin/ruby
# codepoint.rb

UnicodeCategories = %w{ C Cc Cf Cn Co Cs
                        L Ll Lm Lo Lt Lu
                        M Mc Me Mn
                        N Nd Nl No
                        P Pc Pd Pe Pf Pi Po Ps
                        S Sc Sk Sm So
                        Z Zl Zp Zs }

UnicodeProperties = %w{ Arabic       Armenian   Bengali     Bopomofo
                        Braille      Buginese   Buhid       Canadian_Aboriginal
                        Cherokee     Common     Coptic      Cypriot
                        Cyrillic     Deseret    Devanagari  Ethiopic
                        Georgian     Glagolitic Gothic      Greek
                        Gujarati     Gurmukhi   Han         Hangul
                        Hanunoo      Hebrew     Hiragana    Inherited
                        Kannada      Katakana   Kharoshthi  Khmer
                        Lao          Latin      Limbu       Linear_B
                        Malayalam    Mongolian  Myanmar     New_Tai_Lue
                        Ogham        Old_Italic Old_Persian Oriya
                        Osmanya      Runic      Shavian     Sinhala
                        Syloti_Nagri Syriac     Tagalog     Tagbanwa
                        Tai_Le       Tamil      Telugu      Thaana
                        Thai         Tibetan    Tifinagh    Ugaritic Yi
                 }


DataFile = File.dirname(__FILE__) + '/UnicodeData.txt'
UnicodeData = open(DataFile) { |f| f.readlines }

def main
  input = $stdin.read

  mapping = { }

  UnicodeCategories.each do |cate|
    input.gsub! /\\p\{#{cate}\}/ do
      if mapping[cate]
        mapping[cate]
      else
        STDERR.puts "expanding '#{cate}' ..."
        target_lines = UnicodeData.select { |d| d.split(';')[2] =~ /#{cate}/ }
        mapping[cate] = process_lines(target_lines)
        mapping[cate]
      end
    end
  end

  UnicodeProperties.each do |prop|
    input.gsub! /\\p\{#{prop}\}/ do
      if mapping[prop]
        mapping[prop]
      else
        STDERR.puts "expanding '#{prop}' ..."
        target_lines = UnicodeData.select { |d| d.split(';')[1] =~ /#{prop}/i }
        mapping[prop] = process_lines(target_lines)
        mapping[prop]
      end
    end
  end


  print input
end

def process_lines(lines)
  codepoints = lines.map{ |ln| ln.split(';')[0].hex }

  codepoints.map! do |code|
    if 0000_0000 <= code && code <= 0x0000_007F
      '\x%02x' % code
    elsif 0x0000_0080 <= code && code <= 0x0000_07FF
      lower  = (code & 0x3F) | 0x80
      higher = (code >> 6) & 0x1F | 0xc0
      '\x%02x\x%02x' % [higher,lower]
    else
      lower  = (code & 0x3F) | 0x80
      middle = (code >> 6) & 0x3F | 0x80
      higher = (code >> 12) & 0xF | 0xe0
      '\x%02x\x%02x\x%02x' % [higher,middle,lower]
    end
  end

  "(" + codepoints.join('|') + ")"
end

main

実行には、Unicode文字コードデータベースであるUnicodeData.txtが必要なので、ダウンロードして欲しい。

さて、サンプルのFlexファイルを作成してみる。今回は超最低限のファイルを使って実験してみる。
これは、ひらがなだったらその文字を表示・それ以外は無視というスクリプトだ。

/* hiragana.l */
%%

\p{Hiragana}+  { printf("> %s\n", yytext); }
. { }

%%

最低限過ぎる。

使い方は、この内容を標準出力でフィルタして、flexコマンドにパイプで渡してやればよい。

$ cat hiragana.l | ruby codepoint.rb | flex

# lex.yy.cというCソースファイルが生成されているので、これをコンパイル・実行する
$ gcc  lex.yy.c -lfl -o hiragana 

# 実行結果
$ ./hiragana
字句解析器生成系Flexの正規表現で、Onigurumaと同じUnicodeプロパティを使えるようにしてみる
> の
> で
> と
> じ
> を
> えるようにしてみる

ひらがなが入力されたときだけエコーバックされている。とりあえず成功っぽい。

改良の余地は多ので、精進します。

追記(2008/4/9)

好き放題に使うと、正規表現が長くなりすぎてFlexがBuffer Overflowで死ぬので、正規表現の圧縮が必要ですね!

【広告】