天天看點

《正規表達式經典執行個體(第2版)》——2.7 Unicode碼位、類别、區塊和字母表

本節書摘來自異步社群《正規表達式經典執行個體(第2版)》一書中的第2章,第2.7節,作者: 【美】jan goyvaerts , steven levithan著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

問題描述

使用一個正規表達式來查找商标符号(™),要求通過指定其unicode碼位,而不是複制并粘貼一個實際上的商标符号。如果你選擇複制并粘貼,那麼商标符号可以被看作另外一個字面字元,即使并不能從鍵盤上直接輸入它。字面字元已經在執行個體2.1中進行了讨論。

建立一個正規表達式來比對unicode“貨币符号”(currency symbol)類别的任意字元。

建立一個正規表達式來比對在unicode“希臘擴充”區塊中的任意字元。

建立一個正規表達式來比對根據unicode标準屬于希臘字母表一部分的任意字元。

建立一個正規表達式來比對一個字形(grapheme),它通常也被當作同一個字元:一個基本字元加上它所有的組合标記。

解決方案

unicode碼位

在python 2.x中,這些正規表達式需要作為一個unicode字元串u"u2122"或u"u00002122"引用。

pcre必須使用utf-8支援進行編譯,在php中,需要使用/u模式修飾符來打開utf-8支援。

ruby 1.8不支援unicode正規表達式。

unicode類别

pcre必須使用utf-8支援進行編譯,在php中,需要使用/u模式修飾符來打開utf-8支援。javascript和python不支援unicode屬性。可以使用xregexp為javascript添加unicode屬性支援。ruby 1.8不支援unicode正規表達式。

unicode 區塊

javascript、pcre、python和ruby 1.9不支援unicode區塊。但它們支援unicode碼位,可以用本例“變體”一節示範的方法比對區塊。xregexp可以為javascript添加unicode區塊支援。

unicode字母表

unicode字母表(script)支援要求pcre 6.5或者更新版本,而且pcre必須使用utf-8支援進行編譯。在php中,需要使用/u模式修飾符來打開utf-8支援。.net、javascript和python不支援unicode屬性。xregexp可以為javascript添加unicode屬性支援。ruby 1.8不支援unicode正規表達式。

unicode字形

pcre和perl都包含一個專門的記号來比對字形pcre。必須使用utf-8支援進行編譯;在php中,需要使用/u模式修飾符來打開utf-8支援。

.net、java、xregexp和ruby 1.9沒有比對字形的記号。但是它們支援unicode類别,可以用來模拟比對字形。

javascript(不使用xregexp時)和python不支援unicode字形。ruby 1.8不支援unicode正規表達式。

讨論

unicode 碼位

碼位(code point)是unicode字元資料庫中的一個條目。碼位與字元是不一樣的,當然這還要基于你給“字元”什麼樣的含義。在unicode中,在螢幕上作為字元出現的符号被稱作是一個字形(grapheme)。

unicode碼位u+2122表示的是“商标符号”字元。根據所使用的正則流派的不同,你可以使用‹u2122›、‹u{2122}›或‹x{2122}›來比對這個字元。

‹u›的文法要求必須使用四位十六進制數字。這意味着你隻能用它來表示u+0000~u+ffff的unicode碼位。

‹u{⋯}›和‹x{⋯}›文法則允許花括号間出現1~6位十六進制數字,這可以支援從u+000000~u+10ffff的所有碼位。你可以使用‹x{e0}›或‹x{00e0}›來比對u+00e0。u+100000之後碼位是很少使用的,字型和作業系統也沒有對它們提供很好的支援。

python的正規表達式引擎不支援unicode碼位。python 2.x中字面unicode字元串和python 3.x中字面文本字元串需要轉義unicode碼位。u0000 ~uffff表示u+0000到u+ffff的unicode碼位,u00000000~u0010ffff表示全部unicode碼位。在u後必須使用8位十六進制數字,無論是否使用了u+10ffff以後的unicode碼位。

在python代碼中使用字面字元串寫死正規表達式時,可以直接使用‹u2122›和‹u00002122›。從檔案或使用者輸入中讀取正則式時,如果把這些讀取或接收到的unicode轉義直接傳遞給re.compile()将不會正常工作。在python 2.x中,可以調用string.decode ('unicode-escape')解碼unicode轉義。在python 3.x中,可以調用string.encode('utf-8'). decode('unicode-escape')。

碼位可以在字元類之内和之外進行使用。

每個unicode碼位都屬于一個單獨的unicode類别(category)。一共存在30個由兩個字母代表的unicode類别,它們被組織為7個單一字母代表的超類。

‹p{l}› 任意語言的任意字母

‹p{m}› 用于與另外一個字元組合使用的字元(重音符号、變音符号、包圍框等)

‹p{z}› 任何種類的空白或不可見的分隔符

‹p{s}› 數學符号、貨币符号、裝飾标志(dingbat)、制表符(box-drawing)等

‹p{n}› 任意字母表中的任意種類的數字字元

‹p{p}› 任意種類的标點字元

‹p{c}› 不可見的控制字元和未使用的碼位

‹p{cs}› 在utf-16編碼中一個替代對的一半

‹p{cn}› 沒有賦予任何字元的碼位

‹p{ll}›比對屬于“小寫字母”類别的單個碼位。‹p{l}›可以被用作‹[p{ll} p{lu}p{lt}p{lm}p{lo}]›的簡寫形式,用來比對在任意“字母”類别中的單個碼位。

‹p›是‹p›的否定版本。‹p{ll}›比對不屬于ll類别的單個碼位。‹p{l}›比對不擁有任何“字母”屬性的單個碼位。這與‹[p{ll}p{lu}p{lt}p{lm}p{lo}]›是不一樣的,後者會比對所有的碼位。‹p{ll}›比對屬于lu類别(以及除了ll之外的所有其他類别)的碼位,而‹p{lu}›會包含ll碼位。把這兩個組合到一個碼位組中就可以把所有可能的碼位都包括進來。

《正規表達式經典執行個體(第2版)》——2.7 Unicode碼位、類别、區塊和字母表

在perl和pcre 6.5及以後版本中,‹p{l&}›可視為‹[p{ll}p{lu]} p{lt}]›的簡寫,比對字母表中所有擁有大小寫變體的字母。

unicode區塊

unicode字元資料庫把所有碼位劃分為不同的區塊。每個區塊由一個連續範圍内的碼位組成。在unicode 6.1版本的标準中碼位u+0000~u+ffff被劃分為156個區塊:

unicode區塊是一個連續範圍之内的碼位。雖然許多區塊都擁有unicode字母表和unicode類别的名稱,但是它們并不是百分之百相對應的。一個區塊的名稱隻是用來說明它的主要用途。

currency(貨币)區塊中并不包含美元和日元符号。由于曆史的原因,這些符号在basiclatin和latin-1supplement區塊中才能找到。但是二者都屬于currency symbol(貨币符号)類别。如果要比對任何貨币符号,那麼應該使用p{sc},而不是p{incurrency}。

大多數區塊中都包含沒有配置設定的碼位,這些都被包括在了類别‹p{cn}›中。其他unicode類别,以及所有的unicode字母表中,都不會包含未配置設定的碼位。

‹p{inblockname}›的文法可以在.net、xregexp和perl中使用,而java使用的則是‹p{isblockname}›的文法。

perl同樣支援is變體形式,但是我們推薦你堅持使用in的文法,這是為了不與unicode字母表發生混淆。對于字母表來說,perl支援‹p{script}›和‹p{isscript}›,但是不支援‹p{inscript}›。

unicode标準規定區塊名稱不區分大小寫,并且忽略空格、連字元和下劃線。遺憾的是,絕大多數正則流派沒有這麼靈活。.net所有版本和java 4都要求符合上表所示大小寫格式。perl 5.8及以後版本和java 5及以後版本允許混用大小寫格式。perl、java和.net均支援使用連字元和不含空格的格式。推薦使用這種形式。本書讨論的正則流派中,隻有xregexp和perl 5.12及以後版本可以靈活處理unicode區塊名稱中的空格、連字元和下劃線。

除了未配置設定的碼位之外,每個unicode碼位都是剛好屬于一個unicode字母表。未配置設定的碼位不屬于任意字母表。在unicode 6.1版本的标準中,到u+ffff之前的已經配置設定的所有碼位被配置設定到了如下72個字母表中:

字母表是由某種人類特定語言書寫系統使用的一組碼位組成。一些字母表,如thai(泰語),對應于單個的人類語言。其他字母表,如latin(拉丁),則會涉及多種語言。有些語言是由多種字母表來組成的。例如,其中并不存在一種日語unicode字母表;事實上,unicode提供了日國文檔中通常會使用到的hiragana(平假名)、katakana(片假名)、han(漢字)和latin(拉丁)字母表。

在上面的清單中列在第一個的common(常見)字母表沒有按照字母順序排列。這種字母表包含了對于許多字母表相同的各種字元,如标點、空白符号以及其他各色符号。

java要求字母表名稱前使用is,如‹p{isyi}›。perl允許is字首,但不強制要求。xregexp、pcre和ruby則不允許is字首。

unicode标準要求字母表名稱不區分大小寫,并且忽略空格、連字元和下劃線。遺憾的是,絕大多數正則流派沒有這麼靈活。本書中介紹的所有支援字母表的流派都支援符合上表大小寫與下劃線格式的字母表名稱。

當使用到組合标志(combining marks)的時候,碼位與字元的差別就展現出來了。unicode碼位u+0061是“拉丁小寫字母a”,而u+00e0是“加了重音符号的拉丁小寫字母a”。通常來說大多數人把二者都稱作字元。

u+0300是“重音組合符号”的組合标志。它隻有在一個字母之後使用才有意義。一個包含unicode碼位u+0061 u+0300的字元串會被顯示為à,這同u+00e0是完全一樣的。組合标志u+0300會被顯示到字元u+0061的頂上。

之是以會出現兩種不同方式來表示一個加重音符号的字元,是因為在許多曆史上的字元集中,把“帶有重音符号的a”編碼為了單個字元。unicode的設計者認為有必要與這些常用的遺留字元集保持一對一的映射,另外unicode新增了把标志和基本字母分開的表示方式,這樣可以使遺留字元集無法支援的任意組合成為可能。

對于一個正規表達式使用者來說,重要的是本書中介紹的所有正則流派操作的都是碼位而不是圖形化的字元。當我們說正規表達式‹.›比對單個字元的時候,實際上的含義是它比對單個的碼位。如果你的目标文本中包含了兩個碼位u+0061 u+0300,在像java這樣的程式設計語言中它可以使用字元串常量"u0061u0300"來表示,那麼一個點号隻能比對碼位u+0061(也就是a),而不會比對重音符号u+0300。使用正規表達式‹..›才可以同時比對二者。

perl和pcre提供了一個特殊的正規表達式記号‹x›,用來比對任意單個的unicode字形。本質上說,它是unicode版本的特殊點号。無論采取哪種編碼方式,‹x›都會在文本àà中找到兩個比對。如果它的編碼是u00e0u0061u0300,那麼第一個比對是u00e0,第二個比對是u0061u0300。比對任何單一unicode碼位的點号,則會分三次分别比對u00e0、u0061和u0300。

将unicode碼位組合視為字形的規則相當複雜1ff。通常來說,要比對一個字形我們需要比對不是組合标志的任意unicode碼位,以及緊跟其後的所有組合标志(如果有的話)。我們也可以在支援unicode的正則流派中使用正則‹(?>p{m}p{m})›代替‹x›記号比對字形。‹p{m}›比對所有不屬于mark(标志)類别的符号。‹p{m}›比對其後所有的标志(如果存在的話)。

我們将兩個正規表達式記号放置在同一個固化分組(atomic group)中,確定‹p{m}›後面的正規表達式記号比對失敗時不會回溯。‹x{2}.›不會比對àà,因為在‹x{2}›比對兩個重音字母後沒有可供點号比對的字元。同理‹(?>p{m}p{m}){2}.›也不會比對àà。但是如果編碼方式為u00e0u0061u0300,則非捕獲型分組‹(?:p{m} p{m}){2}.›可以比對àà。在第二次嘗試比對時,‹p{m}›會比對u0300,而點号則會比對失敗。是以正則式會回溯,使‹p{m}*›交回它所比對的字元,進而使點号成功比對u0300。

javascript的正則引擎不支援固化分組。xregexp也無法實作此特性,因為xregexp仍然依賴javascript的正則引擎實作其模式比對。是以在使用xregexp時,‹(?>p{m}p{m})›是我們最接近的模拟‹x›的實作。沒有固化分組時,需要牢記正則式中‹(?:p{m}p{m})›後面任何可以比對标志(mark)類别字元的記号都可能使‹p{m}*›回溯。

變體

否定變體

大寫形式的‹p›是小寫形式‹p›的否定變體。例如,‹p{sc}›會比對不擁有“currency symbol”(貨币符号)unicode屬性的任意字元。所有支援‹p›的流派都會在其所支援的屬性、區塊和字母表中支援‹p›。

字元組

所有流派都允許把它們所支援的所有的‹u›、‹x›、‹p›和‹p›記号用在字元組之内。這樣會把碼位所表示的字元,或者是在該類别、區塊或者字母表中的字元添加到字元組中。例如,你可以用如下的正則式來比對一個左引号(初始标點屬性)、一個右引号(終止标點屬性)或商标符号(u+2122):

列出所有字元

如果你的正規表達式流派不支援unicode類别、區塊或字母表的話,那麼你可以把屬于該類别、區塊或字母表的字元枚舉到一個字元組中。對于區塊來說,這會比較容易:因為每個區塊其實就是兩個碼位之間的一個範圍。例如,希臘語擴充(greek extended)區塊包括u+1f00~u+1fff的字元:

然而對于大多數類别和許多字母表來說,與之等價的字元組是單個碼位和較短範圍的一張冗長清單。構成每個類别和許多字母表的字元是散布在unicode表中的。下面表示的是希臘語字母表:

在構造這個正規表達式的過程中,我們用unicodeset web應用(<code>http://unicode.org/cldr/utility/list-unicodeset.jsp</code>)生成了希臘語的字母表。在文本框輸入p{greek},勾選“abbreviate”(縮寫)和“escape”(轉義)複選框,再單擊“show set”(顯示碼位集)按鈕。

如本執行個體先前“unicode碼位”一節的解釋,隻有python支援這種unicode碼位文法。要使其他正則流派使用此正規表達式,需要做一些修改。

如果從字元組中删除u+ffff以後的碼位,則很多流派可以正常使用此表達式:

對于unicode碼位,perl和pcre使用不同的文法。在上述正則式中,需要把‹uffff›替換為‹x{ffff}›、‹u0010ffff›替換為‹x{10ffff}›。java 7同樣支援這個正則式: