轉(zhuǎn)帖|其它|編輯:郝浩|2011-03-14 11:51:31.000|閱讀 1951 次
概述:
關(guān)于正則表達(dá)式的文檔很多,但大部分都是英文的,即便有中文的文檔,也翻譯或改編自英文文檔。在介紹功能時(shí),這樣做沒有大問題,但真要處理文本,就 可能會(huì)遇到一些英文開發(fā)或應(yīng)用環(huán)境中難得見到的問題。比如中文之類多字節(jié)字符的匹配,就是如此。所以,這篇文章專門談?wù)務(wù)齽t表達(dá)式如何處理多字節(jié)字符,更 準(zhǔn)確地說,是如何處理Unicode編碼的文本(為什么只提到Unicode編碼,而沒有提到其它編碼,理由在后面詳述)。
# 界面/圖表報(bào)表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
關(guān)于正則表達(dá)式的文檔很多,但大部分都是英文的,即便有中文的文檔,也翻譯或改編自英文文檔。在介紹功能時(shí),這樣做沒有大問題,但真要處理文本,就 可能會(huì)遇到一些英文開發(fā)或應(yīng)用環(huán)境中難得見到的問題。比如中文之類多字節(jié)字符的匹配,就是如此。所以,這篇文章專門談?wù)務(wù)齽t表達(dá)式如何處理多字節(jié)字符,更 準(zhǔn)確地說,是如何處理Unicode編碼的文本(為什么只提到Unicode編碼,而沒有提到其它編碼,理由在后面詳述)。
首先介紹關(guān)于編碼的基礎(chǔ)知識(shí):
通常來說,英文編碼較為統(tǒng)一,往往采用ascii編碼或兼容ascii的編碼(即編碼表的前127位與ascii編碼一致,常用的各種編碼,包 括Unicode編碼都是如此)。也就是說,英文字母、阿拉伯?dāng)?shù)字和英文的各種符號(hào),在不同編碼下的表示是一樣的,比如字母A,其編碼總是41,常見的編 碼中,英文字符和半角標(biāo)點(diǎn)符號(hào)的編碼都等于ascii編碼,通常只用一個(gè)字節(jié)表示。
但是中文的情況則不同,常見的中文編碼有GBK(CP936)和Unicode兩種,同一個(gè)中文字符在不同編碼下的值并不相同,比如“發(fā)” 字,GBK編碼的值為b7 a2,用兩個(gè)字節(jié)表示;而Unicode編碼的值(也就是代碼點(diǎn),Code Point)為53 d1。如果用UTF-8編碼保存,需要3個(gè)字節(jié)(e5 8f 91);用UTF-16編碼保存,需要4個(gè)字節(jié)(fe ff 53 d1)。
正因?yàn)橹形淖址枰鄠€(gè)字節(jié)來表示,常見的正則表達(dá)式的文檔就有可能無法覆蓋這種情況。比如常見的資料都說,點(diǎn)號(hào)『.』可以匹配“除換行符\n之外的任意字符”,但這可能只適用于“單字節(jié)字符”,因?yàn)辄c(diǎn)號(hào)匹配的其實(shí)只是“除換行符\n之外的任意字節(jié)”而已。不信,我們可以來試試看(以下例子中,程序均使用UTF-8編碼):
Python 2.x
>>> re.search('^.$', '發(fā)') == None # True
PHP 4.x/5.x
preg_match('/^.$/', '發(fā)') // 0
Ruby 1.8
irb(main):001:0> '發(fā)' =~ /^.$/ # nil
之所以會(huì)出現(xiàn)這種情況,是因?yàn)檎齽t表達(dá)式無法正確將多個(gè)字節(jié)識(shí)別為“單個(gè)字符”,讓點(diǎn)號(hào)『.』能正確匹配。不過在Python 3.x、Java、.NET和Ruby 1.9中,字符串默認(rèn)都是采用Unicode編碼,所以不存在上面的問題。如果你使用的是Python 2.x、Ruby 1.8或PHP,也可以顯式指定采用Unicode模式。
Python 2.x
>>> re.search('^.$', u'發(fā)') == None #False
PHP 4.x/5.x
preg_match('/^.$/u', '發(fā)') // 1
Ruby 1.8
irb(main):001:0> '發(fā)' =~ /^.$/u # 0
如果你細(xì)心就會(huì)發(fā)現(xiàn),在Python 2.x中,我們指定的字符串使用Unicode編碼,而文檔里說了,正則表達(dá)式也可以指定Unicode模式的;相反,在PHP和Ruby中,我們指定正則表達(dá)式使用Unicode編碼,而字符串并沒有指定。這到底是怎么回事呢?
我們知道,正則表達(dá)式的操作可以簡(jiǎn)要概括為“用正則表達(dá)式去匹配字符串”,它涉及兩個(gè)對(duì)象:正則表達(dá)式和字符串。對(duì)字符串來說,如果沒有設(shè)定 Unicode模式,則多字節(jié)字符很可能會(huì)拆開為多個(gè)單字節(jié)字符對(duì)待(雖然它們并不是合法的ascii字符),Python 2.x中就是如此,“發(fā)”字在沒有設(shè)定Unicode編碼時(shí),變成了3個(gè)單字節(jié)字符構(gòu)成的字符串,點(diǎn)號(hào)『.』只能匹配其中的單個(gè)“字符”。如果顯式將正則 表達(dá)式設(shè)定為Unicode字符串(也就是在 u'發(fā)' ),則“發(fā)”字視為單個(gè)字符,點(diǎn)號(hào)可以匹配。
而且,如果你在正則表達(dá)式的字符組里使用了中文字符,表示正則表達(dá)式的字符串,也應(yīng)該設(shè)定為Unicode字符串,否則正則表達(dá)式會(huì)認(rèn)為字符組里不是單個(gè)字符,而是3個(gè)單字節(jié)字符:
Python 2.x
>>> re.search('^[我]$', u'我') == None # True
>>> re.search(u'^[我]$', u'我') == None # False
另一方面,在PHP和Ruby中并不存在“Unicode字符串”,所以我們無法修改字符串的屬性。但是,設(shè)定正則表達(dá)式為Unicode模 式,正則表達(dá)式也可以正確識(shí)別字符串中的Unicode字符。所以,如果你用PHP或Ruby的正則表達(dá)式處理Unicode字符串,一定不要忘記指定 Unicode模式。
點(diǎn)號(hào)『.』對(duì)Unicode字符的匹配“我”(采用UTF-8編碼)。
字符串 |
正則表達(dá)式 |
語言 |
是否顯式指定Unicode模式 |
可否匹配 |
我 |
^.$ |
Java |
否(無須指定) |
可以 |
^.$ |
JavaScript |
否(無法指定) |
由瀏覽器的實(shí)現(xiàn)決定 |
|
/^.$/ |
PHP |
否 |
不可以 |
|
/^.$/u |
PHP |
是 |
可以 |
|
/^.$/ |
Ruby 1.8 |
否 |
不可以 |
|
/^.$/u |
Ruby 1.8 |
是 |
可以 |
|
/^.$/ |
Ruby 1.9 |
否 |
可以 |
|
^.$ |
.NET |
否 |
可以 |
|
^.$ |
Python 2.x |
否 |
不可以 |
|
^.$ |
Python 3 |
否 |
可以 |
注:PHP和Ruby的正則表達(dá)式本身是不包含分隔符(分隔符可以有很多種,常見的是反斜線/)的,但PHP指定Unicode模式必須在后一個(gè)分隔符之后寫u,所以在這里將分隔符也寫出來。
不過,如果你熟悉Python語言,會(huì)發(fā)現(xiàn)Python也可以指定正則表達(dá)式使用Unicode模式,這又是怎么回事呢?
不妨回頭仔細(xì)想想你讀過的文檔,正則表達(dá)式中的『\d』和『\w』,都是如何解釋的?或許你的第一反應(yīng)是:『\d』等價(jià)于『[0-9]』,『\w』等價(jià)于『[0-9a-zA-Z_]』。因?yàn)橛行┪臋n說明了這種等價(jià)關(guān)系,有些文檔卻說:『\d』匹配數(shù)字字符,『\w』匹配單詞字符。 然而這只是針對(duì)ascii編碼的規(guī)定,在Unicode編碼中,全角數(shù)字0、1、2之類,應(yīng)該也可以算“數(shù)字字符”,由『\d』匹配;中文的字符,應(yīng)該也 可以算“單詞字符”,由『\w』匹配;同樣的道理,中文的全角空格,應(yīng)該也可以算作“空白字符”,由『\s』匹配。所以,如果你在Python中指定了正 則表達(dá)式使用,『\d』、『\w』、『\s』就能匹配全角數(shù)字、中文字符、全角空格。
Python 2.x(字符均為全角)
>>> re.search('(?u)^\d$', u'1') == None # True
>>> re.search('(?u)^\w$', u'發(fā)') == None # True
>>> re.search('(?u)^\s', u' ') == None # True
老實(shí)說,這樣的規(guī)定有時(shí)候確實(shí)讓人抓狂,假設(shè)你希望用正則表達(dá)式『\d{6,12}』來驗(yàn)證一個(gè)長(zhǎng)度在6到12之間的數(shù)字字符串,卻沒留意『\d』能匹配全角數(shù)字,驗(yàn)證就不夠嚴(yán)密了。
下面的表格列出了常見語言中的匹配規(guī)定:
語言 |
『\w』『\d』『\s』的匹配規(guī)則 |
Java |
均只能匹配ascii字符 |
JavaScript |
均只能匹配ascii字符 |
PHP |
均只能匹配ascii字符 |
Ruby 1.8 |
默認(rèn)情況下只能匹配ascii字符,Unicode模式只影響『\w』的匹配 |
Ruby 1.9 |
均可以識(shí)別Unicode字符 |
.NET |
均可以識(shí)別Unicode字符 |
Python 2.x |
默認(rèn)情況下只能匹配ascii字符,Unicode模式下均可以識(shí)別Unicode字符 |
Python 3 |
默認(rèn)情況下均可以識(shí)別Unicode字符,但可以顯式指定ascii |
注1:一般來說,單詞邊界『\b』能匹配的位置是:一端是『\w』,一端不是『\w』(也可以什么都沒有),其中『\w』的規(guī)定與『\w』一樣,但Java中則不是這樣,細(xì)節(jié)比較復(fù)雜,這里不展開,有興趣的讀者可以自己試驗(yàn)。
注2:在Python 3中可以在表達(dá)式之前添加『(?a)』指定ascii模式。
雖然常見的中文字符編碼有GBK和Unicode兩種,但如果需要使用正則表達(dá)式處理中文,我強(qiáng)烈推薦使用Unicode字符,不僅是因?yàn)檎齽t 表達(dá)式提供了對(duì)Unicode的現(xiàn)成支持,而且因?yàn)镚BK編碼可能會(huì)有其它問題。比如:我們要求匹配“收”字或者“發(fā)”字,很自然會(huì)想到使用字符組『[收 發(fā)]』,這思路是對(duì)的,但如果采用GBK編碼,正則引擎見到的很可能不是“兩個(gè)字符構(gòu)成的字符組”,而是“四個(gè)字節(jié)構(gòu)成的字符組”。
使用GBK編碼,[收發(fā)]的解釋『ca d5 b7 a2』
如果我們用『[收發(fā)]』來匹配字符“罰”(它的GBK編碼是b7 a3),就會(huì)產(chǎn)生錯(cuò)誤——雖然“罰”字既不等于“收”也不等于“發(fā)”,但“罰”和『[收發(fā)]』卻可以匹配一個(gè)字節(jié)。
GBK編碼的情況:
罰 b7 a3
[收發(fā)] ca d5 b7 a2
Unicode編碼的情況(因?yàn)閁nicode編碼能正確識(shí)別,無論采用UTF-8還是UTF-16,Unicode字符都會(huì)正確轉(zhuǎn)化為Unicode編碼點(diǎn))
罰 7f5a
[收發(fā)] 6536 53d1
“罰”的Unicode編碼是7f5a,無論如何也不會(huì)發(fā)生錯(cuò)誤匹配。
如果出于某些限制,只能使用GBK編碼,也有一個(gè)偏方準(zhǔn)確保證『[收發(fā)]』的匹配,就是把字符組『[收發(fā)]』改成多選分支『(收|發(fā))』。此時(shí)如果要匹配成功,只能是兩個(gè)連續(xù)的字節(jié)ca d5或者b7 a2,而“罰”字兩個(gè)字節(jié)為b7 a3,無法匹配。
但這樣也會(huì)有問題,因?yàn)樵贕BK編碼下字符串被當(dāng)作“字節(jié)序列”來對(duì)待。比如字符串 “賬珍”對(duì)應(yīng)四個(gè)字節(jié),d5 ca d5 e4,其中正好出現(xiàn)了“收”字對(duì)應(yīng)的兩個(gè)字節(jié)ca d5,正則表達(dá)式就可能在此處匹配成功。
更重要的問題在于排除型字符組的匹配,仍然使用上面的例子,假如我們希望匹配一個(gè)“收”和“罰”之外的字符,自然的思路就是使用排除型字符組『[^收發(fā)]』。但是通過上面的講解,我們已經(jīng)知道,這樣“排除”的并不是2個(gè)字符,而是4個(gè)字節(jié):ca d5 b7 a2。但“罰”字的GBK編碼為b7 a3,b7這個(gè)字節(jié)被“排除”了,所以正則表達(dá)式會(huì)顯示“罰”字不能由『[^收發(fā)]』匹配,這完全違背了我們的本意。
總的來說,所以如果使用GBK編碼(或者說非Unicode編碼),對(duì)此類問題基本是無解的。因此,根本的辦法還是使用Unicode編碼。
推薦使用Unicode的另一個(gè)理由是,使用Unicode,我們可以指定字符的代碼點(diǎn)范圍,實(shí)現(xiàn)“匹配一個(gè)中文字符”的表達(dá)式。因?yàn)檎齽t引擎 能正確識(shí)別的多字節(jié)字符一般只有Unicode字符,所以即便我們知道GBK編碼中中文字符的編碼范圍,也無法指定一個(gè)字符組來匹配其中的字符,而 Unicode編碼則可以。
在Unicode編碼表里,代碼點(diǎn)4e00-9fff歸類為“CJK 統(tǒng)一表意符號(hào)”CJK Unified Ideographs(參見//en.wikipedia.org/wiki/CJK_Unified_Ideographs),涵蓋了絕大多 數(shù)中文字符,我們可以某個(gè)字符組里匹配此范圍中任何一個(gè)代碼點(diǎn),但是在不同語言中指定這個(gè)范圍的辦法并不同。作為本文的結(jié)尾,在這里列出各種語言中能匹配 “中文字符”的字符組:
語言 |
表示法 |
注釋 |
Java |
[\u4e00-\u9fff] |
|
JavaScript |
[\u4e00-\u9fff] |
所操作字符串必須是Unicode編碼 |
PHP |
[\x{4e00}-\x{9fff}] |
必須指定Unicode模式 |
Ruby |
[\u{4e00}-\u{9fff}] |
Ruby 1.8不支持這種記法,Ruby 1.9中必須顯式指定Unicode模式 |
.Net |
[\u4e00-\u9fff] |
|
Python |
[\u4e00-\u9fff] |
Python 2.x中必須同時(shí)指定字符串和正則表達(dá)式都使用Unicode字符串;Python 3中則不用 |
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請(qǐng)務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請(qǐng)郵件反饋至chenjj@ke049m.cn
文章轉(zhuǎn)載自:網(wǎng)絡(luò)轉(zhuǎn)載