標準添付ライブラリ紹介 【第 12 回】 正規表現 (1)

書いた人:西山

はじめに

Ruby には便利な標準添付ライブラリがたくさんありますが、なかなか知られていないのが現状です。そこで、この連載では Ruby の標準添付ライブラリを紹介していきます。

今回は、正規表現に関係するライブラリについて紹介します。 ということにして (連載タイトル上の建前) 正規表現の話をいろいろと書きます。 都合により次回にも続きます。

正規表現

リファレンスや入門書などを見ればわかることは省略します。 基本的なことや詳しいことはリファレンスマニュアルの正規表現や「詳説 正規表現 第 2 版」などを参照してください。

今回は「\」によるエスケープ、アトム、優先順位、先読みと戻り読みの話です。

その「\」は誰が解釈するのか

正規表現に限らず、エスケープがあるものを扱うときに意識しなければならないのは、そのエスケープは誰が解釈するのかと言うことです。

たとえば

 echo \\\\\\\\ | ruby -pe "gsub(/\\\\\\\\/,'[\\\\\\\\]')"

というコマンドを実行したときに何が表示されるのか、そしてその理由が説明できますか?

筆者が試したところ、

  • bash や zsh と linux 版 ruby で「[]」
  • cmd.exe と mswin32 版 ruby で「[\][\]」
  • cmd.exe と cygwin 版 ruby で「[][][][]」

が出力されました。

まず、「echo \\\\」部分です。bash や zsh の場合、

  1. シェルが「echo \\\\」を解釈して、「echo」と「\\」になる。
  2. 「echo」が「\\」を引数として実行される。
  3. 「echo」が「\\」を解釈して「\」を出力する。

という流れになります。

cmd.exe の場合は

  1. シェルが「echo \\\\」を解釈して、「echo」と「\\\\」になる。
  2. 「echo」が「\\\\」を引数として実行される。
  3. 「echo」が「\\\\」を解釈して「\\\\」を出力する。

という流れになります。

次に「ruby -pe “gsub(/\\\\/,’[\\\\]’)”」の部分です。 これも同様に bash や zsh の場合、

  1. シェルが「ruby -pe “gsub(/\\\\/,’[\\\\]’)”」を解釈して、「ruby」と「-pe」「gsub(/\\/,’[\\]’)」になる。
  2. ruby のパーサが「gsub(/\\/,’[\\]’)」を解釈して、gsubの引数が「\\」という内容の正規表現と「[\]」という内容の文字列になる。
  3. gsub の第一引数の正規表現「\\」がコンパイルされて、「\」にマッチする正規表現になる。
  4. gsub の第二引数「[\]」は「$1」などの代わりに「\1」などのメタ文字を解釈して、置換後の文字列は「[]」となる。

という流れになります。 このプログラムに「\」が入力されて、置換が 1 回実行されて「[]」が出力されます。

cmd.exe と mswin32 版 ruby の場合は

  1. シェルが「ruby -pe “gsub(/\\\\/,’[\\\\]’)”」を解釈して、「ruby」と「-pe」「gsub(/\\\\/,’[\\\\]’)」になる。
  2. ruby のパーサが「gsub(/\\\\/,’[\\\\]’)」を解釈して、gsubの引数が「\\\\」という内容の正規表現と「[\\]」という内容の文字列になる。
  3. gsub の第一引数の正規表現「\\\\」がコンパイルされて、「\\」にマッチする正規表現になる。
  4. gsub の第二引数「[\\]」は「$1」などの代わりに「\1」などのメタ文字を解釈して、置換後の文字列は「[\]」となる。

という流れになります。 このプログラムに「\\\\」が入力されて、置換が 2 回実行されて「[\][\]」が出力されます。

cmd.exe と cygwin 版 ruby の場合は

  1. シェルが「ruby -pe “gsub(/\\\\/,’[\\\\]’)”」を解釈して、「ruby」と「-pe」「gsub(/\\\\/,’[\\\\]’)」になる。
  2. cygwin がコマンドライン全体の「ruby -pe “gsub(/\\\\/,’[\\\\]’)”」を再度解釈して、「ruby」と「-pe」「gsub(/\\/,’[\\]’)」になる。
  3. ruby のパーサが「gsub(/\\/,’[\\]’)」を解釈して、gsubの引数が「\\」という内容の正規表現と「[\]」という内容の文字列になる。
  4. gsub の第一引数の正規表現「\\」がコンパイルされて、「\」にマッチする正規表現になる。
  5. gsub の第二引数「[\]」は「$1」などの代わりに「\1」などのメタ文字を解釈して、置換後の文字列は「[]」となる。

という流れになります。 このプログラムに「\\\\」が入力されて、置換が 4 回実行されて「[][][][]」が出力されます。

このような点を理解した上で、String#gsub の説明や \ の影響 を読むと、慣れないうちはブロックを使う方が良い理由がわかると思います。

アトム

正規表現は Perl の正規表現の用語を使うと、アトムから成り立っています。

アトムには

  • 文字そのもの
  • メタ文字
  • 文字クラス

などがあります。

文字そのものとは「a」や「あ」などの文字そのものにマッチするものです。

メタ文字

Ruby の正規表現のメタ文字には

  • 「\ を伴わない英数字」はメタ文字ではない
  • 「\ を伴う記号」はメタ文字ではない (「*」は「繰り返し」でなく「アスタリスク」)

という規則があります。 「\ を伴う英数字」は必ずメタ文字というわけではありませんが、将来メタ文字になる可能性があるので、現在メタ文字になっていないものは使わない方が良いでしょう。

メタ文字に含まれるのかどうかはわからないのですが、改行文字を意味する「\n」なども正規表現では使えます。

これらは「\」の話のところで書いたように誰が解釈するのかを知っておくと、 文字列から「Regexp.new」などで正規表現オブジェクトを生成するときに「\」がいくつ必要なのかを理解しやすくなります。

例えば「/\n/」というリテラルなら、Ruby のパース時には「\n」という内容の正規表現になり、 正規表現のコンパイル時に改行文字にマッチするアトムとして解釈されます。 「Regexp.new(“\n”)」でもマッチする対象は同じになるのですが、 文字列として改行文字になっていて、 正規表現のコンパイル時には「\」の解釈がないという違いがあります。 そして「/\n/」と同じ意味になるのは

  • Regexp.new(“\n”)
  • Regexp.new(‘\n’)
  • Regexp.new(‘\n’)

などです。 この 3 つは文字列リテラルの書き方の違いだけで、「Regexp.new」の引数に渡るオブジェクトの内容は同じです。

参考として irb での実行例を載せておきます。 「Regexp.new(“\n”)」だけ違うことがわかると思います。

$ irb --simple-prompt -f
>> /\n/
=> /\n/
>> Regexp.new("\n")
=> /
/
>> Regexp.new("\\n")
=> /\n/
>> Regexp.new('\n')
=> /\n/
>> Regexp.new('\\n')
=> /\n/
>> "\\n"
=> "\\n"
>> '\n'
=> "\\n"
>> '\\n'
=> "\\n"
>> 

後方参照 (\1, \2, …)

後方参照は、改行文字のように正規表現のコンパイル時でも意味が同じということはなく、 「\」の数を間違えると全く違う意味になるので注意が必要です。 例えば「Regexp.new(“\1”)」なら文字コード 1 の文字にマッチする正規表現になり、 「Regexp.new(“\1”)」なら後方参照する括弧がないので、単独では何にもマッチしない正規表現になり (「/(\d)#{Regexp.new(“\1”)}/」のように他の括弧を含む正規表現に埋め込むとマッチする)、 「Regexp.new(“(\d)\1”)」なら「00」などのように同じ数字が続くものにマッチする正規表現になります。

「/\01/」は inspect では「/\01/」になりますが、「/\001/」と同じ意味です。 0 から始まって 3 桁までの数字の場合は 8 進数での文字コードの指定になります。 4 桁以上の場合は、例えば「\0001」なら「\000」と「1」の意味になります。

% irb --simple-prompt -f
>> "\1"
=> "\001"
>> '\1'
=> "\\1"
>> "\x01"
=> "\001"
>> Regexp.new("\1")
=> /\001/
>> Regexp.new("\\1")
=> /\1/
>> Regexp.new('\1')
=> /\1/
>> Regexp.new("\1") =~ "\1"
=> 0
>> Regexp.new('\1') =~ "\1"
=> nil
>> Regexp.new('(\d)\1') =~ "00"
=> 0
>> Regexp.new('(\d)\1') =~ "01"
=> nil
>> /\1/
=> /\1/
>> /\01/
=> /\01/
>> /\001/
=> /\001/
>> /\0001/
=> /\0001/
>> /\1/ =~ "\1"
=> nil
>> /\01/ =~ "\1"
=> 0
>> /\001/ =~ "\1"
=> 0
>> /\0001/ =~ "\1"
=> nil
>> /\0001/ =~ "\0"+"1"
=> 0
>> 

幅のあるメタ文字

「.」が普通は改行にはマッチしないことに注意が必要です。 改行もマッチさせるには「m」オプションを使って、 「/./m」のように正規表現リテラルを書くか、 「Regexp.new(‘.’, Regexp::MULTILINE)」のように「Regexp::MULTILINE」を指定した「Regexp.new」使うか、 「(?m:.)」のようにクロイスタと呼ばれる記法を使って「m」オプションを指定します。

「\ を伴う英数字」のうち幅のあるメタ文字の主なものは「\w」や「\D」のように文字クラスのような意味のものです。

幅 0 のメタ文字

幅 0 のメタ文字とは、正規表現のその位置が特定の条件にあっていればマッチするアトムです。

  • 「^」:行頭
  • 「$」:行末
  • 「\A」:文字列先頭
  • 「\Z」:文字列末尾または末尾が改行ならその改行の直前
  • 「\z」:文字列末尾
  • 「\b」:(文字クラスの外では) 語境界 (\w と \W の間)
  • 「\B」:非語境界
  • 「\G」:前回マッチした箇所 (の直後) か初回のみ先頭 (\A と同じ)

青木さんの添削にもありましたが、文字列先頭や文字列末尾の意味で「^」や「$」を使ってはいけません。Perl などの他の言語の正規表現とは意味が違うので気をつけてください。 たとえば、CGI の入力のチェックで間違って「^\d+$」のように使ってしまうと、「数字だけからなる文字列」を受け付けたつもりでも「数字のみの行」を含む文字列を受け付けることになってしまいます。

「\Z」も普通は使うことはないでしょう。「\A」とセットで文字列全体をチェックするのなら「\z」を使うべきです。「\Z」は「^」に対応する「$」のように「\A」に対応するものとして存在するだけで、普通は使うものではないと思います。行を意識して処理をしたいのなら、「\A」と「\Z」ではなく「^」と「$」が向いていることの方が多いはずです。

「\b」は文字クラスの中と外で意味が変わるメタ文字です。 文字クラスの中と外は正規表現の中と外のように別言語だと思った方が良いでしょう。 文字クラスの外の「\b」は「\w」と「\W」(または文字列の端) の間にマッチします。 たとえば「”abc.”.gsub(/\b/, ‘!’)」なら「!abc!.」になります。

「\B」は逆の意味になります。 たとえば「”abc.”.gsub(/\B/, ‘!’)」なら「a!b!c.!」になります。

「\G」は使用頻度が低いため説明は省略します。 用途によっては「\G」の代わりに strscan を使うことを検討してください。

文字クラス

文字クラスは「[]」内に列挙した文字にマッチするアトムです。

たとえば「(a b c d e f)」と「[abcdef]」はほぼ同じ意味になります。
しかし、マッチしたいものが一文字とわかっているのなら、「 」による選択を使うよりも、文字クラスを使った方が無駄なバックトラックが発生しないため速いです。        

文字クラスの使い方でよくある間違いとして「[foo]」や「[^(foo)]」のようなものがあります。 それぞれ

  • 「[fo]」と同じで「f」または「o」
  • 「[^()fo]」と同じで「()fo」以外の一文字

という意味になります。

文字クラスはうまく組み合わせるといろいろな表現が可能です。 例えば、

  • [\s\S] : 全ての文字
  • [^\W_] : 「\w」から「_」を除いたもの

優先順位

Perl の正規表現の優先順位は 4 つあります。 Ruby の正規表現の優先順位について明記されているものは見たことがないのですが、 同じと思って良いでしょう。

  1. 括弧など
  2. 「?」「*」「{m,n}」などの繰り返し
  3. シーケンス (アトムの並び)
  4. 」による選択

一番優先順位の高い括弧とは「(〜)」や「(?:〜)」などです。 算術演算の括弧で「(1+2)*3」のように使うのと同じで、優先順位の低い選択をまとめたいときに「^(class|def)\s\w+$」の使えるように優先順位が高くなっているのだと思います。

次に繰り返しです。繰り返しはシーケンスよりも優先順位が高いので「\s\w+」の場合、「\s\w」の繰り返しではなく「\w」の繰り返しという意味になります。「\s\w」の繰り返しという意味にしたい場合は「(\s\w)+」のように括弧を使う必要があります。

次にシーケンスです。シーケンスとは「ab」のように「a」と「b」が並んでいるときに「a」の次に「b」があるという意味になることです。 数式で「ab」と書いてあれば「a×b」という意味なのと同じような感じだと思えば良いと思います。

最後に「 」による選択です。「^class def\s\w+$」は選択がシーケンスよりも優先順位が低いため、「^class」または「def\s\w+$」という意味になります。
「^class\s\w+$」または「^def\s\w+$」とほぼ同じ意味にするには「^(class def)\s\w+$」のように括弧を使う必要があります。  

先読み (?=〜), (?!〜)

先読み (lookahead) は、幅 0 の正規表現です。 「^」や「$」と同じような「幅 0 の正規表現」ということをわかっておくことが重要です。

たとえば、左に「a」がない「b」という正規表現を書きたいとします。 このとき、先読みの意味を勘違いしていると「(?!a)b」と書いてしまうことになります。

ここでは「/(?!a)b/ =~ “abb”」という処理で考えてみます。 希望としては、左に「a」がない「b」ということで 3 文字目の「b」にマッチしてほしいはずです。 (以下の流れはあくまでも筆者のイメージなので、実際の実装がこうなっていることを確認したわけではありません。)

  1. まず文字列「abb」の「a」の前の位置を考えます。ここで「(?!a)」という正規表現の括弧の中に入ります。
  2. 括弧の中の正規表現「a」の頭のアトム「a」は幅 0 の正規表現ではないので文字列の次の位置を調べます。
  3. 文字列の次の「a」の位置で正規表現のアトム「a」がマッチします。
  4. 括弧の中の正規表現の末尾に到達したので、先読みの括弧の中全体としてはマッチしたことになります。
  5. 括弧の中の正規表現「a」全体としてはマッチしてしまったので、「(?!a)」という正規表現としてはマッチしなかったことになります。
  6. 外に戻って「a」の前の位置からの正規表現「(?!a)b」全体のマッチも他の選択肢がないので失敗です。
  7. バックトラックして正規表現の頭からやり直しです。同じところから調べても意味がないので、文字列側は次の位置に進みます。
  8. 正規表現の先頭が幅 0 の正規表現なので、「a」の位置は飛ばして次に進みます。
  9. 次の「a」と「b」の間の位置を考えます。ここで再び「(?!a)」という正規表現の括弧の中に入ります。
  10. 括弧の中の正規表現「a」の頭のアトム「a」は幅 0 の正規表現ではないので文字列の次の位置を調べます。
  11. 文字列の次の「b」の位置で正規表現のアトム「a」がマッチしません。
  12. マッチしなかったのでここで括弧の中の正規表現のマッチは、マッチしなかったという結果で終了です。
  13. 括弧の中の正規表現「a」としてはマッチしなかったので、「(?!a)」という正規表現としてはマッチしたことになります。
  14. 正規表現の次のアトム「b」を調べます。幅 0 のアトムではないので、文字列の次の位置に進みます。
  15. 文字列の次の位置「b」は正規表現のアトム「b」とマッチします。
  16. 正規表現全体としては「a__b__b」の 2 文字目の「b」にマッチしたことになります。

結果的に左に「a」があるかどうかに関係なく最初の「b」にマッチしてしまいました。

ここで本当に書きたかった正規表現は、次に説明する戻り読みがなければ簡単にはできません。

代わりに「(?:\A [^a])b」のように文字列先頭または「a」以外の文字に続く「b」としてマッチさせて、一緒にマッチさせた「b」の前の文字は別途処理する方法などがあります。

先読みが有用な例として、Rubyリファレンスマニュアル - 正規表現の数字を 3 桁ずつコンマで区切るサンプルの方法 2 をあげておきます。

p "tone of 12345Hz".gsub(/(\d)(?=(?:\d\d\d)+(?!\d))/, '\1,')
=> ruby 1.8.0 (2003-08-07) [i586-linux]
   "tone of 12,345Hz"

この例での処理の大まかな流れは

  1. 正規表現の最初の「\d」が文字列の「1」より左ではマッチしない。
  2. 正規表現の最初の「\d」が文字列の「1」でマッチする。括弧がついているので後方参照用に覚えておく。
  3. 先読みの部分のうち「(?:\d\d\d)+」の部分が「234」にマッチする。 1. 「(?!\d)」の中の「\d」が「5」にマッチするので「(?!\d)」全体としてはマッチしない。
  4. 「(?=(?:\d\d\d)+(?!\d))」全体としてもマッチしない。
  5. 正規表現全体としてもマッチしない。
  6. 正規表現の最初の「\d」が文字列の「2」でマッチする。括弧がついているので後方参照用に覚えておく。
  7. 先読みの部分のうち「(?:\d\d\d)+」の部分が「345」にマッチする。 1. 「(?!\d)」の中の「\d」が「H」にマッチしないので全体としてはマッチする。
  8. 「(?=(?:\d\d\d)+(?!\d))」全体としてもマッチする。
  9. 正規表現全体としてもマッチする。
  10. 「\1,」つまり「\1」は後方参照なので覚えておいた「2」を使って「2,」に置き換える。
  11. 正規表現の最初の「\d」が文字列の「3」でマッチする。括弧がついているので後方参照用に覚えておく。
  12. 先読みの部分のうち「(?:\d\d\d)+」の部分がマッチしないので、先読み全体としてもマッチしない。
  13. 正規表現全体としてもマッチしない。
  14. 正規表現の最初の「\d」が文字列の「4」でマッチする。括弧がついているので後方参照用に覚えておく。
  15. 先読みの部分のうち「(?:\d\d\d)+」の部分がマッチしないので、先読み全体としてもマッチしない。
  16. 正規表現全体としてもマッチしない。
  17. 正規表現の最初の「\d」が文字列の「5」でマッチする。括弧がついているので後方参照用に覚えておく。
  18. 先読みの部分のうち「(?:\d\d\d)+」の部分がマッチしないので、先読み全体としてもマッチしない。
  19. 正規表現全体としてもマッチしない。
  20. 正規表現の最初の「\d」が文字列の「5」より右でもマッチしない。

となると思います。

戻り読み (?<=〜), (?<!〜)

Ruby 1.8 以前では使えませんが、Ruby 1.9 以降では使えるようになっているため、戻り読み (lookbehind) も説明しておきます。

先ほどの例は戻り読みを使うと「(?<!a)b」と書くことが出来ます。

ここでは Ruby 1.9 を使って「/(?<!a)b/ =~ “abb”」という処理をすることを考えてみます。 (以下の流れはあくまでも筆者のイメージなので、実際の実装がこうなっていることを確認したわけではありません。)

  1. まず文字列「abb」の「a」の前の位置を考えます。ここで「(?<!a)」という正規表現の括弧の中に入ります。
  2. 括弧の中の正規表現「a」の長さは 1 なので、現在の位置より 1 文字分左の位置からマッチするかどうか調べます。(戻り読みでは、長さのわかる正規表現しか使えないようで、「(?<=a.*b)」のような正規表現は使えません。)
  3. 1 文字分左の位置には文字がないので、正規表現「a」にはマッチしません。
  4. 括弧の中の正規表現「a」としてはマッチしなかったので、「(?<!a)」という正規表現全体としてはマッチしたことになります。
  5. 正規表現の次のアトムの「b」を調べます。幅 0 のアトムではないので、文字列の次の位置に進みます。
  6. 文字列の次の位置「a」は正規表現のアトム「b」とマッチしません。
  7. 正規表現「(?<!a)b」全体としてもマッチしなかったことになります。
  8. バックトラックして正規表現の頭からやり直しです。同じところから調べても意味がないので、文字列側は次の位置に進みます。
  9. 文字列の次の位置「a」を調べます。最初が幅 0 の正規表現なので、次に進みます。
  10. 次の位置「a」と「b」の間を調べます。ここで再び「(?<!a)」という正規表現の括弧の中に入ります。
  11. 括弧の中の正規表現「a」の長さは 1 なので、現在の位置より 1 文字分左の位置からマッチするかどうか調べます。つまり「a」の前の位置になります。
  12. 括弧の中の正規表現「a」の最初は幅 0 の正規表現ではないので文字列の次の位置を調べます。
  13. 文字列の次の「a」の位置で正規表現がマッチします。
  14. 括弧の中の正規表現の末尾に到達したので、戻り読みの括弧の中全体としてはマッチしたことになります。
  15. 括弧の中の正規表現「a」としてはマッチしたので、「(?<!a)」という正規表現全体としてはマッチしなかったことになります。
  16. 外に戻って「a」と「b」の間の位置からの正規表現「(?<!a)b」全体もマッチしなかったことになります。
  17. バックトラックして正規表現の頭からやり直しです。同じところから調べても意味がないので、文字列側は次の位置に進みます。
  18. マッチしなかったので、文字列の次の位置「b」を調べます。最初が幅 0 の正規表現なので、次に進みます。
  19. 次の位置「b」と「b」の間を調べます。ここで再び「(?<!a)」という正規表現の括弧の中に入ります。
  20. 括弧の中の正規表現「a」の長さは 1 なので、現在の位置より 1 文字分左の位置からマッチするかどうか調べます。つまり「a」と「b」の間の位置になります。
  21. 括弧の中の正規表現「a」の最初は幅 0 の正規表現ではないので文字列の次の位置を調べます。
  22. 文字列の次の「b」の位置で正規表現がマッチしません。
  23. マッチしなかったのでここで括弧の中の正規表現のマッチは、マッチしなかったという結果で終了です。
  24. 括弧の中の正規表現「a」としてはマッチしなかったので、「(?<!a)」という正規表現としてはマッチしたことになります。

  25. 正規表現の次のアトム「b」を調べます。幅 0 のアトムではないので、文字列の次の位置に進みます。
  26. 文字列の次の位置「b」は正規表現のアトム「b」とマッチします。
  27. 正規表現全体としては「ab__b__」の 3 文字目の「b」にマッチしたことになります。

希望通り、左に「a」がない「b」ということで 3 文字目の「b」にマッチしました。

eregex

eregex というライブラリは [ruby-dev:1028] Regexp#operators からのスレッドで、 こういうものもライブラリとして作ることが出来るという例として作られたものです。 eregex.rb の中にも

# this is just a proof of concept toy.

と書いています。

どういうものかというと、eregex.rb の末尾の

if __FILE__ == $0
  p "abc" =~ /b/|/c/
  p "abc" =~ /b/&/c/
end
のように Regexp の「 」(or) や「&」(and) のようなことが出来るようになります。

さらに and や or をとったりできない、 マッチ後の MatchData がどちらのマッチ結果になるのかわからないなど、 実用するには不向きな点があるので、 あくまでも例として見るのが良いのではないかと思います。

今の Ruby で正規表現の or に相当することがしたい場合は Regexp.union を使えば良いのではないかと思います。 Regexp.union は正規表現ならそのまま、文字列なら Regexp.quote して繋げてくれるため、 「Regexp.new(Regexp.quote(str))」の代わりに使うというのもありかもしれません。

参考として irb での簡単な実行例を載せておきます。

% irb --simple-prompt -f
>> Regexp.union /./, '.'
=> /(?-mix:.)|\./
>> Regexp.union /./
=> /./
>> Regexp.union '.'
=> /\./
>> 

まとめ

この前のるびま本読書会で正規表現が苦手な人が多そうだと思ったので、正規表現について書いてみました。 正規表現の優先順位については Effective Perl で知りました。 正規表現の優先順位についての説明は滅多に見かけないのですが、 参考になれば幸いです。

正規表現は言語ごとに細かい違いがあり、日本語での用語の統一もされてないようで、 「後方参照」といえば「back reference」の訳語として使われることが多い (Ruby のリファレンスマニュアルでもそうなっている) のですが、「lookbehind」の訳語として使われることもあるようです。 この記事の草稿の段階では lookbehind の訳語として後方参照と書いていたのを、途中で気付いて直したりしました。

著者について

西山和広。 最近は自己紹介でこの連載を書いてます、と言っています。 Ruby hotlinks 五月雨版のメンテナンスもたまにやってます。

Ruby リファレンスマニュアル刷新計画は一番人手が必要そうな第 3 段階が進行中なので、るりま Wiki を参考にしてお手伝いをお願いします。

標準添付ライブラリ紹介 連載一覧