无聊之人--除了技术,还是技术,你懂得
分类: Python/Ruby
2011-08-21 07:43:47
7.6. Case study: Parsing Phone Numbers
7.6 案例研究:解析电话号码
So far you've concentrated on matching whole patterns. Either the pattern matches, or it doesn't. But regular expressions are much more powerful than that. When a regular expression does match, you can pick out specific pieces of it. You can find out what matched where.
迄今为止,你都全神贯注的用在模式匹配上面。不论模式是否匹配。与模式匹配相比,正则表达式还有更加强大的作用。当一个正则表达式匹配的时候,你可以从中筛选出特殊的片段。你可以却在它在那里匹配你的模式。
This example came from another real-world problem I encountered, again from a previous day job. The problem: parsing an American phone number. The client wanted to be able to enter the number free-form (in a single field), but then wanted to store the area code, trunk, number, and optionally an extension separately in the company's database. I scoured the Web and found many examples of regular expressions that purported to do this, but none of them were permissive enough.
这个例子同样来源于我所遇到的另一个现实问题,同样是我的一个日常工作。问题是:解析一个美国的电话号码。客户想自由的输入号码(单一字段),但是接着还想存储下区域号码,主要部分,作为可选的部分在公司的数据库中可以自由的扩展。我搜索互联网,虽然发现了许多使用了正则表达式的可以用来解决上面的问题,但是没有一个让我完全接受。
Here are the phone numbers I needed to be able to accept:
下面是我需要处理的电话号码:
Quite a variety! In each of these cases, I need to know that the area code was 800, the trunk was 555, and the rest of the phone number was 1212. For those with an extension, I need to know that the extension was 1234.
多种多样!,在每一个例子中,我需要知道区域号码是800,尾部是555,余下的号码是1212.对于那些扩展的部分,我需要知道扩展是1234
Let's work through developing a solution for phone number parsing. This example shows the first step.
让我开始研究电话解析问题的解决方案。下面的例子展示了第一步。
Example 7.10. Finding Numbers
例7.10 查找数字
Always read regular expressions from left to right. This one matches the beginning of the string, and then (\d{3}). What's \d{3}? Well, the {3} means “match exactly three numeric digits”; it's a variation on the {n,m} syntax you saw earlier. \d means “any numeric digit” (0 through 9). Putting it in parentheses means “match exactly three numeric digits, and then remember them as a group that I can ask for later”. Then match a literal hyphen. Then match another group of exactly three digits. Then another literal hyphen. Then another group of exactly four digits. Then match the end of the string. 正则表达式在读取的时候总是自左向右读取。该模式也匹配字符串的开始,接着是(\d{3}).\d{3}表示什么呢?哦,{3}表示标签精确匹配三个数字。它是先前你所{n,m}的一种变异。’\d‘的意思是任意数字(包括从0到9)。在一个括号中则意味着它能且只能匹配三个数字,同时记住这是一组,稍后我们会用到。接着匹配了一个Python字面量-。接着又刚好匹配了三个数字。下面又是一个字面量。然后又刚好匹配4个数字。最后匹配到字符串的结束。 |
|
To get access to the groups that the regular expression parser remembered along the way, use the groups() method on the object that the search function returns. It will return a tuple of however many groups were defined in the regular expression. In this case, you defined three groups, one with three digits, one with three digits, and one with four digits. 为了访问正则表达式中的组(这点,自始至终你都必须记住),你可以在搜搜函数返回的对象上面应用使用groups()方法来实现。它将返回一个不变的数组,数组的个数刚好等于你定义在正则表达中的组数。在本例中,你定义了三个组,一组包含3个数字,另一组也是三个数字,最后一个包含4个数字。 |
|
This regular expression is not the final answer, because it doesn't handle a phone number with an extension on the end. For that, you'll need to expand the regular expression. 这个表达式不是最终的答案,因为它没有处理在电话号码末尾存在扩展的情况。对于那种情况,你需要扩展你的正则表达式。 |
Example 7.11. Finding the Extension
例7.11 查找扩展部分
This regular expression is almost
identical to the previous one. Just as before, you match the beginning of the
string, then a remembered group of three digits, then a hyphen, then a
remembered group of three digits, then a hyphen, then a remembered group of four
digits. What's new is that you then match another hyphen, and a remembered
group of one or more digits, then the end of the string. 这个正则表达式同上一个几乎是完全相同的。如前面一样,该模式匹配字符串的开头,然后是一组包含了3个数字的正则表达式组,接着是连接符(-),接着是的3个数字正则表达式组,然后又一个连接符,最后是四个数字的正则表达式组。不同的地方在于你接着又匹配了另一个连接符,然后是一个可能包含1个或是更多数字的正则表达式组,最后匹配字符串的结尾。 The groups() method now
returns a tuple of four elements, since the regular expression now defines
four groups to remember. Groups()方法现在返回一个包含了4个元素的tuple,因为正则表达式现在定义了4个正则表达式组。 Unfortunately, this regular expression
is not the final answer either, because it assumes that the different parts
of the phone number are separated by hyphens. What if they're separated by
spaces, or commas, or dots? You need a more general solution to match several
different types of separators. 不幸的是,该正则表达式同样也不是最终的答案,因为它假定电话的不同部分是通过连接符(-)来分隔。假如电话号码是通过空格,或是逗号,或是点(.),那情况会是什么样呢?你需要一种个通用的解决方法来匹配几种不同的分隔符。 Oops! Not only does this regular
expression not do everything you want, it's actually a step backwards,
because now you can't parse phone numbers without an
extension. That's not what you wanted at all; if the extension is there, you
want to know what it is, but if it's not there, you still want to know what
the different parts of the main number are. 噢!正则表达式不仅没有达到你想要的目标,而且它还倒退了,这是因为如果没有扩展部分,你就不能解析电话号码。这跟不就不是你想要的:如果存在扩展,你想知道它是什么,但是如果不存在扩展,你同样需要知道主要号码的不同之处是什么。
The next example shows the regular expression to handle separators between the different parts of the phone number.
下面的例子展示了正则表达式对于使用了不同分隔符的电话号码是如何处理的。
Example 7.12. Handling Different Separators
例7.12 处理不同的分隔符
Hang on to your hat. You're matching the beginning of the string, then a group of three digits, then \D+. What the heck is that? Well, \D matches any character except a numeric digit, and + means “1 or more”. So \D+ matches one or more characters that are not digits. This is what you're using instead of a literal hyphen, to try to match different separators. 好好听着!你从字符串的开始进行匹配,接着匹配三个数字,\D+,它究竟是什么呢?哦,\D除了数字以外的,能匹配任意字符,而+的意思是一个或是多个。因此\D+能匹配一个或是多个非数字字符。这就是你为了匹配不同的分隔符,使用\D+而不是用的连接符字面量。 |
|
Using \D+ instead of - means you can now match phone numbers where the parts are separated by spaces instead of hyphens. 使用\D+而不是-意味着你可以匹配电话号码中的任意分隔符,而不仅仅是连接符。 |
|
Of course, phone numbers separated by hyphens still work too. 当然,对于分隔符是连接符的电话号码,正则表达式同样使用。 |
|
Unfortunately, this is still not the final answer, because it assumes that there is a separator at all. What if the phone number is entered without any spaces or hyphens at all? 不幸的是,这个仍不是最终的答案。因为该正则表达式假定存在一个分隔符。如果输入的电话号码没有任何空格或是连接符,会怎么样呢? |
|
Oops! This still hasn't fixed the problem of requiring extensions. Now you have two problems, but you can solve both of them with the same technique. 噢!,这个仍然不能解决对于要求扩展的情况。现在,你有两个问题,你可以同样的方法来同时解决这两个问题。 |
The next example shows the regular expression for handling phone numbers without separators.
下面的例子展示了正则表达式如何处理没有分隔符的电话号码。
Example 7.13. Handling Numbers Without Separators
例7.13 处理数组:没有分隔符
The only change you've made since that last step is changing all the + to *. Instead of \D+ between the parts of the phone number, you now match on \D*. Remember that+ means “1 or more”? Well, * means “zero or more”. So now you should be able to parse phone numbers even when there is no separator character at all. 从上一步开始你所需要做的唯一改变就是将+替换*。在不同的电话号码部分之间,你使用\D*,而不是\D+进行匹配。记住+的意思是1个或是多个。噢,*的意思是0个或是多个—任意个。因此在电话号码各部分之间没有分隔符时你也能够处理。 |
|
Lo and behold, it actually works. Why? You matched the beginning of the string, then a remembered group of three digits (800), then zero non-numeric characters, then a remembered group of three digits (555), then zero non-numeric characters, then a remembered group of four digits (1212), then zero non-numeric characters, then a remembered group of an arbitrary number of digits (1234), then the end of the string. 你瞧,它确实工作了。为什么呢?从字符串的开始进行匹配,接着是一个3个一组的数字(800),接着没有任何非数字分隔符,又是3个一组的数字(555),接着又是4个一组的数字(1212),还没有非数字分隔符,接着是任意个数字所够成的扩展部分(1234),字符串结束。 |
|
Other variations work now too: dots instead of hyphens, and both a space and an x before the extension. 其它的情况也同样工作:点而不仅仅是连接符,以及空格和扩展前面的X。 |
|
Finally, you've solved the other long-standing problem: extensions are optional again. If no extension is found, the groups() method still returns a tuple of four elements, but the fourth element is just an empty string. 最后,你解决了另一个长期悬而未决的问题:扩展是可选的。如果没有扩展,方法method()将仍返回包含了4个元素的不变数组,但是第四个元素是一个空字符串。 |
|
I hate to be the bearer of bad news, but you're not finished yet. What's the problem here? There's an extra character before the area code, but the regular expression assumes that the area code is the first thing at the beginning of the string. No problem, you can use the same technique of “zero or more non-numeric characters” to skip over the leading characters before the area code. 我讨厌听到坏消息的,但是你还没有完成呢。问题出在哪里呢?在区域代码前面有一个额外的字符,但是正则表达式认为区域代码是字符串开始的第一部分。没问题,你可以使用同样的技术(任意个费数字字符)来跳过区域代码前面的先导字符。 |
The next example shows how to handle leading characters in phone numbers.
下面的例子显示如何处理电话号码中的先导字符。
Example 7.14. Handling Leading Characters
例7.14 处理先导字符
This is the same as in the previous example, except now you're matching \D*, zero or more non-numeric characters, before the first remembered group (the area code). Notice that you're not remembering these non-numeric characters (they're not in parentheses). If you find them, you'll just skip over them and then start remembering the area code whenever you get to it. 除了你在第一组之前(区域代码)之前使用\D*(匹配任意个字符)来进行匹配以外,该模式同上面的模式仍然是一样。注意你没有标记这些非数字字符(它们没在括号内){这里的记住,也就是我们常说的反向引用}。如果你查到到它们,你会跳过他们然后开始匹配区域代码,不论你什么时候开始匹配。 |
|
You can successfully parse the phone number, even with the leading left parenthesis before the area code. (The right parenthesis after the area code is already handled; it's treated as a non-numeric separator and matched by the \D* after the first remembered group.) 你可以顺利的解析电话号码,即使在区域代码前面存在先导左括号。(在区域代码后面的右括号我们以及处理过,它被认为是非数字分隔符,在第一个区域代码后通过\D*来匹配。) |
|
Just a sanity check to make sure you haven't broken anything that used to work. Since the leading characters are entirely optional, this matches the beginning of the string, then zero non-numeric characters, then a remembered group of three digits (800), then one non-numeric character (the hyphen), then a remembered group of three digits (555), then one non-numeric character (the hyphen), then a remembered group of four digits (1212), then zero non-numeric characters, then a remembered group of zero digits, then the end of the string. 进行一下明智的检查来确保你没有损坏你过去的工作。因为先导字符时完全可选的,该模式接着匹配字符串的开始,接着是一个非数字字符(连字符),接着是3个一组的数字(800),接着又是一个非数字字符(连字符),后面是3个一组的数字(555)。又是一个非数字字符(连字符),接着是4个一组的数字(1212),接着是一个非数字字符,最后是0个数字,按后匹配到字符串的末尾。 |
|
This is where regular expressions make me want to gouge my eyes out with a blunt object. Why doesn't this phone number match? Because there's a 1 before the area code, but you assumed that all the leading characters before the area code were non-numeric characters (\D*). Aargh. 这里就是正则表达式使用一个未知对象来欺骗我的眼睛。为什么这个电话号码不能解析?这是因为在区域代码前面有一个1,但是你假定区域代码前面所有的字符都是非数字字符。真让真抓狂! |
Let's back up for a second. So far the regular expressions have all matched from the beginning of the string. But now you see that there may be an indeterminate amount of stuff at the beginning of the string that you want to ignore. Rather than trying to match it all just so you can skip over it, let's take a different approach: don't explicitly match the beginning of the string at all. This approach is shown in the next example.
让我们改进一下第二个表达式。到先前为止,正则表达式能匹配所有从字符串开始的电话号码。但是现在你看到在字符串开始之前存在存在许多不能确定的字符,而你还想忽略它。与其匹配对这些字符进行匹配,还不不完全跳过他们。我们采取一种完全不同的方法:一点也不显式的匹配字符串开始的字符。这个方法如下。
Example 7.15. Phone Number, Wherever I May Find Ye
例7.15 电话号码:无论如何都能找到
Note the lack of ^ in this regular expression. You are not matching the beginning of the string anymore. There's nothing that says you need to match the entire input with your regular expression. The regular expression engine will do the hard work of figuring out where the input string starts to match, and go from there. 注意到正则表达式缺少了^.你不在匹配字符串的开始部分。没有任何人说你必须是使用正则表达式来完全匹配输出。正则表达式将会完成最艰苦的工作:查到到字符串的开始进行匹配,然后从那儿开始进行匹配。 |
|
Now you can successfully parse a phone number that includes leading characters and a leading digit, plus any number of any kind of separators around each part of the phone number. 现在你能顺利解析那些包含了先导字符以及不包含先导字符,外加电话号码不同部分之间存在任意种类分隔符的电话号码。 |
|
Sanity check. this still works. 谨慎检查。该表达式仍然工作 |
|
That still works too. 同样工作。 |
See how quickly a regular expression can get out of control? Take a quick glance at any of the previous iterations. Can you tell the difference between one and the next?
你看到正则表达式很快就超出了你的控制。迅速浏览一下先前的迭代过程。你能分辨出这个例子和下一个之间的不同么?
While you still understand the final answer (and it is the final answer; if you've discovered a case it doesn't handle, I don't want to know about it), let's write it out as a verbose regular expression, before you forget why you made the choices you made.
虽然你完全理解了最后的答案(它是最终版本,如果你发现了一个它不能处理的例子,我根本就不想知道),在你忘记你为什么改变的你的选择之前,我们在写一个verbose版本的正则表达式
Example 7.16. Parsing Phone Numbers (Final Version)
例7.16 解析电话号码(终极版本)
Other than being spread out over multiple lines, this is exactly the same regular expression as the last step, so it's no surprise that it parses the same inputs. 除了再一次展开多行以外,在最后一步,这个正则表达都是完全相同的,解析相同的输入也就没有什么值得惊讶的了。 |
|
Final sanity check. Yes, this still works. You're done. |
最终谨慎的检测。是的,它仍然能共组。你的工作完成了。