Boost 的 STL Container 切割工具(中):Tokenizer

| | 0 Comments| 17:59
Categories:

上一篇大概介紹了 C 的 strtok() 這個專門用來切割 C 字串的函式,以及 Boost C Libraries 裡的 String Algorithms Library 的 split() 這個通用性更廣的函式。而接下來這篇要介紹的,則是功能再進一步的切割工具,Boost 的 Tokenizer(官方頁面)。

Tokenizer 基本介紹

Boost 的 tokenizer 本身是一個 template class,要使用的話要加上 boost/tokenizer.hpp 這個 header 檔;而他是以 STL container 的形式來提供一個處理序列的方法,由於是 template 的形式,所以能處理的資料類型不僅限於字串,也可以用在別的 STL container。他本身的定義如下:

template <
  
typename TokenizerFunc = char_delimiters_separator<char>,
  
typename Iterator = std::string::const_iterator,
  
typename Type = std::string
>

class tokenizer

可以看的出來,tokenizer 這個 class 有三個型別參數,分別是 TokenizerFuncIteratorType

  • 第一項的 TokenizerFunc 是用來切割的方法。
    Boost 提供了 char_separatorescaped_list_separatoroffset_separator 以及 char_delimiters_separator 這四種不同方法可以使用;不過在 Boost 的文件中有提到,char_delimiters_separator 是過時的(deprecated)版本,建議改使用 char_separator
    但是 Heresy 覺得比較怪的是,目前 Boost 還是把他設定成為預設值…
  • 第二項的 Iterator 是代表輸入序列的 iterator 型別,是用來讀取傳入、要處理的資料用的。
     
  • 第三項的 Type 則是指定切割出來的結果會是怎樣的形式。

 

Tokenizer 的簡單範例

而由這三者都有提供預設值,所以可以不額外指定而直接使用;而這樣的預設狀態下,他會是針對 std::string 做處理,而處理的方法,則是預設的「char_delimiters_separator<char>」,也就是透過字元來切割字串。

下面的程式碼,就是一個 tokenizer 最簡單的使用範例:

#include <iostream>
#include <string>
#include <boost/tokenizer.hpp>

using namespace std;

int main(){
string str =
"Hello, the beautiful world!";
boost::tokenizer<> tok( str );
for( boost::tokenizer<>::iterator beg=tok.begin(); beg!=tok.end(); beg )
cout << *beg <<
"\n";
}

在這個範例裡,根據要切割的字串 str 建立了一個 tokenizer 的物件 tok,而之後就可以透過 tok 的 iterator,用類似 STL containter 的形式,來存取切割結果的每一項了∼

這樣的程式的執行結果會和之前使用 strtok() 的範例一樣,是:

Hello
the
beautiful
world

在這個範例裡,tokenizer 的三個型別參數都是直接使用預設值,而就連要用來切割的字元,其實也是 char_delimiters_separator<char>  的預設值,而沒有額外指定;在這個狀況下,tokenizer 會是使用空白和標點符號來做切割,所以「,」和「!」都會被當作切割用的字元。而在一般要切割字串的情況下,這樣應該就可以不用多加設定,直接很方便地使用了∼

而如果要自己控制要用那些字元來做分割呢?如果是使用 char_separator 的話,可以寫成:

#include <stdlib.h>
#include <iostream>
#include <string>
#include <boost/tokenizer.hpp>

using namespace std;

int main( int argc, char** argv )
{
string str =
"Hello, the beautiful world!";
boost::char_separator<
char> sep(" ,!");

boost::tokenizer< boost::char_separator<
char> > tokens(str, sep);
boost::tokenizer< boost::char_separator<
char> >::iterator tok_iter;
for( tok_iter = tokens.begin(); tok_iter != tokens.end(); tok_iter)
cout << *tok_iter << endl;
return 0;
}

在這裡,主要的變化在於指定了 tokenizerTokenizerFuncchar_separator<char>;而同時,我們也建立了一個 char_separator<char> 的物件 sep,並在建構 tok 的時候傳進去,當作切割用的方法;而這個切割的方法,則是指定要用「 」、「,」和「!」來做切割。這樣出來的結果會和上面的例子完全一樣,不過差異就在於這樣可以自由地控制要用那些字元來做切割了∼

而接下來,也將針對 char_separatorescaped_list_separatoroffset_separator 這三種預設的切割方法,來做簡單的說明。至於已經過時的 char_delimiters_separator,Heresy 基本上就跳過了。 ^^"

 

char_separator 的細節

char_separator官網說明)基本上類似 strtok(),是用字元來把字串進行切割的;不過實際上由於 tokenizer 整個架構都是採用 template 的形式,所以 char_separator 也可以使用其他型別的資料,不一定局限於 char。除此之外,char_separator 也有提供更多的控制,可以做到一些 strtok() 做不到的事。

他完整的建構子的形式是:

char_separator( const Char* dropped_delims,
               
const Char* kept_delims = 0,
                empty_token_policy empty_tokens = drop_empty_tokens)

可以發現,他用來設定分割用的字元(delimiter)有兩種,一種是用來切割後就丟掉、不顯示的 dropped delimiter(第一個參數、dropped_delims),另一種則是在切割後還是會保留下來的 kept delimiter(第二個參數、kept_delims)。而另外第三個參數,則是設定要不要把切割出來是空的結果給丟掉,而選擇則只有保留和丟掉兩種(keep_empty_tokensdrop_empty_tokens)。

像在上面的例子裡,就是只有設定 dropped delimiter,所以那些用來做切割判斷的字元都不會顯示出來。而如果我們把上面的 sep 稍作修改,改成「boost::char_separator<char> sep(" ", ",!");」的話,那執行的結果則會變成:

Hello
,
the
beautiful
world
!

可以看的出來,本來沒有顯示出來的標點符號(「,」和「!」)由於現在變成是 kept delimiter,所以現在都變成獨立一項顯示出來了∼

而如果再將最後一個參數設定為保留,也就是將 sep 改為「boost::char_separator<char> sep(" ", ",!", boost::keep_empty_tokens);」,並把輸出的部份前後加上「[」、「]」的話,那結果就會變成是:

[Hello]
[,]
[]
[the]
[beautiful]
[world]
[!]
[]

也就是空字串也都會被抓出來了∼

 

escaped_list_separator 的細節

escaped_list_separator官網說明)是設計用來處理 cvs(comma separated value)的格式化 function object。基本上,csv 就是透過逗號「,」和換行來紀錄資料、廣泛地被使用的一種格式,詳細可以參考維基百科。像下面就是一個簡單的範例:

Year,Make,Model,Length
1997,Ford,E350,2.34
2000,Mercury,Cougar,2.38

而 Boost 的 escaped_list_separator 允許使用者可以自訂脫逸字元(escape character,預設是「」)、區隔欄位的字元(預設是「,」),以及引號(預設是「"」)。所以在有了這些設定後,就可以相對簡單地來處理這一類的資料了∼

下面就是一個簡單的範例:

#include <stdlib.h>
#include <iostream>
#include <string>

#include <boost/tokenizer.hpp>

using namespace std;

int main( int argc, char** argv )
{
string s =
"Field 1,"quotes around fields, allows commas",\"escape\"";
boost::tokenizer< boost::escaped_list_separator<
char> > tok(s);
boost::tokenizer< boost::escaped_list_separator<
char> >::iterator beg;
for( beg =tok.begin(); beg != tok.end(); beg )
cout << *beg <<
"\n";
return 0;
}

這樣的執行結果會是:

Field 1
putting quotes around fields, allows commas
"escape"

可以看到,基本上字串會被「,」分成三個欄位,而由於有引號「"」的關係,所以切出來的第二個字串裡的「,」並不會變成切割用的字元。而如果有必要用特殊字元的話,也可以使用脫逸字元「」來做處理。透過這樣的機制,他可以比使用 char_separator 時更有彈性地分析字串,在許多情況下,應該是可以有更好的效果的。

而如果有需要自訂的話,escaped_list_separator 的建構子支援三個參數,依序是拖曳字元、分割字元和引號,形式可以是單一字元或是字串。例如下面的例子,就是稍微修改後的結果:

#include <stdlib.h>
#include <iostream>
#include <string>

#include <boost/tokenizer.hpp>

using namespace std;

int main( int argc, char** argv )
{
string s =
"'Field, 1',"commas,";\"escape/"";
boost::escaped_list_separator<
char> sep( "\/", ";,", ""'" );
boost::tokenizer< boost::escaped_list_separator<
char> > tok(s, sep);
boost::tokenizer< boost::escaped_list_separator<
char> >::iterator beg;
for( beg =tok.begin(); beg != tok.end(); beg )
cout << *beg <<
"\n";
return 0;
}

其中,sep 就是設定脫逸字元是「」或「/」、分隔字元是「;」或「,」,而引號則允許「"」或「’」。

 

offset_separator 的細節

最後一個,則是 offset_separator官網說明)。和前兩者是透過設定字元的方法來作分割不同,offset_separator 是直接指定要切割成多長的段落。 他最簡單的範例,大概就如下:

#include <iostream>
#include <string>

#include <boost/tokenizer.hpp>

using namespace std;

int main( int argc, char** argv )
{
string s =
"12252001";
int offsets[] = {2,2,4};
boost::offset_separator f(offsets, offsets 3);
boost::tokenizer< boost::offset_separator > tok(s,f);
boost::tokenizer< boost::offset_separator >::iterator beg;
for( beg = tok.begin(); beg != tok.end(); beg )
cout << *beg <<
"\n";

return 0;
}

而這樣執行的結果,就是會把這個字串切割成 2-2-4 的形式,也就是:

12
25
2001

這樣的功能在 Heresy 來看,主要應該是針對固定格式的資料(例如電話號碼、日期)來做處理了∼

另外,offset_separator 建構子實際上是要四個參數的,除了要把 offset 的開始和結束位置傳進去外,也可以再透過之後的兩個 bool 變數來做一些細部的調整:

offset_separator( Iter begin, Iter end,

bool wrap_offsets = true,
                 
bool return_partial_last = true)

其中第一個 wrap_offsets 是指定如果當字串按照指定的數值分割完後如果還有剩的話,是否要繼續重新切割;如果是 false 的話,就會停下來不繼續分割。而第二個 return_partial_last 則是指定位數不夠的時候,是否要傳回來。這部分比較單純,有興趣的話可以自己玩看看。

 

關於 Boost 的 Tokenizer 大概就先寫到這了。之後如果還有時間和力氣的話,應該會寫一下怎樣把 Tokenizer 應用在其他型別的資料上,以及如何自己寫一個 TokenizerFunc。不過,都之後再說吧∼


Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *