StrScanner user manual


StrScanner 取り扱い説明書

この拡張モジュールの目的

StrScanner は高速なスキャンを行うための拡張モジュールです。

Ruby レベルでは、文字列の先頭から順々にマッチを行っていこうとすると、どうしても 残りの文字列をあらためて生成してやらなければいけません。たとえば、ごく簡単な スキャナの例として次のようなものを書いたとします。

ATOM = /\A\w+/
SPACE = /\A[ \t]+/

while str.size > 0 do
  if ATOM === str then
    str = $'
    return $&
  elsif SPACE === str then
    str = $'
    return $&
  end
end

これでも確かに目的を果たすことはできるのですが、この場合だと、「$'」を呼びだすごとに マッチした後の部分の文字列が生成されてしまいます。もとの文字列が小さければよいのですが、 文字列が大きくなると急激にオーバーヘッドが増えていき、使いものにならないほどスピードが おちてしまいます。プロファイルをとってみるとわかりますが、この増加分はほとんどすべて メモリ割り当てで消費されています。
まあそれもあたりまえで、1トークンが平均5バイトくらいと仮定すると、スキャンが終わる までのメモリの総使用量は (str.size * 2) + (str.size ** 2 / 5) で、2乗のオーダーで 増加するわけです(最初の str.size * 2 は元の文字列とトークンにしたものの合計)。

str.index( regexp, point ) でいけるかなーと思ったんですが、やってみるとこれは \A を うけつけてくれないので捨てました。もちろん \A を使わないでやってもいいんですが、 そうすると今度は先頭からマッチしているかどうか調べてやる必要がでてきますし、いちいち 文字列全体にマッチをくりかえすので、文字列が大きくなるとマッチのスピードが急激に低下して しまいます。

というわけで作ってみたのが StrScanner です。C レベルで文字列をいじって文字列の再生成を ポインタのインクリメントにおきかえているので、相当オーバーヘッドが少なくなります。 簡単に計測したところでは、2KBの文字列でも実測10倍は速くなりました。 文字列が大きくなるほどさらに差はひらいて、400KBで試すともう天と地ほどの違いがでます。

簡単な使いかたと注意

まず単純な例として、上に挙げたお手軽スキャンルーチンを書きかえてみます。 見やすいように、もう一度上の版もならべておきます。

お手軽バージョン

ATOM = /\A\w+/
SPACE = /\A[ \t]+/

while str.size > 0 do
  if ATOM === str then
    str = $'
    return $&
  elsif SPACE === str then
    str = $'
    return $&
  end
end

StrScannerバージョン

ATOM = /\A\w+/
SPACE = /\A[ \t]+/

s = StrScanner.new( str )
while s.rest? do
  if temp = s.scan( ATOM ) then
    return temp
  elsif temp = s.scan( SPACE ) then
    return temp
  end
end

まず、スキャンの前にStrScannerオブジェクトを生成します。
メソッド scan は引数の正規表現とのマッチをしてその部分文字列を返し、同時に そのあとまでポインタを進めてくれます。上の例でのマッチ、再生成、文字列摘出が まとめてできるわけです。まあ、たいして難しいところもないですね。
類似のメソッドとしては、ポインタだけすすめる skip、マッチしたかどうかだけを 知る match? があります。

ちなみに、マッチに使う正規表現は必ず文字列の先頭からマッチしなければいけません (ようは、 /\A…/ じゃないとだめってことです)。もし先頭より後でマッチした場合は、 マッチしたところまで全部がマッチしたものとみなされてしまいます。例えば次のような場合は

s = StrScanner.new( "word   word" )
ret = s.scan( /\s+/ )

何事もなかったかのように ret には "word " が返ります。先頭からマッチしなかった場合、 それを知る手段はありません。skip、match? でも同様です。


StrScanner reference manual

クラスメソッド

new( str: String, dup_p = true ): StrScanner
新しいStrScannerオブジェクトを生成します。strはスキャンする文字列、dup_pは 文字列を複製して使うかどうかを真偽値で指定します。

dupしないと生成が高速になりますが、その場合もとの文字列からとりだしたポインタを そのまま使うので、もしスキャン中にその文字列がガーベージコレクトされると落ちます。 また、他のスレッドがその文字列を触れるときも危険です。StrScannerでは最初に取得した ポインタと長さを最後まで使うので、もし文字列が短かく変更されたり、realloc がおこったり すると落ちます。
よほどの理由(文字列が100MBあるとか…)がない限り、おとなしく dup しておいたほうが いいです。…てゆーか、そういう場合はそもそも別の手段を使うほうがいいですよね…

メソッド

scan( regex: Regexp ): String
正規表現regexとマッチを行って、マッチしたらスキャンポインタを進めたうえで その部分の文字列を返し、マッチしなかったらnilを返します。
skip( regex: Regexp ): Integer
正規表現regexとマッチを行って、マッチしたらスキャンポインタを進めたうえで マッチした文字列の長さを返し、マッチしなかったらnilを返します。
match?( regex: Regexp ): Boolean
正規表現regexとマッチを行って、マッチしたらスキャンポインタは進めずに マッチした文字列の長さを返し、マッチしなかったらnilを返します。
fullscan( regex: Regexp, makestr_p: Boolean, fwdptr_p: Boolean ): Object
正規表現regexとマッチを行います。マッチしなかったらnilを返します。
マッチした場合は、makestr_pが真のときにはマッチした文字列を返します。 偽のときはマッチした文字列の長さを返します。
fwdptr_pが真のときはスキャンポインタを進めます。偽のときはそのままになります。
getch : String
スキャンポインタが指す1バイトを文字列として返し、ポインタをひとつすすめます。
rest : String
スキャンポインタ以降の文字列を返します。
rest? : Boolean
スキャンポインタのうしろにまだ文字列があるかどうかを真偽値で返します。
restsize : Integer
スキャンポインタのうしろの文字列の長さを返します。
unscan
スキャンポインタを一回分もとに戻します。一回分以上は取り消せません。
matched
一回前のスキャンポインタから現在のポインタまでの文字列を返します。
matchedsize
matched と同じ文字列の長さを返します。

Copyright(c) 1998-1999 Minero Aoki