つれづれの正規表現 (第三巻)

これは第一巻および第二巻の続きである。 すなわち、正規表現を使いこなせないという掲示板の声に応える形で 書きはじめたものである。 いまさら書くまでもないとは思うが、私はこれを実用に供するように書こうとは 思ってはいない。正規表現に関した読みもの程度に考えてもらった方がよい。 それが証拠に正規表現の解説などまったく無視して書いた部分もある。

例によって、 このテキストを読まれる際には何等かの正規表現のリファレンスなどを 手もとに置いて読み進まれることを希望する。繰り返すが、このテキストには 正規表現の一覧表などはない。また、このテキストで扱う 正規表現は Vine Linux 1.1 にインストールされている perl 5.00404 の 正規表現である。スクリプトはすべてこの perl で動作を確認している。 日本語化された jperl などを使用されている方は正規表現の 解釈に於いて動作が違う場合もあるので注意されたい。

この巻は以前に掲示板で何か良い『正規表現のドリル』みたいな ものはないかと、問われたことに端を発している。 色々考えた挙げ句に、 BNF あるいは BNF もどきで書いてあるものを正規表現に翻訳して見る ことを思い付いた。これなら、RFC などを漁ればいくらでも 『正規表現の練習問題』を手にいれることが出来る。 そういったわけで、この巻にはそういった練習をするのにはどうしたら良いかの 案内の役割がある。 この他にも、W3C にある DTD なんかが手頃かも知れない。 ただし、正規表現はかっこの対の認識や入れ子構造までは 捕捉できないので限界はある。これには注意されたい。

ところで、この巻を途中まで書いたところで Jeffrey Friedl の『正規表現』を 買って読んでみた。書評などでこの本の内容については大雑把に知っていたので あるが、私が読んでみた限り書評からイメージされる内容とはかけ離れていた。 難しい本だと聞いていたのだが、確かに正規表現のコンパイルの過程などを 記した部分は難しいかも知れないが、他の部分はそんなに難しいと言うほどの ものではない。特に perl を使っている人には参考になる部分が多いと思う。 正規表現を離れて perl の動作について詳しく書いてあるからだ。 当初このテキストはそんなに難しいものをターゲットにせずに、いわば、 正規表現と perl の入門をかねたつもりで書いていた。 しかし、Jeffrey Friedl の『正規表現』を読む限りでは、ある意味 このテキストは無意味と言えば無意味といえる。 逆に、彼の『正規表現』を読むための架け橋になるかもしれないと、いう 意味ではずいぶんと意味のあるものになったかも知れない。 いずれにしても、内容はかなり似通っている。第二巻の IP アドレスの 正規表現もまったく同じことが書いてある。当然のことながら、 Friedl の正規表現の方がパフォーマンスは良い。

Augmented Bakus-Naur Form (ABNF)

BNF というのはプログラミング言語の文法を規定するのに 良く用いられる。例えば、perl4 のキャメルブックの最後に perl4 の文法を 書いたものがある。この数式の羅列のようなものが BNF である。 perl4 のキャメルブックのは正式なものではなくてより人間が見やすいようなも のになっている。( perl5 の場合はもはや BNF では文法を規定できない。) C 言語なども文法は BNF を使って規定されている。 プログラミング言語の文法と言うとおそらく多くの人は 尻込みをしてしまうかも知れないが、 例えば、ちょっとした式を定義するのにも使える。 実際に、RFC でこれが使われはじめたのは RFC733 や RFC822 あたりからである。電子メール関係の定義に これを使うと記述が簡潔になるからである。 以後、インターネットの様々な規格が BNF で定義されるようになった。 ちょっと見るとすぐにわかるが、これは正規表現のルールを 知っている人には簡単に理解できる。 ABNF の定義は RFC2234 に書いてある。

ルール

2.1  Rule Naming

   The name of a rule is simply the name itself; that is, a sequence of
   characters, beginning with  an alphabetic character, and followed by
   a combination of alphabetics, digits and hyphens (dashes).

        NOTE:     Rule names are case-insensitive

   The names <rulename>, <Rulename>, <RULENAME> and <rUlENamE> all refer
   to the same rule.

『ルールの名前と言うのは単純に名前それ自身である』とある。 これだけだと何が何だがわからないが、通常 BNF ではルールの名前は <rulename> などと不等号で括ることになっていから このようなことが書いてある。 ただ、英文の文章中だと不等号で括らないとわけがわからない場合があるので、 ルールの名前を呼ぶ際に <rulename> などと書いても良いことになっている。 このように、 説明する文章中などで使う場合などは不等号で括っても構わないが、 文法的には ABNF の中ではそのまま rulename などとしてつかうということになっている。 それ以外で文法的に認められているのは、 繰り返しなんかを表現するために紛らわしい場合に限られている。

さらに、上のようにルールの名前に使える文字はきまっている。正規表現で表すと

[a-z][a-z0-9-]*

となる。なお、名前は大文字小文字を区別しないので、例えば、 rulenameRuleNameRULENAME も同じルールを表す名前だと見なされる。

2.2  Rule Form

   A rule is defined by the following sequence:

        name =  elements crlf

   where <name> is the name of the rule, <elements> is one or more rule
   names or terminal specifications and <crlf> is the end-of- line
   indicator, carriage return followed by line feed.  The equal sign
   separates the name from the definition of the rule.  The elements
   form a sequence of one or more rule names and/or value definitions,
   combined according to the various operators, defined in this
   document, such as alternative and repetition.

上のように『何々とは何々である』といった式の羅列が ABNF の実体で 個々の『何々とは何々である』という内容を表す式をルールと言う。 これは第二巻で正規表現を定義するのに部分にわけて変数で定義したが そのようなものと同じ考えであると理解すればいい。 まとめると

name =  elements crlf

というルールの名前は name であり、 それは elements として定義される。 ということになる。

これらのルールを書く際にはルールはイコールが揃うように、 必ず左にそろえること、 一つのルールが複数の行にまたがる場合には 二行目以降をインデントすることなどがすすめられている。 (と、いうよりはこれはかかる手間を考えると強制に近い。 綺麗なフォーマットで書かない正当な理由はちょっと考えにくい。)

2.3  Terminal Values

   Rules resolve into a string of terminal values, sometimes called
   characters.  In ABNF a character is merely a non-negative integer.
   In certain contexts a specific mapping (encoding) of values into a
   character set (such as ASCII) will be specified.

   Terminals are specified by one or more numeric characters with the
   base interpretation of those characters indicated explicitly.  The
   following bases are currently defined:

        b           =  binary

        d           =  decimal

        x           =  hexadecimal

2.2 の定義のように『何々とは何々である』を続けていくと最後には これ以上定義できないところまでたどり着く、そういった末端・終端の 定義がここにある。 なお、上の binary というのは 2 進数という意味である。

すると、

        CR          =  %d13

        CR          =  %x0D

などとなる。さらに、これらを連結させるのには

        CRLF        =  %d13.10

などとピリオドを使う。上の場合何進数であるかは既にわかっているので 二番目の 10 は十進数の 10 である。

ABNF ではリテラルを直接使って良い。ただし、ダブルクォートで括る。

        command     =  "command string"

これは自動的に commandco と… g を連結させたものであることを意味 する。なお、大文字小文字を区別しないので、

        rulename = "abc"

は "abc", "Abc", "aBc", "abC", "ABc", "aBC", "AbC", "ABC" などと同じである。 大文字小文字の区別をはっきりさせたい場合には 文字コードを使って

        rulename    =  %d97 %d98 %d99

        rulename    =  %d97.98.99

などと書かないといけない。

演算子

3.1  Concatenation                                  Rule1 Rule2

   A rule can define a simple, ordered string of values -- i.e., a
   concatenation of contiguous characters -- by listing a sequence of
   rule names.  For example:

        foo         =  %x61           ; a

        bar         =  %x62           ; b

        mumble      =  foo bar foo

        So that the rule <mumble> matches the lowercase string "aba".

連結させるにはただ並べれば良い。空白は無視される

3.2  Alternatives                               Rule1 / Rule2

   Elements separated by forward slash ("/") are alternatives.
   Therefore,

        foo / bar

   will accept <foo> or <bar>.

『または』を表す。正規表現だと通常『|』が使われる。

foo|bar
3.3  Incremental Alternatives                    Rule1 =/ Rule2

   It is sometimes convenient to specify a list of alternatives in
   fragments.  That is, an initial rule may match one or more
   alternatives, with later rule definitions adding to the set of
   alternatives.  This is particularly useful for otherwise- independent
   specifications which derive from the same parent rule set, such as
   often occurs with parameter lists.  ABNF permits this incremental
   definition through the construct:

        oldrule     =/ additional-alternatives

例えば、

        ruleset     =  alt1 / alt2

        ruleset     =/ alt3

        ruleset     =/ alt4 / alt5

        ruleset     =  alt1 / alt2 / alt3 / alt4 / alt5

と等価である。

3.4  Value Range Alternatives                           %c##-##

   A range of alternative numeric values can be specified compactly,
   using dash ("-") to indicate the range of alternative values.  Hence:

        DIGIT       =  %x30-39

ダッシュで文字コードの範囲を表す。上の場合

        DIGIT       =  "0" / "1" / "2" / "3" / "4" / "5" / "6" /

                           "7" / "8" / "9"

と同じ。正規表現で書けば

[\x30-\x39]

となる。

連結記号と範囲指定は同時には出来ない。 例えば、印字可能な文字が一つだけ CRCL にはさまれている というのは

        char-line = %x0D.0A %x20-7E %x0D.0A

と書かなければならない。

3.5  Sequence Group                             (Rule1 Rule2)

   Elements enclosed in parentheses are treated as a single element,
   whose contents are STRICTLY ORDERED.   Thus,

        elem (foo / bar) blat

   which matches (elem foo blat) or (elem bar blat).

        elem foo / bar blat

   matches (elem foo) or (bar blat).

いくつかのエレメントをグループ化するのには括弧で括る。また 括弧を使って誤解なく定義するようにと勧告されている。例えば、

        elem foo / bar blat

よりも

        (elem foo) / (bar blat)

を使うべきであると勧告されている。

3.6  Variable Repetition                                *Rule

   The operator "*" preceding an element indicates repetition. The full
   form is:

        <a>*<b>element

   where <a> and <b> are optional decimal values, indicating at least
   <a> and at most <b> occurrences of element.

正規表現で言うと

(element){a,b}

などといった書き方に相当する。 例えば、

        <a>*<b>element

elementa 以上 b 以下の繰り返しに相当する。 また、

        *<element>

は 0 回以上の繰り返しに、だから正規表現風に書けば

(element)*

となる。

        1*<element>

は一回以上の繰り返しに相当する。 これも正規表現風に書けば

(element){1,}

である。

        3*3<element>

はちょうど 3 回の繰り返しとなる。そこで、

3.7  Specific Repetition                                  nRule

   A rule of the form:

        <n>element

   is equivalent to

        <n>*<n>element

   That is, exactly  <N>  occurrences  of <element>. Thus 2DIGIT is a
   2-digit number, and 3ALPHA is a string of three alphabetic
   characters.

などといった定義もある。 これは良く使われるからである。 これは正規表現の場合

(element){3}

などと書ける。 さらに、

3.8  Optional Sequence                                   [RULE]

   Square brackets enclose an optional element sequence:

        [foo bar]

   is equivalent to

        *1(foo bar).

という記法もあり、正規表現でいうと疑問符に相当する。

(element)?
3.9  ; Comment

   A semi-colon starts a comment that continues to the end of line.
   This is a simple way of including useful notes in parallel with the
   specifications.

コメントである。セミコロンから行末まではコメントとして扱われる。

3.10 Operator Precedence

   The various mechanisms described above have the following precedence,
   from highest (binding tightest) at the top, to lowest and loosest at
   the bottom:

        Strings, Names formation
        Comment
        Value range
        Repetition
        Grouping, Optional
        Concatenation
        Alternative

   Use of the alternative operator, freely mixed with concatenations can
   be confusing.

        Again, it is recommended that the grouping operator be used to
        make explicit concatenation groups.

例によって演算子には優先度がある。 上が一番優先度が高く下へいくほど優先度はさがる。ただし、基本的には 括弧を使ってグルーピングした方が良い。

コアルール

簡単な練習に良いので、RFC2234 の APPENDIX にある CORE とよばれる ルールセットを見てみよう。

6.1  Core Rules

   Certain  basic  rules  are  in uppercase, such as SP, HTAB, CRLF,
   DIGIT, ALPHA, etc.

        ALPHA          =  %x41-5A / %x61-7A   ; A-Z / a-z

        BIT            =  "0" / "1"

        CHAR           =  %x01-7F
                               ; any 7-bit US-ASCII character,
                                  excluding NUL

        CR             =  %x0D
                               ; carriage return

        CRLF           =  CR LF
                               ; Internet standard newline

        CTL            =  %x00-1F / %x7F
                               ; controls

        DIGIT          =  %x30-39
                               ; 0-9

        DQUOTE         =  %x22
                               ; " (Double Quote)

        HEXDIG         =  DIGIT / "A" / "B" / "C" / "D" / "E" / "F"

        HTAB           =  %x09
                               ; horizontal tab

        LF             =  %x0A
                               ; linefeed

        LWSP           =  *(WSP / CRLF WSP)
                               ; linear white space (past newline)

        OCTET          =  %x00-FF
                               ; 8 bits of data

        SP             =  %x20
                               ; space

        VCHAR          =  %x21-7E
                               ; visible (printing) characters

        WSP            =  SP / HTAB
                               ; white space

大部分が直接に文字コードを指定しているような形なので理解に 困難はないと思われる。これを正規表現に翻訳するとどうなるかを 考えるとちょっとした正規表現の簡単な練習になる。 例えば、ALPHA

[\x41-\x5a]|[\x61-\x7a]

である。ただし、正規表現なら普通は

[a-zA-Z]

と書くところである。BIT も同様に

[01]

ですむ。 LWSP がちょっとこのなかでは複雑であるが、 WSP

[ \t]

だから

([ \t]|(\r\n[ \t])*

という感じになる。

ABNF による ABNF

ABNF 自体を ABNF で書いてみよう。これには上のコアルールセットも 使っている。

   This syntax uses the rules provided in Appendix A (Core).

        rulelist       =  1*( rule / (*c-wsp c-nl) )

        rule           =  rulename defined-as elements c-nl
                               ; continues if next line starts
                               ;  with white space

        rulename       =  ALPHA *(ALPHA / DIGIT / "-")

        defined-as     =  *c-wsp ("=" / "=/") *c-wsp
                               ; basic rules definition and
                               ;  incremental alternatives

        elements       =  alternation *c-wsp

        c-wsp          =  WSP / (c-nl WSP)

        c-nl           =  comment / CRLF
                               ; comment or newline

        comment        =  ";" *(WSP / VCHAR) CRLF

        alternation    =  concatenation
                          *(*c-wsp "/" *c-wsp concatenation)

        concatenation  =  repetition *(1*c-wsp repetition)

        repetition     =  [repeat] element

        repeat         =  1*DIGIT / (*DIGIT "*" *DIGIT)

        element        =  rulename / group / option /
                          char-val / num-val / prose-val

        group          =  "(" *c-wsp alternation *c-wsp ")"

        option         =  "[" *c-wsp alternation *c-wsp "]"

        char-val       =  DQUOTE *(%x20-21 / %x23-7E) DQUOTE
                               ; quoted string of SP and VCHAR
                                  without DQUOTE

        num-val        =  "%" (bin-val / dec-val / hex-val)

        bin-val        =  "b" 1*BIT
                          [ 1*("." 1*BIT) / ("-" 1*BIT) ]
                               ; series of concatenated bit values
                               ; or single ONEOF range

        dec-val        =  "d" 1*DIGIT
                          [ 1*("." 1*DIGIT) / ("-" 1*DIGIT) ]

        hex-val        =  "x" 1*HEXDIG
                          [ 1*("." 1*HEXDIG) / ("-" 1*HEXDIG) ]

        prose-val      =  "<" *(%x20-3D / %x3F-7E) ">"
                               ; bracketed string of SP and VCHAR
                                  without angles
                               ; prose description, to be used as
                                  last resort

少しこれらについて正規表現で表すことを考えてみる。 ただし、正規表現は文法を規定するものではないので単純にマッチするか どうかしか判定できない。

まず、prose-val

<[\x20-\x3d\x3f-\x7e]*>

である。hex-val

x[0-9A-F]+((\.([0-9A-F]+))|(-([0-9A-F]+)))?

dec-val

d\d+((\.\d+)|(-\d+))?

bin-val は

b[01]+((\.[01]+)|(-[01]+))?

すると num-val は拡張正規表現を使えば

% (
    b[01]+((\.[01]+)|(-[01]+))? # bin-val
    |
    d\d+((\.\d+)|(-\d+))? # dec-val
    |
    x[0-9A-F]+((\.([0-9A-F]+))|(-([0-9A-F]+)))? # hex-val
  )

となる。これを普通の正規表現で書けば

%((b[01]+((\.[01]+)|(-[01]+))?)|(d\d+((\.\d+)|(-\d+))?)|(x[0-9A-F]+((\.([0-9A-F]+))|(-([0-9A-F]+)))?))

となり大変に見づらい。 char-val

"[\x20-\x21\x23-\x7e]*"

である。 commentVCHAR[\x21-\x7e]CRLF\x0d\x0a さらに WSP[\x20\x09] なので

;[\x09\x20-\x7e]*\x0d\x0a

となる。 c-nl はちょっと工夫して

(;[\x09\x20-\x7e]*)?\x0d\x0a

となる。すると c-wsp

((;[\x09\x20-\x7e]*)?\x0d\x0a)?[\x20\x09]

ただし、 最初にも注意したように正規表現は文法を規定するものではないので限界がある。 例えば、alternation をみると concatenation がその定義に入っている。 さらに、concatenation には repetition が入っている。 repetition には element があり、element には option がある。で、 option を見ると alternation となる。 これでも最後は終端値にたどり着くがこれを行うのは正規表現の仕事ではない。 ただし、電子メールアドレスや URI の類は正規表現で ある程度まで捕まえることが出来る。 正確に言うと特定の文字列が電子メールアドレスらしき姿を しているかどうかということの判定なら正規表現で出来る ということである。

誤解しないで欲しいのは、正規表現の翻訳を試みたのは あくまでも正規表現の練習になるからである。 いくら正規表現を使っても パーサとは別のレキシカルアナライザを作るのには ある程度役にたつがパーサは作れない。

URL

再び URL の話になる。ABNF をお目にかけたところで URL の ABNF を RFC1738 から抜きだして来た。 上の説明と違うのは、『または』の / のかわりに | が用いられているところ である。

; The generic form of a URL is:

genericurl     = scheme ":" schemepart

; Specific predefined schemes are defined here; new schemes
; may be registered with IANA

url            = httpurl | ftpurl | newsurl |
                 nntpurl | telneturl | gopherurl |
                 waisurl | mailtourl | fileurl |
                 prosperourl | otherurl

; new schemes follow the general syntax
otherurl       = genericurl

; the scheme is in lower case; interpreters should use case-ignore
scheme         = 1*[ lowalpha | digit | "+" | "-" | "." ]

schemepart     = *xchar | ip-schemepart


; URL schemeparts for ip based protocols:

ip-schemepart  = "//" login [ "/" urlpath ]

login          = [ user [ ":" password ] "@" ] hostport
hostport       = host [ ":" port ]
host           = hostname | hostnumber
hostname       = *[ domainlabel "." ] toplabel
domainlabel    = alphadigit | alphadigit *[ alphadigit | "-" ] alphadigit
toplabel       = alpha | alpha *[ alphadigit | "-" ] alphadigit
alphadigit     = alpha | digit
hostnumber     = digits "." digits "." digits "." digits
port           = digits
user           = *[ uchar | ";" | "?" | "&" | "=" ]
password       = *[ uchar | ";" | "?" | "&" | "=" ]
urlpath        = *xchar    ; depends on protocol see section 3.1

; The predefined schemes:

; FTP (see also RFC959)

ftpurl         = "ftp://" login [ "/" fpath [ ";type=" ftptype ]]
fpath          = fsegment *[ "/" fsegment ]
fsegment       = *[ uchar | "?" | ":" | "@" | "&" | "=" ]
ftptype        = "A" | "I" | "D" | "a" | "i" | "d"

; FILE

fileurl        = "file://" [ host | "localhost" ] "/" fpath

; HTTP

httpurl        = "http://" hostport [ "/" hpath [ "?" search ]]
hpath          = hsegment *[ "/" hsegment ]
hsegment       = *[ uchar | ";" | ":" | "@" | "&" | "=" ]
search         = *[ uchar | ";" | ":" | "@" | "&" | "=" ]

; GOPHER (see also RFC1436)

gopherurl      = "gopher://" hostport [ / [ gtype [ selector
                 [ "%09" search [ "%09" gopher+_string ] ] ] ] ]
gtype          = xchar
selector       = *xchar
gopher+_string = *xchar

; MAILTO (see also RFC822)

mailtourl      = "mailto:" encoded822addr
encoded822addr = 1*xchar               ; further defined in RFC822

; NEWS (see also RFC1036)

newsurl        = "news:" grouppart
grouppart      = "*" | group | article
group          = alpha *[ alpha | digit | "-" | "." | "+" | "_" ]
article        = 1*[ uchar | ";" | "/" | "?" | ":" | "&" | "=" ] "@" host

; NNTP (see also RFC977)

nntpurl        = "nntp://" hostport "/" group [ "/" digits ]

; TELNET

telneturl      = "telnet://" login [ "/" ]

; WAIS (see also RFC1625)

waisurl        = waisdatabase | waisindex | waisdoc
waisdatabase   = "wais://" hostport "/" database
waisindex      = "wais://" hostport "/" database "?" search
waisdoc        = "wais://" hostport "/" database "/" wtype "/" wpath
database       = *uchar
wtype          = *uchar
wpath          = *uchar

; PROSPERO

prosperourl    = "prospero://" hostport "/" ppath *[ fieldspec ]
ppath          = psegment *[ "/" psegment ]
psegment       = *[ uchar | "?" | ":" | "@" | "&" | "=" ]
fieldspec      = ";" fieldname "=" fieldvalue
fieldname      = *[ uchar | "?" | ":" | "@" | "&" ]
fieldvalue     = *[ uchar | "?" | ":" | "@" | "&" ]

; Miscellaneous definitions

lowalpha       = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" |
                 "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" |
                 "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" |
                 "y" | "z"
hialpha        = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" |
                 "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" |
                 "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"

alpha          = lowalpha | hialpha
digit          = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" |
                 "8" | "9"
safe           = "$" | "-" | "_" | "." | "+"
extra          = "!" | "*" | "'" | "(" | ")" | ","
national       = "{" | "}" | "|" | "\" | "^" | "~" | "[" | "]" | "`"
punctuation    = "<" | ">" | "#" | "%" | <">


reserved       = ";" | "/" | "?" | ":" | "@" | "&" | "="
hex            = digit | "A" | "B" | "C" | "D" | "E" | "F" |
                 "a" | "b" | "c" | "d" | "e" | "f"
escape         = "%" hex hex

unreserved     = alpha | digit | safe | extra
uchar          = unreserved | escape
xchar          = unreserved | reserved | escape
digits         = 1*digit

これを正規表現で表していこう。まず基本的なところは

lowalpha       = [a-z]
hialpha        = [A-Z]
alpha          = [a-zA-Z]
digit          = [0-9]
safe           = [\$_.+-]
extra          = [!*'(),]
national       = [][{}|\\^~`]
punctuation    = [<>#%"]
reserved       = [;/?:@&=]
hex            = [0-9a-fA-F]
escape         = %[0-9a-fA-F][0-9a-fA-F]

従って、

unreserved     = [a-zA-Z0-9\$_.+\-!*'(),]
uchar          = [a-zA-Z0-9\$_.+\-!*'(),]|%[0-9a-fA-F][0-9a-fA-F]
xchar          = [a-zA-Z0-9\$_.+\-!*'(),;/?:@&=]|%[0-9a-fA-F][0-9a-fA-F]
digits         = [0-9]+

もちろん次のように短くできる。

unreserved     = [\w\$.+\-!*'(),]
uchar          = [\w\$.+\-!*'(),]|%[\da-fA-F][\da-fA-F]
xchar          = [\w\$.+\-!*'(),;/?:@&=]|%[\da-fA-F][\da-fA-F]
digits         = \d+

最初のホスト名などから表してみよう。まずは、部分だけを考える。

domainlabel    = [a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]
toplabel       = [a-zA-Z]|[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]
alphadigit     = [a-zA-Z0-9]
hostnumber     = \d+\.\d+\.\d+\.\d+
port           = \d+
user           = ([\w\$.+\-!*'(),;?&=]|%[\da-fA-F][\da-fA-F])*
password       = ([\w\$.+\-!*'(),;?&=]|%[\da-fA-F][\da-fA-F])*
urlpath        = ([\w\$.+\-!*'(),;/?:@&=]|%[\da-fA-F][\da-fA-F])*

次に hostname を表す。長く複雑なので拡張正規表現を使うことにする。

  # hostname       = *[ domainlabel "." ] toplabel

  # *[ domainlabel "." ]
  ([a-zA-Z0-9] | [a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9] \.)*
  # toplabel
  ([a-zA-Z] | [a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])

これが出来れば host は簡単だ。

  # host           = hostname | hostnumber

  ( # hostname
    # *[ domainlabel "." ]
    ([a-zA-Z0-9] | [a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9] \.)*
    # toplabel
    ([a-zA-Z] | [a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])
  )
  |
  ( \d+\.\d+\.\d+\.\d+ ) # hostnumber

hostport

  (
    ( # hostname
      # *[ domainlabel "." ]
      ([a-zA-Z0-9] | [a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9] \.)*
      # toplabel
      ([a-zA-Z] | [a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])
    )
    |
    ( \d+\.\d+\.\d+\.\d+ ) # hostnumber
  )
  ( : \d+ )?  # port 

最後に login である。

  # login          = [ user [ ":" password ] "@" ] hostport

  (
    ( [\w\$.+\-!*'(),;?&=] | %[\da-fA-F][\da-fA-F] )* # user
    ( : ( [\w\$.+\-!*'(),;?&=] | %[\da-fA-F][\da-fA-F])* )? @
  )?
  (
    ( # hostname
      # *[ domainlabel "." ]
      ([a-zA-Z0-9] | [a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9] \.)*
      # toplabel
      ([a-zA-Z] | [a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])
    )
    |
    ( \d+\.\d+\.\d+\.\d+ ) # hostnumber
  )
  ( : \d+ )?  # port 

これだけわかると telnet はすぐに出来る。

  # telneturl      = "telnet://" login [ "/" ]

  telnet://
  (
    ( [\w\$.+\-!*'(),;?&=] | %[\da-fA-F][\da-fA-F] )* # user
    ( : ( [\w\$.+\-!*'(),;?&=] | %[\da-fA-F][\da-fA-F])* )? @
  )?
  (
    ( # hostname
      # *[ domainlabel "." ]
      ([a-zA-Z0-9] | [a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9] \.)*
      # toplabel
      ([a-zA-Z] | [a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])
    )
    |
    ( \d+\.\d+\.\d+\.\d+ ) # hostnumber
  )
  ( : \d+ )?  # port 
  /?  

大体これで準備が出来たので、ftp を見てみる。

fsegment       = ([\w\$.+\-!*'(),?:@&=]|%[\da-fA-F][\da-fA-F])*
ftptype        = [AIDaid]

となるので、これを使うと

  # fpath          = fsegment *[ "/" fsegment ]

  ( [\w\$.+\-!*'(),?:@&=] | %[\da-fA-F][\da-fA-F] )*
  (
    / ( [\w\$.+\-!*'(),?:@&=] | %[\da-fA-F][\da-fA-F] )*
  )*
  # ftpurl         = "ftp://" login [ "/" fpath [ ";type=" ftptype ]]

  ftp://
  (
    ( [\w\$.+\-!*'(),;?&=] | %[\da-fA-F][\da-fA-F] )* # user
    ( : ( [\w\$.+\-!*'(),;?&=] | %[\da-fA-F][\da-fA-F])* )? @
  )?
  (
    ( # hostname
      # *[ domainlabel "." ]
      ([a-zA-Z0-9] | [a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9] \.)*
      # toplabel
      ([a-zA-Z] | [a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])
    )
    |
    ( \d+\.\d+\.\d+\.\d+ ) # hostnumber
  )
  ( : \d+ )?  # port 
  ( /

    ( [\w\$.+\-!*'(),?:@&=] | %[\da-fA-F][\da-fA-F] )*
    (
      / ( [\w\$.+\-!*'(),?:@&=] | %[\da-fA-F][\da-fA-F] )*
    )*

    ( ;type = [AIDaid] )?
  )?

あと gophernews, nntp なども上の ABNF からわかる範囲で 書いてみれば良い練習になると思う。 もちろん、第二巻で指摘したように、不要な括弧をはずしたり あるいは括弧を (?: ) に 変えたりすることも試みると良いかも知れない。

余談だが上のパス名のところが

  / ( [\w\$.+\-!*'(),?:@&=] | %[\da-fA-F][\da-fA-F] )*
    (
      / ( [\w\$.+\-!*'(),?:@&=] | %[\da-fA-F][\da-fA-F] )*
    )*

となっている。これは例えば、

/usr///////local///bin

なんていうものにもマッチする。実は、これはこれで正しい。 実際に、

cd /usr///////local///bin

なんてやってみるとちゃんと /usr/local/bin へ移動する。

なお、このテキストでは RFC1738 を参考にして話を進めたが、 その後 RFC2396 に URI の定義がある。 正規表現の練習ではなくて別の必要があって参照するのなら、 RFC2396 などを参考にした方が良いと思う。

$Id: re3.html,v 1.2 2000/05/07 21:22:22 ageha Exp $