Perl 6 程式設計:Regexes

PUBLISHED ON JAN 30, 2018 — PROGRAMMING

常規表示式 (regular expression) 是一種用於字串比對的小型語言 (mini-language),這個概念最早於 1956 年提出。在許多命令列工具和程式語言中有提供常規表示式的功能,許多語言是透過函式庫的形式提供常規表示式的功能,不過也有一些語言或工具內建常規表示式,像是 AWK、Perl 5 和 Ruby 等。Perl 6 承襲 Perl 家族的傳統,將常規表示式內建在語法中。

註:Perl 6 將常規表示式改稱為 Regexes。

常規表示式的版本

常規表示式在長年演進中,出現數個大同小異的版本,常見的版本有以下數種:

  • POSIX 標準
    • BRE (Basic Regular Expression)
    • ERE (Extended Regular Expression)
    • SRE (Simple Regular Expression),即將淘汰
  • Perl 5 的常規表示式

由於版本眾多,使用常規表示式時會發生樣式 (pattern) 不相容或無法使用的情形,這只能透過查詢該語言或工具的使用手冊才能知道實際可用的樣式為何。由於 Perl 5 在文字處理上的歷史地位,在實務上,常常會以 Perl 5 的常規表式做為實質的 (de facto) 標準,像是 Python、Ruby、PCRE (C 函式庫) 等都是以 Perl 5 的常規表示式為基準。

使用常規表示式

常規表示式常見的功能有四個面向:

  • 比對:確認字串是否符合特定樣式
  • 抓取:從字串中抓取符合樣式的子字串
  • 取代:從字串中將符合樣式的部分取代成另一個字串
  • 切開:依據樣式將字串切開

以下例字比對字串是否符合樣式:

"perl" ~~ m/ perl / or die "Unmatched";

可以將 / (slash) 取代為其他成對分隔符號,需要在樣式中寫 / 時較方便:

"perl" ~~ m{ perl } or die "Unmatched";

也可以在建立樣版物件後,再進行比對:

my $pattern = rx/ perl /;
"perl" ~~ $pattern or die "Unmatched";

若比對成功,可以用內建變數 $/ 取得比對後的結果:

if 'abcdef' ~~ m/ de / {
    ~$/ eq "de" or die "Wrong string";
    $/.prematch eq "abc" or die "Wrong string";
    $/.postmatch eq "f" or die "Wrong string";
    $/.from == 3 or die "Wrong location";
    $/.to == 5 or die "Wrong location";
};

撰寫樣板

註:本節不會列出所有的規則,請自行至 Perl 6 官網查詢相關規則。

使用 regexes 時常會失敗,往往都是樣板寫錯的緣故。比較好的方式,是將樣板相關的程式碼獨立出來測試,確定樣板正確後再繼續撰寫程式。

對於 regexes 的初學者來說,時常覺得 regexes 的樣板像天書般難以閱讀。其實,regexes 由四個簡單的概念組合:

  • 單一字元 (single character)
  • 組合 (composition)
  • 重覆 (repeation)
  • 替代 (alternation)

再加上幾個輔助的特性:

  • 定位點 (anchors)
  • 群組 (grouping)
  • 旗標 (flags)
  • 斷言 (assertion)

這樣就可以開始寫樣板了。

單一字元

對於普通字元,如 a 來說,單一的字元即代表比對該字元。但是有些字元是 meta-characters,在常規表示式中有特殊意義,這也就是我們需要學習的部分。一些實例如下:

  • 預設字元集,像 \w 表示所有的英文字母
  • Unicode 字元集,像 <:Lu> 表示 Unicode 字母大寫
  • 自定字元集,像 [abc] 表示 abc 其中之一,而 [^abc] 則表示 abc 三者以外的所有字元
  • . (dot) 表示任意單一字元

在這些字元中,Unicode 字元集還有額外的集合操作,像是 <:Ll+:N> 表示所有的 Unicode 小寫字母和數字的集合。

組合

將單一字元連續排列可組合出較長的字串,像是 [fq]oo 表示 fooqoo 等。

重覆

透過 quantifier,可指定重覆某個字元多次。一些實例如下:

  • *:重覆零到多次 (greedy)
  • +:重覆一到多次 (greedy)
  • ?:重覆零到一次
  • *?:重覆零到多次,儘可能取較少次 (non-greedy)
  • +?:重覆一到多次,儘可能取較少次 (non-greedy)
  • ** 2..5:重覆二至五次

以下是 greedy 和 non-greedy 比對的差異:

# Greedy
'abababa' ~~ /a .* a/;
~$/ eq "abababa" or die "Wrong pattern";

# Non-greedy
'abababa' ~~ /a .*? a/;
~$/ eq "aba" or die "Wrong pattern";

定位點

定位點本身不代表字元,但可限制樣式在特定的位置內,實例如下:

  • ^ 字串開頭和 $ 字串結尾
  • ^^ 行開頭和 $$ 行結尾
  • |W 字的界限和 !|w 非字的界限
  • << 字前和 >> 字後
"seafood" ~~ / foo / or die "Matched";
"seafood" ~~ / << foo >> / and die "Unmatched";

替代

利用 || 可在兩個樣式中擇一,如下例:

"bar" ~~ m/ foo || bar / or die "Unmatched";

也可以僅選取子樣式:

"qoo" ~~ m/ (f || q)oo / or die "Unmatched";

群組

透過成對的 () 可以抓出我們想要的子字串,如下例:

"192.168.0.1" ~~ m/(\d+)\.(\d+)\.(\d+)\.(\d+)/ or die "Unmatched";
$/.elems == 4 or die "Wrong group count";
$/[0] == 192 or die "Wrong number";
$/[1] == 168 or die "Wrong number";
$/[2] == 0 or die "Wrong number";
$/[3] == 1 or die "Wrong number";

如果想將特定樣式組成群組,但不想抓取該群組的字串,可用成對的 [] 將樣式包住,如下例:

'abc' ~~ m/ [a||b] (c) /;
$/[0] eq "c" or die "Wrong string";

旗標

旗標 (flags) 可改變樣式比對的行為,在 Perl 6 稱為 adverb,一些例子如下:

  • :i:ignorecase:忽略大小寫
  • :g:global:比對多次,不重疊
  • :p:pos:指定比對位置
  • :c:continue:指定比對起始位置
  • :ov:overlap:重疊比對字串
  • :ex:exhaustive:找出所有可能的結果
  • :s:sigspace:將空白計入常規表示式
  • :r:ratchet:不要回溯
  • :P5:Perl5:使用 Perl 5 的常規表示式語法 (不建議使用)

實例如下:

"abc" ~~ m:i/ ABC / or die "Unmatched";

斷言

斷言可限定樣式一定要在某個樣式之前或之後,範例如下:

  • 正向斷言
    • <?before X>:向前看 (lookahead)
    • <?after X>:向後前 (lookbehind)
  • 負向斷言
    • <!before X>:向前看
    • <!after X>:向後看

實例如下:

'Etiquette' ~~ / (.*?) <?after 'qu'> (e .*) /;
$/[1] eq "ette" or die "Wrong string";

取代

除了前述的比對外,取代 (substitution) 也是常規表示式的一個功能,如下例:

my $s = "food";
$s ~~ s/ foo /see/;
$s eq "seed" or die "Wrong string";

切開

切開 (split) 則是另一個使用常規表示式的方式,如下例:

my @s = "foo,bar,baz".split(/\, || \t/);

@s.elems == 3 or die "Wrong string";
@s[0] eq "foo" or die "Wrong string";
@s[1] eq "bar" or die "Wrong string";
@s[2] eq "baz" or die "Wrong string";

增加可讀性

Perl 6 的樣式可以像程式碼般撰寫,如下例:

my regex float { <[+-]>?\d*'.'\d+[e<[+-]>?\d+]? }

由於 Perl 6 的樣式在預設情形下不會將空白納入樣式,可將上例改寫如下:

my regex float {
     <[+-]>?        # optional sign 
     \d*            # leading digits, optional 
     '.'
     \d+
     [              # optional exponent 
        e <[+-]>?  \d+
     ]?
}

對於較長的樣式,這樣可增加可讀性。

comments powered by Disqus