收藏 分享(赏)

C++字符串分词.doc

上传人:hyngb9260 文档编号:7470048 上传时间:2019-05-19 格式:DOC 页数:13 大小:118.50KB
下载 相关 举报
C++字符串分词.doc_第1页
第1页 / 共13页
C++字符串分词.doc_第2页
第2页 / 共13页
C++字符串分词.doc_第3页
第3页 / 共13页
C++字符串分词.doc_第4页
第4页 / 共13页
C++字符串分词.doc_第5页
第5页 / 共13页
点击查看更多>>
资源描述

1、C+字符串分词董波QQ:84638372一 简介字符串分词,即按照某一规则,将一个完整的字符串分割为更多的字段。在 C 库当中,strtok/wcstok 提供了类似的功能,C+标准库兼容了C 库。 C+的 stringstream 有类似的功能,boost.string_algorithm 也有提供类似的泛型算法。另外在 boost 当中专门提供了 boost.tokenizer 来做这样的工作,它的实现是对 C+泛型设计的一个不错的诠释,当然,它远没有达到完美的程度。 Matthew Wilson 在它的 stlsoft 中也提供了类似的组件,stlsoft.string_tokenise

2、r。它们各有各自的特点,接下来我们对此做一些探讨和研究。二 C 库C 库中提供了 strtok/wcstok 来实现类似的功能,但是它们具有明显的缺点:1. 不可重入性。这是因为它用内部的静态变量来保存相关状态。如果 C 库实现没有考虑 TLS 的话,则还有竞争条件的问题(更多信息可以参考 Chapter 21: Thread-Local Storage)。2. 参数必须是可写入的。3. 参数必须是 C 风格字符串。4. 总是跳过空白。下面是一个早期字符串函数的例程(改编自 Matthew WilsonExtended STL, Volume 1 Chapter 27 ):#include u

3、sing namespace std;int main() char str = “abc,def;ghi,jkl;“;char* outer = NULL;char* inner = NULL;for( outer = strtok( str, “;“) ; NULL != outer; outer = strtok(NULL, “;“) )printf( “Outer token: %sn“, outer );/for( inner = strtok( outer, “,“); NULL != inner; inner = strtok( NULL, “,“) )/ printf( “In

4、ner token: %sn“, inner );/return 0;如上面的程序,如果解注释那一段代码将导致工作不正常,也不会达到我们想要的目的,输出可能如下:Outer token: abc,defInner token: abcInner token: def请按任意键继续. . .在 Windows 下面,Visual C+ 2005 起提供了新的安全版函数,在一定程度上可以解决这种问题(UNIX 系统下面有 strtok_r 有类似的功能) ,因为它们是可重入的。下面是上面例程的升级版:#include using namespace std;int main() char str

5、= “abc,def;ghi,jkl;“;char* outer = NULL;char* inner = NULL;char* pOut = NULL;char* pIn = NULL;for( outer = strtok_s( str, “;“, NULL != outer; outer = strtok_s(NULL, “;“, for( inner = strtok_s( outer, “,“, NULL != inner; inner = strtok_s( NULL, “,“, return 0;在我的机器上输出如下:szRes: | pTok: szRes: | pTok: s

6、zRes: | pTok: szRes: | pTok: 请按任意键继续. . .但是即便是如此,我们也不能解决它所存在的其它问题,比如刚才提到的 2、3 、4 点。三 C+ stringstream这也是一种可以用来分词的方法,但是实际上用的并不多,而且功能也不够强大,而且很多人都不能很好的掌握 stringstream,因为我们平时用得太少了。#include #include #include using namespace std;int main() stringstream str(“abcd efg kk dd “ );string tok;while( getline( str

7、, tok, ) )cout#include #include using namespace std;#include int main() string ss( “HelloWorld!He.lloWorld!he“ );vector tmp;/ 以标点符号分开!vectorassert( boost:addressof(tmp) = boost:addressof(tt) );copy( tt.begin(),tt.end(),ostream_iterator( cout,“n“ ) );return 0;输出:HelloWorldHelloWorldhe请按任意键继续. . .我们可以

8、看到,我们对整个拆分过程是不可控的,即使在某些时候我们可能只对拆分后的前两个单词感兴趣我们也不得不用容器来获取和保存所有结果,这对于字符串很长的情况那实在是让人不能接受,或许我们应该“按需分配” ,所以便有了迭代器的方法。4.2 迭代器boost:algorithm:split_iterator 可以用来拆分字符串,同时还需要搭配一些 Finder(比如 token_finder)和断言式(或者说判断式) 。当然我们也可以自己 DIY 一个 Finder。下面是一个简单的例程:#include #include using namespace std;#include int main() s

9、tring str(“abc d*dd a“);boost:algorithm:split_iterator iStr( str,boost:algorithm:token_finder( boost:algorithm:is_any_of( “* “ ) ) );boost:algorithm:split_iterator end;while( iStr != end )cout iStr( str,boost:algorithm:token_finder(boost:algorithm:is_any_of( “* “ ),boost:algorithm:token_compress_on

10、) );这个时候将开启压缩,输出可能如下:abcddda请按任意键继续. . .相对于 boost.tokenizer,字符串算法库提供的分词手法要少一些,如果要更多的功能的话我们还是需要自己 DIY 一个 Finder 的。自己 DIY 一个 Finder 并不复杂,我们只需要保证我们的 Finder 拥有与此类似的重载即可:templateiterator_rangeoperator()(ForwardIteratorT Begin,ForwardIteratorT End ) const;至于这个 Finder 内部您要保存什么信息都可以由您自己决定。这和 boost.tokenizer

11、 采用的策略也是类似的,因此它们两个的扩展性都是很强的。本来应该多说一些关于 Boost 字符串算法库的内容的,因为毕竟 Tr2 中有它,但是这不是这个文档的重点。五 boost.tokenizerboost.tokenizer 是一个专门提供的字符串分词库,它本身由视图容器和一些迭代器以及迭代器视图组成。虽然我认为可能随着 Boost 字符串算法库的日趋成熟和强大,这个库可能会被拿掉,但是研究和学习它的一些东西还是有一些价值的。5.1 组件5.1.1 tokenizertokenizer 是一个视图容器,它本身并不包含具体的数据,它存在于 boosttokenizer.hpp 中。templ

12、ate , typename Iterator = std:string:const_iterator,typename Type = std:stringclass tokenizerTokenizerFunc : 一个符合 TokenizerFunc 概念的拆分工具。Iterator : 用于访问每个拆分后数据的迭代器。Type: 用于保存拆分后的数据。5.1.2 token_iteratortoken_iterator 是一个迭代器,它用于访问我们的拆分后数据,它位于 boosttoken_iterator.hpp 中。template class token_iterator: pub

13、lic iterator_facade, Type, typename detail:minimum_category:type:type , const TypeIterator begin_;Iterator end_;bool valid_;Type tok_;它们分别是:分词工具类对象、开始位置、结束位置、有效位以及结果。在 token_iterator 的实现中,这两个函数很重要:void increment()BOOST_ASSERT(valid_);valid_ = f_(begin_,end_,tok_);const Typereturn tok_;他们分别对应了迭代器自增和提

14、领操作。因此我们可以知道提领操作返回的只是一个常引用,并不会有什么太大的开销,而对于自增来说,它的开销取决于拆分工具的实现。更直接的来说就是取决于拆分工具的 operator ()的实现。5.1.3 分词工具类(TokenizerFunc) 的概念模型boost.tokenizer 为我们提供了四个内置的工具类,它们分别是:char_separator、escaped_list_separator、offset_separator 以及char_delimiters_separator。其中 char_delimiters_separator 已经被 deprecated 了,我们应该使用 c

15、har_separator 来代替它。它们的实现都位于boosttoken_functions.hpp 中。在详细介绍这种工具类之前必须描述一下它的模型和概念,因为如果我们要自己 DIY 一个分词工具类的话,那么就需要符合它的规则。首先 TokenizerFunc 在应用中被 tokenizer 和 token_iterator 按值保存,因此它应该是可拷贝构造的,参考源码我们可以发现类似的代码:void assign(Iterator first, Iterator last, const TokenizerFuncf_ = f;因此,TokenizerFunc 应该是可赋值的。由于不存在友

16、元关系,因此这两个函数应该必须是 public 的。参考 tokenizer 和 token_iterator 的实现还可以发现用到它的其它地方,下面是一个摘录:void initialize()if(valid_) return;f_.reset();valid_ = (begin_ != end_)?f_(begin_,end_,tok_):false;因此我们可以推断出 TokenizerFunc 应该具有一个 reset 的成员函数,它的意义应该是保证迭代器可以用于一个新的分析得以进行。另外还应该具有一个 operator ()的重载,这个重载应该具有三个或者更多的参数,并且支持 3

17、个参数的调用(其它的参数有默认值) 。这至少的三个参数是开始位置、结束位置以及保存分析结果的 tok_,这里的 tok_应该是作为引用传递的。并且这个 operator()总是有一个 bool 返回值,如果返回true 则代表分析可以继续;如果返回 false 则对迭代器的有效性产生影响。5.2 工具类解析5.2.1 char_separatorchar_separator 可能是我们最常用到的工具了,让我们先学会如何使用它。例子 1(摘自 boost 文档):#include #include #include using namespace std;#include int main()

18、std:string str = “;Hello|world|-foo-bar;yow;baz|“; typedef boost:tokenizer tokenizer; boost:char_separator sep(“-;|“); tokenizer tokens(str, sep); for (tokenizer:iterator tok_iter = tokens.begin();tok_iter != tokens.end();+tok_iter) std:cout “; std:cout 请按任意键继续. . .它除了分隔符之外其它全部使用默认的参数,这将使得分词过程将遗弃所有的

19、 sep 参数中的字符。但是 char_separator 并不仅仅是作为strtok 的替代物存在的,它比 strtok 强大得多。下面是 char_separator 的数据成员:private:string_type m_kept_delims;string_type m_dropped_delims;bool m_use_ispunct;bool m_use_isspace;empty_token_policy m_empty_tokens;bool m_output_done;char_separator 多了这样的一些概念:遗弃分隔符、保留分隔符以及 empty 开关。遗弃分隔符:

20、我们可以简单的认为它就是不会出现在分割后的任何一个结果里面。保留分隔符:任何一个保留分隔符都将作为一个独立的结果存在。empty 开关:处理是否将 empty 视为一个结果。将刚才的代码做一点点简单的修改:boost:char_separator sep(“-;“, “|“);其它不变,我们将得到输出:请按任意键继续. . .另外如果将输出字符串修改为这样:std:string str = “;Hello|wor ld|-foo-bar;yo w;baz|“;此时将得到输出:请按任意键继续. . .我们可以看到空格并没有被视为一个分隔符。要想将空格视为分隔符需要在 sep 的第一个参数中显式的

21、指定,比如:boost:char_separator sep(“-; “, “|“ );现在来看保留 empty 的情况,代码如下:#include #include #include using namespace std;#include int main() std:string str = “;Hello|wor ld|-foo-bar;yo w;baz|“; typedef boost:tokenizer tokenizer; boost:char_separator sep(“-;“, “|“, boost:keep_empty_tokens ); tokenizer tokens

22、(str, sep); for (tokenizer:iterator tok_iter = tokens.begin();tok_iter != tokens.end();+tok_iter) std:cout “; std:cout Tok;boost:char_separator sep; / 缺省构造 Tok tok(str, sep);for(Tok:iterator tok_iter = tok.begin(); tok_iter != tok.end(); +tok_iter)std:cout “; std:cout 请按任意键继续. . .5.2.2 escaped_list_

23、separator这个组件用于分析和提取 csv 格式的字符串。关于 csv: http:/ boost 文档) :#include #include #include using namespace std;#include using namespace boost;int main() trystring s = “Field 1,“putting quotes around fields, allows commas“,Field 3“; tokenizer tok(s); for(tokenizer :iterator beg=tok.begin(); beg!=tok.end();

24、+beg) cout offset_separator(Iter begin, Iter end, bool wrap_offsets = true,bool return_partial_last = true): offsets_(begin,end), current_offset_(0),wrap_offsets_(wrap_offsets),return_partial_last_(return_partial_last) offset_separator(): offsets_(1,1), current_offset_(),wrap_offsets_(true), return_

25、partial_last_(true) 重要的概念有两个(选自 boost 文档) :wrap_offsets_: 指明当所有偏移量用完后是否回绕到偏移量序列的开头继续。例如字符串 “1225200101012002“ 用偏移量 (2,2,4) 分解,如果 wrap_offsets_ 为 true, 则分解为 12 25 2001 01 01 2002. 如果 wrap_offsets_为 false, 则分解为 12 25 2001,然后就由于偏移量用完而结束。return_partial_last_: 指明当被分解序列在生成当前偏移量所需的字符数之前结束,是否创建一个单词,或是忽略它。例如

26、字符串 “122501“ 用偏移量 (2,2,4) 分解,如果 return_partial_last_ 为 true,则分解为 12 25 01. 如果为 false, 则分解为 12 25,然后就由于序列中只剩下 2 个字符不足 4 个而结束。简单的例程(选自 boost 文档):#include #include #include using namespace std;#include using namespace boost;int main() string s = “12252001“; int offsets = 2,2,4;offset_separator f(offset

27、s, offsets+3); tokenizer tok(s,f); for(tokenizer:iterator beg=tok.begin();beg!=tok.end();+beg) cout inline_InIt _Find_if(_InIt _First, _InIt _Last, _Pr _Pred) / find first satisfying _Pred_DEBUG_RANGE(_First, _Last);_DEBUG_POINTER(_Pred);for (; _First != _Last; +_First)if (_Pred(*_First)break;return

28、 (_First);template inline_InIt find_if(_InIt _First, _InIt _Last, _Pr _Pred) / find first satisfying _Pred_ASSIGN_FROM_BASE(_First,_Find_if(_CHECKED_BASE(_First), _CHECKED_BASE(_Last), _Pred);return (_First);这并没有对字符串搜索做任何优化,同时也无从优化。另外 find 函数对字符串搜索使用:memchr 进行优化。 而 basic_string 的成员函数find 使用的是 char_t

29、raits:find,而这个 find 也是使用:memchr 来进行了优化的,对于宽字符串来说使用: wmemchr 进行优化。除了搜索算法不合理之外还有重复计算的问题。参考char_separator的实现中,在template bool operator()(InputIteratorfor (; next != end +next)assigner:plus_equal(tok,*next);m_output_done = true;很明显,is_dropped 很有可能会遭遇重复计算的问题,或许 is_dropped 效率很高,但是我想再快也比不过访问一个 bool 变量吧?这样的行

30、为在其它地方也可以看到。5.3.2 字符集问题支持多字符集是一个库是否强大的标志之一,因为标准库的 basic_string 提供了 char 和 wchar_t 的实现,那么我们的字符串分词也应该至少支持这两种字符,然而实际上我们发现 boost.tokenizer 做不到。比如下面的代码:explicit escaped_list_separator(Char e = ,Char c = ,Char q = “): escape_(1,e), c_(1,c), quote_(1,q), last_(false) 比如当使用 wchar_t 来具现化的时候,这能通过编译吗?不能。再次参考 c

31、har_separator 的两个私有函数:bool is_kept(Char E) const if (m_kept_delims.length()return m_kept_delims.find(E) != string_type:npos;else if (m_use_ispunct) return std:ispunct(E) != 0; elsereturn false;bool is_dropped(Char E) constif (m_dropped_delims.length()return m_dropped_delims.find(E) != string_type:np

32、os;else if (m_use_isspace) return std:isspace(E) != 0; elsereturn false;注意上面红色的部分,类似这样的操作是不能够写死的,这样无法支持 wchar_t,这应该通过一个 policy 或者 traits 来实现。因此boost.tokenizer 是不能很好的支持宽字符集的,至少库为我们提供的工具类不能很好的支持。相对而言,stlsoft 在这方面处理的就好得多,当然我们也可以通过自己 DIY 一个 Finder 来实现更广泛的字符集支持和更具效率的字符串分词。六 stlsoft:string_tokeniserstlsof

33、t 是 Matthew Wilson 所做的一个程序库,它的网站是: http:/www.stlsoft.org/ 我们可以免费得到它,而且它的实现全部位于头文件中,无需编译。在 Matthew Wilson 的书Extended STL, Volume 1 : Collections and Iterators中对这个字符串分词做了一些介绍,相对而言它是一个很不错的实现。其实现位于 stlsoftstringstring_tokeniser.hpp 中。在运行下面的程序之前,请确保已经安装和配置好了 stlsoft。#include #include #include using names

34、pace std;#include int main()const wstring strRes(L“:abc:def:ghi:jkl:?kk?dd:“);stlsoft:string_tokeniser tokens( strRes, L: );wcout.imbue( locale(“chs“) );copy(tokens.begin(),tokens.end(),ostream_iterator( wcout, L“n“ ) );return 0;输出:abcdefghijkl?kk?dd请按任意键继续. . .七 效率大 PK现在让我们来对上面所提到的一些东东进行测试吧,首先说明一下这

35、个测试是不完整的,并不代表它们每一个在各种不同情况下的表现。设计本来就是一个相互取舍的过程,也许它在这种情况下表现不好,但是到了另外一种情况下它反而是最好的选择,因此,不能以这样的一个简单的测试来彻底肯定或者彻底的否定它们中的任何一个。源码如下:#include #include #include #include #include using namespace std;#define _HAVE_STL_SOFT_ / 如果没有安装 stlsoft请注释这一行即可#ifdef _HAVE_STL_SOFT_#include #endif / #ifdef _HAVE_STL_SOFT_#i

36、nclude #include #include using namespace boost;int main()const string str( “abc*def*eght*kkk*ddd“ );const int ciCount = 20000;typedef std:vector STR_VEC;STR_VEC vec;vec.reserve( 10 );cout(), * ) );vec.resize(10); / 防止崩溃cout iter(str, boost:algorithm:token_finder( bind2nd( equal_to(), * ) ) );boost:a

37、lgorithm:split_iterator end;int index =0;while( iter != end )/ 防止多余的Copy 动作/ vec.push_back( string( boost:begin(*iter), boost:end(*iter) ) );vecindex+.assign( boost:begin(*iter), boost:end(*iter) );+iter;cout tokens( str, boost:char_separator(“*“) );copy( tokens.begin(), tokens.end(), back_insert_it

38、erator( vec ) );#ifdef _HAVE_STL_SOFT_vec.clear();cout tokens( str, * );copy( tokens.begin(), tokens.end(), back_insert_iterator( vec ) );#endif / #ifdef _HAVE_STL_SOFT_return 0;我们对同样的字符串执行同样的拆分操作 2 万次。在 Debug 模式下面有输出:C-strtok_s: 0.42 sboost.string_algorithm_container: 7.56 sboost.string_algorithm_i

39、terator: 5.55 sboost.tokenizer: 2.53 sstlsoft.string_tokeniser: 2.19 s请按任意键继续. . .在 Release 下面有输出:C-strtok_s: 0.04 sboost.string_algorithm_container: 0.14 sboost.string_algorithm_iterator: 0.05 sboost.tokenizer: 0.14 sstlsoft.string_tokeniser: 0.05 s请按任意键继续. . .通过分析我们知道,C 的自然是最快的,因为它不需要多余的 Copy 动作,而

40、其它的都会有这样的操作,同时 C 库提供的字符串函数大多用汇编写的,所以比较快。但是当我们让编译器全速优化之后发现 boost 字符串算法库的迭代器和 stlsoft 的迭代器也是相当快的。最慢的当然是使用容器来存放结果了,因为它始终都会不断的拷贝。在实际应用当中我们应该具体问题具体分析,根据操作的环境选择最适合的处理方式。举个简单的例子,比如我们只对拆分结果的第某个感兴趣,那么很显然 stlsoft 的迭代器就更适合一些,为什么呢?刚才我们看过一些 boost.tokenizer 的源码,知道在迭代器中它保存了一个 tok_作为结果的容器,每次自增都会将结果拷贝到这个 tok_中,也就是说每

41、次自增都会有拷贝发生,而对于 stlsoft 便不是如此,它在迭代器中保存的是两个迭代器所组成的范围,只要当我们提领的时候才会构造字符串:V operator *() constreturn traits_type:create(m_find0, m_find1);很明显这种效率会高于 boost.tokenizer。但是另外一个情况下,比如说我们需要多次提领同一迭代器,那么对于 stlsoft 来说便不合适了,因为每次提领它都会构造,使得最终我们多次构造字符串。而 boost.tokenizer 这时候更适合,因为它传回的是保存在内部的 tok_的常引用:const Typereturn tok_;这也从另外一个角度证明了一个观点:没有绝对的好与不好,只有适合与不适合的问题。本文原创,转帖请著名出处。参考资料已经于文中指出。另外不保证文中所有信息都是正确的,没有人是绝对正确的。如果您有不同的见解我很乐意与您讨论、与您相互分享知识。QQ :84638372Blog: http:/

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 企业管理 > 管理学资料

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报