C++11 對於長年埋藏在國內大專體系計算機程式課程血液中的 C++03 算是新增了許多令人耳目一新的功能,許多改動我認為是讓程式碼本身具有了語義 (如 std::move,std::forward,,有些改動是讓程式碼變得較簡潔易讀 (例如 auto),而有一大部份是功能性函式庫的加強,這篇要喇的 regex 就是其中之一。這也是本系列第一篇,希望之後能繼續寫下去 lol

預備知識

本篇假設大家都知道 regular expression 都是幹啥用的了。並且我假設大家都知道要 compile 有大部份完備 C++11 語法的 code 需要 gcc 4.8 (clang 要幾版我忘了 ㄏㄏ),然後參數要下 -std=c++11。當然如果你裝的是 gcc 6 那預設就是 -std=c++14 皆大歡喜。時代在進步,不要執著於 gcc 4.6 這種可怕的版本了 (後面會講這版可怕在哪 ?)。

regex 的表示方式

要做事情需要引入標頭檔,在這邊要用的叫做 #include <regex>。和大多數 STL 一樣,regex 是在 std 這個 namespace 底下。然後就是宣告一個 regex 物件出來了。十分簡單,就長得像下面這樣

regex_declare.cpp
#include <regex>

int main()
{
  std::regex reg1("\\s\\w+");
  std::regex reg2("[0-9]{3}\\w+", std::regex_constants::icase);
}

不汙辱大家的智商,第一個參數的重點只在 escaping 的部份。要表示 regexp 中的 \d \S 這些東西需要兩個反斜線。 Ctor 放的第二個參數代表這個 regexp 的設定 (http://en.cppreference.com/w/cpp/regex/syntax_option_type)。例如,我這邊打的 std::regex_constants::icase 代表不分大小寫的匹配。

匹配你要的字串 (regex_search, regex_match)

看個例子我相信大家就會懂了:

regex_search_match.cpp
#include <regex>
#include <string>
#include <iostream>

int main()
{
  std::string article[] = {
    "T1234 689 8+9=17!!",
    "9.2 i55s T515xxyz XI998",
    "APTX4869",
    "C8763"
  };
  std::regex reg("[A-Z]\\d+");
  std::smatch m;
  std::ssub_match sm;
  
  for(auto &line : article) {
    if(regex_search(line, m, reg)) {
      std::cout << "regex_search: " << std::endl;
      for(auto &match: m) {
        sm = match;
        std::cout << sm.str() << std::endl;
      }
    }
    
    if(regex_match(line, m, reg)) {
      std::cout << "regex_match: " << std::endl;
      for(auto &match: m) {
        sm = match;
        std::cout << sm.str() << std::endl;
      }
    }
  }
}

結果:

regex_search:
T1234
regex_search:
T515
regex_search:
X4869
regex_search:
C8763
regex_match:
C8763

在一段文字中匹配你要的部份有兩個方法:regex_search, regex_match,兩者的差距在於前者是在文字之中匹配,後者需要整段文字完美匹配你的 regexp。程式碼中的 smatch 物件是用來存放匹配結果的集合,裡面放著一堆 ssub_match,代表著每個「子匹配」(就是你用 regexp 中用 () group 到的東西,第一個 group 就是 m[1];第二個是 m[2];比較特別的是 m[0] 代表整個 match)。
所以從上面我們可以看到只有 C8763 有出現在 regex_match 的結果中。但第二行的 I998 人呢?這就是 C++11 的 regex 比起 javascript 不同規範的地方了。regex_search 只會匹配文字中的第一個符合處。要拿到文字中所有的匹配有兩種方法:利用 ssub_match 中記錄的位置資訊 (比較不漂亮點) 或 std::sregex_iterator (比較潮的做法)。

深入一點看 ssub_match

一樣先來個例子:

ssub_match.cpp
#include <regex>
#include <string>
#include <iostream>
#include <iterator>

int main()
{
  std::string article[] = {
    "8+9=17!! 1+2=3",
    "48*9=432",
  };
  std::regex reg("(\\d+)[+\\-*/](\\d+)=(\\d+)");
  std::smatch m;
  std::ssub_match sm ;
  
  for(auto &line : article) {
    if(regex_search(line, m, reg)) {
      std::cout << "Numbers in the equation " << line << " " << std::endl;
      auto start_it = m[0].first; 
      for(size_t i=1; i<m.size(); i++) {
        sm = m[i];
        std::cout << " " << sm.str() <<  // 等價於 m.str(i)
          " [" << *sm.first << ", " << *sm.second << "], " << 
          " [" << std::distance(start_it, sm.first) << ", " << std::distance(start_it, sm.second) << "]" << "\n"; // 起點位置等價於 m.position(i)
      }
      std::cout << '\n';
    }
  }
}


結果會是

Numbers in the equation 8+9=17!! 1+2=3
 8 [8, +],  [0, 1]
 9 [9, =],  [2, 3]
 17 [1, !],  [4, 6]

Numbers in the equation 48*9=432
 48 [4, *],  [0, 2]
 9 [9, =],  [3, 4]
 432 [4, ],  [5, 8]
 

一樣,第一行中的 1+2=3 沒有在匹配結果中。不過我們先可以來看一下新代入的成員,也就是 ssub_matchfirst 還有 second 成員。cppreference 提到 ssub_match 其實是 sub_match 的其中一個特化 (template specialization),其中特化代入的型態是 std::string::const_iterator,並且繼承了 std::pair。因此他一樣有 pairfirst 還有 second,代表著這個子匹配的起點和終點。而且既然他是個 string 的 iterator,你就可以對他 dereference,也就是前面加 *拿到該位置的字元,也可以利用 std::distance 來得到他和起點的位置差,從上面的結果可以看到:sub_match 其實代表的是匹配的 [起點,終點+1]。
講到這裡一個顯而易見的方法拿到文字中所有匹配的方法應該就出現了:每次匹配完之後,利用第 0 個 sub_match 的 second 以後的文字再做一次 regex_search 不就可以拿到所有的匹配了嗎?不過聰明的 STL 已經幫我們做好這件事了,就是直接拿匹配結果 (smatch) 的 suffix() 就可以了。

修改過後代碼如下:

ssub_match_in_deep.cpp
#include <regex>
#include <string>
#include <iostream>
#include <iterator>

int main()
{
  std::string article[] = {
    "8+9=17!! 1+2=3 !!!444+5=9999",
    "48*9=432 QAQ obov 876+00=35",
  };
  std::regex reg("(\\d+)[+\\-*/](\\d+)=(\\d+)");
  std::smatch m;
  std::ssub_match sm ;
  
  for(auto line : article) {
    while(regex_search(line, m, reg)) {
      std::cout << "Numbers in the equation " << line << " " << std::endl;
      auto start_it = m[0].first; 
      for(size_t i=1; i<m.size(); i++) {
        sm = m[i];
        std::cout << " " << sm.str() <<  
          " [" << *sm.first << ", " << *sm.second << "], " << 
          " [" << std::distance(start_it, sm.first) << ", " << std::distance(start_it, sm.second) << "]" << 
          "\n";
      }
      line = m.suffix().str(); // 重點在這兒
    }
    std::cout << '\n';
  }
}

比較潮得到所有匹配的方法: regex_iterator

針對字串的匹配可以使用 sregex_iterator,直接看範例吧

regex_iterator.cpp
#include <iostream>
#include <regex>
#include <string>

int main()
{
  std::string article[] = {
    "b123456789@xxx.yyy.zz uiuiuiui@facebook.com blahblah fa@gmail.com",
    "yooouser@domain.name osososos@cs.com"
  };

  std::regex email_reg("([\\w\\d]+)@(\\S+)");
  for(auto &line : article) {
    auto result_start = std::sregex_iterator(line.begin(), line.end(), email_reg);
    auto result_end   = std::sregex_iterator();
    for(auto it = result_start; it!= result_end; ++it) {
      std::smatch match = *it;
      std::cout << match.str() << std::endl;
      std::cout << "User name: " << match.str(1) << ". Domain: " << match.str(2) << std::endl;
    }
  }
}

執行結果:

b123456789@xxx.yyy.zz
User name: b123456789. Domain: xxx.yyy.zz
uiuiuiui@facebook.com
User name: uiuiuiui. Domain: facebook.com
fa@gmail.com
User name: fa. Domain: gmail.com
yooouser@domain.name
User name: yooouser. Domain: domain.name
osososos@cs.com
User name: osososos. Domain: cs.com

sregex_iterator 就是個指向 smatch 的 iterator。對他 dereference 就可以拿到一個 smatch,所以從某個 sregex_iterator 開始遍歷到相對應於 STL container 的 iterator end() 就可以拿到文章中所有的 smatch。建造相對應的 begin() iterator 可直接利用 sregex_iterator 的 ctor -- 傳入欲匹配的文章 begin 和 end,和要抓的 regex,就這麼簡單粗爆。end() 更是直白簡明,sregex_iterator 什麼參數都不代,就行啦。

最後要注意的事情是,如果很不幸的你的 compiler 是 gcc 4.6 年代的東西,你可以順利的 compile 上面這份 code 成 object file,但是最後 link 的時候就會發現什麼東西都 undefined reference 喔 ^___^。因為舊的 gcc 只有把 header file 中的 regexiterator 宣告寫好,實作通通沒作唷~。

比較多功能一點的: regex_token_iterator

這個東西的 begin 和 end 用法和 sregex_iterator 基本類似,差別在於 ctor 可以傳第四個參數來代表說我想拿哪個 (或哪些) sub-matches,比較特別的是可以傳 -1 代表 inverse match 的部份 (也就是沒 match 到的部份啦)。看例子八

regex_token_iterator
#include <regex>
#include <iostream>
#include <string>

int main()
{
  std::string text = "hi hi2 x=3; //comment a=15; b=-5; y=9988";
  std::regex re("(\\w+)=([\\-\\d]+)");
  auto start = std::sregex_token_iterator(text.begin(), text.end(), re, -1);
  auto end = std::sregex_token_iterator();
  std::cout << "Not matching part" << std::endl;
  for(auto it = start; it != end; ++it) {
    std::cout << *it << std::endl;
  }

  std::cout << "Matching part: whole match and sub_match 1" << std::endl;
  start = std::sregex_token_iterator(text.begin(), text.end(), re, {0, 1});
  // 第四個參數可以直接傳 initializer list 拿多個 sub-matches
  for(auto it = start; it != end; ++it) {
    std::cout << *it << std::endl;
  }
}

結果:

Not matching part
hi hi2
; //comment
;
;
Matching part: whole match and sub_match 1
x=3
x
a=15
a
b=-5
b
y=9988
y

取代/替換匹配的部份: regex_replace

不管是 M$ word 這類高大上的編輯器或是簡單有力的 vim 的一定都會有取代這功能。而 C++11 也提供了這個方便的功能方便我們把文章中匹配到的部份替換成別的文字。
老嫗能解的用法如下:

regex_replace(文章, regex 物件, 替換格式);
// 回傳替換後的文章

這個替換格式是有那麼點梗的,梗是什麼呢?就是在 gcc 4.6 一定得傳 std::string 進去才行,傳 const char* 進去會被 compiler 靠盃致死 ^____^。另一方面,在 vim 中可將 regex register 放進去替換後的結果 (\1\2 這些)。在這邊使用的是 $&, $1, $2...;分別代表了原匹配字串、子匹配 1、子匹配 2…。此外,還有兩個特別的 format specifiers

$`
$'

分別對應到前面提到 smatchprefix()suffix(),也就是這個 smatch 之前/之後的文字。

regex_replace.cpp
#include <iostream>
#include <regex>
#include <string>

int main()
{
  std::string article("Resources exquisite set arranging moonlight sem him household had. Months had too ham cousin remove far spirit. She procuring the why performed continual improving.");
  std::regex reg_3w("\\b\\w{3}\\b");
  std::cout << "Original text: " << std::endl << 
    article << std::endl;
  std::cout << std::endl;
  std::cout << "Highlight 3-length word: " << std::endl << 
    regex_replace(article, reg_3w, "[$&]") << std::endl;

  std::cout << std::endl;
  std::regex reg_s_suffix("(\\w+)s\\b");
  std::cout << "Remove and highlight s-suffix: " << std::endl << 
    regex_replace(article, reg_s_suffix, "**$1**") << std::endl;
}

運行結果

Original text:
Resources exquisite set arranging moonlight sem him household had. Months had too ham cousin remove far spirit. She procuring the why performed continual improving.

Highlight 3-length word:
Resources exquisite [set] arranging moonlight [sem] [him] household [had]. Months [had] [too] [ham] cousin remove [far] spirit. [She] procuring [the] [why] performed continual improving.

Remove and highlight s-suffix:
**Resource** exquisite set arranging moonlight sem him household had. **Month** had too ham cousin remove far spirit. She procuring the why performed continual improving.

我們可以看到兩次的替換中,第一次是將長度為 3 的字用 [ ] 包起來。而第二次的替換是將所有 s 結尾的字去掉 s 並前後加上 **。去掉 s 的方法很簡單,就是我們直接在替換的格式中放入第一個子匹配,而不是整個匹配,就這樣輕輕鬆鬆。