# 7.6.?個案研究:解析電話號碼
迄今為止,你主要是匹配整個模式,不論是匹配上,還是沒有匹配上。但是正則表達式還有比這更為強大的功能。當一個模式_確實_ 匹配上時,你可以獲取模式中特定的片斷,你可以發現具體匹配的位置。
這個例子來源于我遇到的另一個現實世界的問題,也是在以前的工作中遇到的。問題是:解析一個美國電話號碼。客戶要能 (在一個單一的區域中) 輸入任何數字,然后存儲區號、干線號、電話號和一個可選的獨立的分機號到公司數據庫里。為此,我通過網絡找了很多正則表達式的例子,但是沒有一個能夠完全滿足我的要求。
這里列舉了我必須能夠接受的電話號碼:
* `800-555-1212`
* `800 555 1212`
* `800.555.1212`
* `(800) 555-1212`
* `1-800-555-1212`
* `800-555-1212-1234`
* `800-555-1212x1234`
* `800-555-1212 ext. 1234`
* `work 1-(800) 555.1212 #1234`
格式可真夠多的!我需要知道區號是 `800`,干線號是 `555`,電話號的其他數字為 `1212`。對于那些有分機號的,我需要知道分機號為 `1234`。
讓我們完成電話號碼解析這個工作,這個例子展示第一步。
## 例?7.10.?發現數字
```
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')
>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234')
>>>
```
| | |
| --- | --- |
| \[1\] | 我們通常從左到右閱讀正則表達式。這個正則表達式匹配字符串的開始,接著匹配 `(\d{3})`。`\d{3}` 是什么呢?好吧,`{3}` 的含義是“精確匹配三個數字”;這是曾在前面見到過的 [`{n,m}`](n_m_syntax.html "7.4.?使用 {n,m} 語法") 語法的一種變形。`\d` 的含義是 “任何一個數字” (`0` 到 `9`)。把它們放大括號中意味著要“精確匹配三個數字位,_接著把它們作為一個組保存下來,以便后面的調用_”。接著匹配一個連字符,接著是另外一個精確匹配三個數字位的組,接著另外一個連字符,接著另外一個精確匹配四個數字為的組,接著匹配字符串的結尾。 |
| \[2\] | 為了訪問正則表達式解析過程中記憶下來的多個組,我們使用 `search` 函數返回對象的 `groups()` 函數。這個函數將返回一個元組,元組中的元素就是正則表達式中定義的組。在這個例子中,定義了三個組,第一個組有三個數字位,第二個組有三個數字位,第三個組有四個數字位。 |
| \[3\] | 這個正則表達式不是最終的答案,因為它不能處理在電話號碼結尾有分機號的情況,為此,我們需要擴展這個正則表達式。 |
## 例?7.11.?發現分機號
```
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')
>>> phonePattern.search('800-555-1212-1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800 555 1212 1234')
>>>
>>> phonePattern.search('800-555-1212')
>>>
```
| | |
| --- | --- |
| \[1\] | 這個正則表達式和上一個幾乎相同,正像前面的那樣,匹配字符串的開始,接著匹配一個有三個數字位的組并記憶下來,接著是一個連字符,接著是一個有三個數字位的組并記憶下來,接著是一個連字符,接著是一個有四個數字位的組并記憶下來。不同的地方是你接著又匹配了另一個連字符,然后是一個有一個或者多個數字位的組并記憶下來,最后是字符串的結尾。 |
| \[2\] | 函數 `groups()` 現在返回一個有四個元素的元組,由于正則表達式中定義了四個記憶的組。 |
| \[3\] | 不幸的是,這個正則表達式也不是最終的答案,因為它假設電話號碼的不同部分是由連字符分割的。如果一個電話號碼是由空格符、逗號或者點號分割呢?你需要一個更一般的解決方案來匹配幾種不同的分割類型。 |
| \[4\] | 啊呀!這個正則表達式不僅不能解決你想要的任何問題,反而性能更弱了,因為現在你甚至不能解析一個沒有分機號的電話號碼了。這根本不是你想要的,如果有分機號,你要知道分機號是什么,如果沒有分機號,你仍然想要知道主電話號碼的其他部分是什么。 |
下一個例子展示正則表達式處理一個電話號碼內部,采用不同分隔符的情況。
## 例?7.12.?處理不同分隔符
```
>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')
>>> phonePattern.search('800 555 1212 1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212-1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('80055512121234')
>>>
>>> phonePattern.search('800-555-1212')
>>>
```
| | |
| --- | --- |
| \[1\] | 當心啦!你首先匹配字符串的開始,接著是一個三個數字位的組,接著是 `\D+`,這是個什么東西?好吧,`\D` 匹配任意字符,_除了_ 數字位,`+` 表示“1 個或者多個”,因此 `\D+` 匹配一個或者多個不是數字位的字符。這就是你替換連字符為了匹配不同分隔符所用的方法。 |
| \[2\] | 使用 `\D+` 代替 `-` 意味著現在你可以匹配中間是空格符分割的電話號碼了。 |
| \[3\] | 當然,用連字符分割的電話號碼也能夠被識別。 |
| \[4\] | 不幸的是,這個正則表達式仍然不是最終答案,因為它假設電話號碼一定有分隔符。如果電話號碼中間沒有空格符或者連字符的情況會怎樣哪? |
| \[4\] | 我的天!這個正則表達式也沒有達到我們對于分機號識別的要求。現在你共有兩個問題,但是你可以利用相同的技術來解決它們。 |
下一個例子展示正則表達式處理_沒有_ 分隔符的電話號碼的情況。
## 例?7.13.?處理沒有分隔符的數字
```
>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')
>>> phonePattern.search('80055512121234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800.555.1212 x1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212', '')
>>> phonePattern.search('(800)5551212 x1234')
>>>
```
| | |
| --- | --- |
| \[1\] | 和上一步相比,你所做的唯一變化就是把所有的 `+` 變成 `*`。在電話號碼的不同部分之間不再匹配 `\D+`,而是匹配 `\D*` 了。還記得 `+` 的含義是“1 或者多個”嗎? 好的,`*` 的含義是“0 或者多個”。因此,現在你應該能夠解析沒有分隔符的電話號碼了。 |
| \[2\] | 你瞧,它真的可以勝任。為什么?首先匹配字符串的開始,接著是一個有三個數字位 (`800`) 的組,接著是 0 個非數字字符,接著是一個有三個數字位 (`555`) 的組,接著是 0 個非數字字符,接著是一個有四個數字位 (`1212`) 的組,接著是 0 個非數字字符,接著是一個有任意數字位 (`1234`) 的組,最后是字符串的結尾。 |
| \[3\] | 對于其他的變化也能夠匹配:比如點號分隔符,在分機號前面既有空格符又有 `x` 符號的情況也能夠匹配。 |
| \[4\] | 最后,你已經解決了長期存在的一個問題:現在分機號是可選的了。如果沒有發現分機號,`groups()` 函數仍然返回一個有四個元素的元組,但是第四個元素只是一個空字符串。 |
| \[5\] | 我不喜歡做一個壞消息的傳遞人,此時你還沒有完全結束這個問題。還有什么問題呢?當在區號前面還有一個額外的字符時,而正則表達式假設區號是一個字符串的開始,因此不能匹配。這個不是問題,你可以利用相同的技術“0或者多個非數字字符”來跳過區號前面的字符。 |
下一個例子展示如何解決電話號碼前面有其他字符的情況。
## 例?7.14.?處理開始字符
```
>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')
>>> phonePattern.search('(800)5551212 ext. 1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212', '')
>>> phonePattern.search('work 1-(800) 555.1212 #1234')
>>>
```
| | |
| --- | --- |
| \[1\] | 這個正則表達式和前面的幾乎相同,但它在第一個記憶組 (區號) 前面匹配 `\D*`,0 或者多個非數字字符。注意,此處你沒有記憶這些非數字字符 (它們沒有被括號括起來)。如果你發現它們,只是跳過它們,接著只要匹配上就開始記憶區號。 |
| \[2\] | 你可以成功地解析電話號碼,即使在區號前面有一個左括號。(在區號后面的右括號也已經被處理,它被看成非數字字符分隔符,由第一個記憶組后面的 `\D*` 匹配。) |
| \[3\] | 進行仔細的檢查,保證你沒有破壞前面能夠匹配的任何情況。由于首字符是完全可選的,這個模式匹配字符串的開始,接著是 0 個非數字字符,接著是一個有三個數字字符的記憶組 (`800`),接著是 1 個非數字字符 (連字符),接著是一個有三個數字字符的記憶組 (`555`),接著是 1 個非數字字符 (連字符),接著是一個有四個數字字符的記憶組 (`1212`),接著是 0 個非數字字符,接著是一個有 0 個數字位的記憶組,最后是字符串的結尾。 |
| \[4\] | 此處是正則表達式讓我產生了找一個硬東西挖出自己的眼睛的沖動。為什么這個電話號碼沒有匹配上?因為在它的區號前面有一個 `1`,但是你認為在區號前面的所有字符都是非數字字符 (`\D*`)。唉! |
讓我們往回看一下。迄今為止,正則表達式總是從一個字符串的開始匹配。但是現在你看到了,有很多不確定的情況需要你忽略。與其盡力全部匹配它們,還不如全部跳過它們,讓我們采用一個不同的方法:根本不顯式地匹配字符串的開始。下面的這個例子展示這個方法。
## 例?7.15.?電話號碼,無論何時我都要找到它
```
>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234')
('800', '555', '1212', '1234')
```
| | |
| --- | --- |
| \[1\] | 注意,在這個正則表達式的開始少了一個 `^` 字符。你不再匹配字符串的開始了,也就是說,你需要用你的正則表達式匹配整個輸入字符串,除此之外沒有別的意思了。正則表達式引擎將要努力計算出開始匹配輸入字符串的位置,并且從這個位置開始匹配。 |
| \[2\] | 現在你可以成功解析一個電話號碼了,無論這個電話號碼的首字符是不是數字,無論在電話號碼各部分之間有多少任意類型的分隔符。 |
| \[3\] | 仔細檢查,這個正則表達式仍然工作的很好。 |
| \[4\] | 還是能夠工作。 |
看看一個正則表達式能夠失控得多快?回頭看看前面的例子,你還能區別它們么?
當你還能夠理解這個最終答案的時候 (這個正則表達式就是最終答案,即使你發現一種它不能處理的情況,我也真的不想知道它了),在你忘記為什么你這么選擇之前,讓我們把它寫成松散正則表達式的形式。
## 例?7.16.?解析電話號碼 (最終版本)
```
>>> phonePattern = re.compile(r'''
# don't match beginning of string, number can start anywhere
(\d{3}) # area code is 3 digits (e.g. '800')
\D* # optional separator is any number of non-digits
(\d{3}) # trunk is 3 digits (e.g. '555')
\D* # optional separator
(\d{4}) # rest of number is 4 digits (e.g. '1212')
\D* # optional separator
(\d*) # extension is optional and can be any number of digits
$ # end of string
''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')
('800', '555', '1212', '')
```
| | |
| --- | --- |
| \[1\] | 除了被分成多行,這個正則表達式和最后一步的那個完全相同,因此它能夠解析相同的輸入一點也不奇怪。 |
| \[2\] | 進行最后的仔細檢查。很好,仍然工作。你終于完成了這件任務。 |
## 關于正則表達式的進一步閱讀
* Regular Expression HOWTO 講解正則表達式和如何在 Python 中使用正則表達式。
* _Python Library Reference_ 概述了 [`re` module](http://www.python.org/doc/current/lib/module-re.html)。
- 版權信息
- 第?1?章?安裝 Python
- 1.1.?哪一種 Python 適合您?
- 1.2.?Windows 上的 Python
- 1.3.?Mac OS X 上的 Python
- 1.4.?Mac OS 9 上的 Python
- 1.5.?RedHat Linux 上的 Python
- 1.6.?Debian GNU/Linux 上的 Python
- 1.7.?從源代碼安裝 Python
- 1.8.?使用 Python 的交互 Shell
- 1.9.?小結
- 第?2?章?第一個 Python 程序
- 2.1.?概覽
- 2.2.?函數聲明
- 2.3.?文檔化函數
- 2.4.?萬物皆對象
- 2.5.?代碼縮進
- 2.6.?測試模塊
- 第?3?章?內置數據類型
- 3.1.?Dictionary 介紹
- 3.2.?List 介紹
- 3.3.?Tuple 介紹
- 3.4.?變量聲明
- 3.5.?格式化字符串
- 3.6.?映射 list
- 3.7.?連接 list 與分割字符串
- 3.8.?小結
- 第?4?章?自省的威力
- 4.1.?概覽
- 4.2.?使用可選參數和命名參數
- 4.3.?使用 type、str、dir 和其它內置函數
- 4.4.?通過 getattr 獲取對象引用
- 4.5.?過濾列表
- 4.6.?and 和 or 的特殊性質
- 4.7.?使用 lambda 函數
- 4.8.?全部放在一起
- 4.9.?小結
- 第?5?章?對象和面向對象
- 5.1.?概覽
- 5.2.?使用 from _module_ import 導入模塊
- 5.3.?類的定義
- 5.4.?類的實例化
- 5.5.?探索 UserDict:一個封裝類
- 5.6.?專用類方法
- 5.7.?高級專用類方法
- 5.8.?類屬性介紹
- 5.9.?私有函數
- 5.10.?小結
- 第?6?章?異常和文件處理
- 6.1.?異常處理
- 6.2.?與文件對象共事
- 6.3.?for 循環
- 6.4.?使用 `sys.modules`
- 6.5.?與目錄共事
- 6.6.?全部放在一起
- 6.7.?小結
- 第?7?章?正則表達式
- 7.1.?概覽
- 7.2.?個案研究:街道地址
- 7.3.?個案研究:羅馬字母
- 7.4.?使用 {n,m} 語法
- 7.5.?松散正則表達式
- 7.6.?個案研究:解析電話號碼
- 7.7.?小結
- 第?8?章?HTML 處理
- 8.1.?概覽
- 8.2.?sgmllib.py 介紹
- 8.3.?從 HTML 文檔中提取數據
- 8.4.?BaseHTMLProcessor.py 介紹
- 8.5.?locals 和 globals
- 8.6.?基于 dictionary 的字符串格式化
- 8.7.?給屬性值加引號
- 8.8.?dialect.py 介紹
- 8.9.?全部放在一起
- 8.10.?小結
- 第?9?章?XML 處理
- 9.1.?概覽
- 9.2.?包
- 9.3.?XML 解析
- 9.4.?Unicode
- 9.5.?搜索元素
- 9.6.?訪問元素屬性
- 9.7.?Segue [9]
- 第?10?章?腳本和流
- 10.1.?抽象輸入源
- 10.2.?標準輸入、輸出和錯誤
- 10.3.?查詢緩沖節點
- 10.4.?查找節點的直接子節點
- 10.5.?根據節點類型創建不同的處理器
- 10.6.?處理命令行參數
- 10.7.?全部放在一起
- 10.8.?小結
- 第?11?章?HTTP Web 服務
- 11.1.?概覽
- 11.2.?避免通過 HTTP 重復地獲取數據
- 11.3.?HTTP 的特性
- 11.4.?調試 HTTP web 服務
- 11.5.?設置 User-Agent
- 11.6.?處理 Last-Modified 和 ETag
- 11.7.?處理重定向
- 11.8.?處理壓縮數據
- 11.9.?全部放在一起
- 11.10.?小結
- 第?12?章?SOAP Web 服務
- 12.1.?概覽
- 12.2.?安裝 SOAP 庫
- 12.3.?步入 SOAP
- 12.4.? SOAP 網絡服務查錯
- 12.5.?WSDL 介紹
- 12.6.?以 WSDL 進行 SOAP 內省
- 12.7.?搜索 Google
- 12.8.? SOAP 網絡服務故障排除
- 12.9.?小結
- 第?13?章?單元測試
- 13.1.?羅馬數字程序介紹 II
- 13.2.?深入
- 13.3.?romantest.py 介紹
- 13.4.?正面測試 (Testing for success)
- 13.5.?負面測試 (Testing for failure)
- 13.6.?完備性檢測 (Testing for sanity)
- 第?14?章?測試優先編程
- 14.1.?roman.py, 第 1 階段
- 14.2.?roman.py, 第 2 階段
- 14.3.?roman.py, 第 3 階段
- 14.4.?roman.py, 第 4 階段
- 14.5.?roman.py, 第 5 階段
- 第?15?章?重構
- 15.1.?處理 bugs
- 15.2.?應對需求變化
- 15.3.?重構
- 15.4.?后記
- 15.5.?小結
- 第?16?章?函數編程
- 16.1.?概覽
- 16.2.?找到路徑
- 16.3.?重識列表過濾
- 16.4.?重識列表映射
- 16.5.?數據中心思想編程
- 16.6.?動態導入模塊
- 16.7.?全部放在一起
- 16.8.?小結
- 第?17?章?動態函數
- 17.1.?概覽
- 17.2.?plural.py, 第 1 階段
- 17.3.?plural.py, 第 2 階段
- 17.4.?plural.py, 第 3 階段
- 17.5.?plural.py, 第 4 階段
- 17.6.?plural.py, 第 5 階段
- 17.7.?plural.py, 第 6 階段
- 17.8.?小結
- 第?18?章?性能優化
- 18.1.?概覽
- 18.2.?使用 timeit 模塊
- 18.3.?優化正則表達式
- 18.4.?優化字典查找
- 18.5.?優化列表操作
- 18.6.?優化字符串操作
- 18.7.?小結
- 附錄?A.?進一步閱讀
- 附錄?B.?五分鐘回顧
- 附錄?C.?技巧和竅門
- 附錄?D.?示例清單
- 附錄?E.?修訂歷史
- 附錄?F.?關于本書
- 附錄 G. GNU Free Documentation License
- G.0. Preamble
- G.1.?Applicability and definitions
- G.2.?Verbatim copying
- G.3.?Copying in quantity
- G.4.?Modifications
- G.5.?Combining documents
- G.6.?Collections of documents
- G.7.?Aggregation with independent works
- G.8.?Translation
- G.9.?Termination
- G.10.?Future revisions of this license
- G.11.?How to use this License for your documents
- 附錄 H. GNU 自由文檔協議
- H.0. 序
- H.1.?適用范圍和定義
- H.2.?原樣復制
- H.3.?大量復制
- H.4.?修改
- H.5.?合并文檔
- H.6.?文檔合集
- H.7.?獨立著作聚集
- H.8.?翻譯
- H.9.?終止協議
- H.10.?協議將來的修訂
- H.11.?如何為你的文檔使用本協議
- 附錄 I. Python license
- I.A. History of the software
- I.B.?Terms and conditions for accessing or otherwise using Python
- 附錄 J. Python 協議
- J.0. 關于譯文的聲明
- J.A.?軟件的歷史
- J.B.?使用 Python 的條款和條件