CASE-003
Grep & Co. 数据侦探事务所
委托书
委托人
胡同里“野花”花店的老板
证物
一本被雨淋湿撕破的熟客电话簿,用 OCR 识别出来的名单。分隔符五花八门,还混进了莫名其妙的数字。
破案条件
从名单里收集所有的电话号码和邮政编码。
早上一上班,所长已经把昨天写的车牌模式投在了屏幕上。“与其把 \d 写四遍,不如直接写‘四遍’。今天学这个。”这时门开了,胡同里花店的老板抱着一叠湿透的纸走进来。“熟客的电话簿全被雨淋湿了。我扫描后转成了文字,可乱得一塌糊涂。”
OCR 识别出来的名单如下。每个号码的分隔符都不一样,最后一行看着也有点不对劲。
kim 010-0000-1234 04542
lee 010 0000 9876 04545
park 02-0000-5678 04538
choi 010.0000.4321 04539
yoon 1234-5678-9012用来写“同一个东西出现几次”的语法,叫作量词。今天就用量词来整理这份名单。
量词决定紧挨在前面的那一块重复几次。\d{4} 和 \d\d\d\d 是一样的。来把第 2 课的车牌模式缩短一下。
re.findall(r"\d{2}[가-힣]\d{4}", "12가3457 123가4567") # ['12가3457', '23가4567']是变短了,可问题也露出来了。2019 年起发行的新式车牌,前面是三位数。从 123가4567 里,它丢掉了开头的 1,只截下了 23가4567。
{m,n} 表示至少 m 次、至多 n 次。把前面改成 \d{2,3},两位和三位就都能接住了。
re.findall(r"\d{2,3}[가-힣]\d{4}", "12가3457 123가4567") # ['12가3457', '123가4567']{2,3} 这样,逗号前后不要加空格。加了空格就不再是量词了(见下面多特的提醒)。\d{5}。证据分析室
re.search(r"", text)
返回值
{m,n} 的其中一边可以空着不写。
{m,}:m 次以上,上不封顶{,n}:0 次以上、n 次以下。因为 0 次也可以,所以连一个字符都没有的位置也能匹配re.findall(r"a{2,}", "a aa aaaa") # ['aa', 'aaaa']
re.findall(r"\d{3,}", "1 22 333 4444") # ['333', '4444']
re.findall(r"\d{,2}", "123") # ['12', '3', '']最后那个结果末尾的 '',是在字符串最末尾、数字为 0 个的位置上匹配到的。这种空匹配在 3-6 里细说。
如果 { 不是量词的样子,它就被当成普通字符。a{x} 找的是 a、{、x、} 这四个字符。
re.findall(r"a{x}", "a{x} ax") # ['a{x}']
re.findall(r"a{2, 3}", "aa a{2, 3}") # ['a{2, 3}']多特的提醒
我像 a{2, 3} 这样在逗号后面多敲了一个空格,结果它一声不吭,悄悄找了个莫名其妙的东西。不像量词的话,它就只当普通字符看嘛。没报错,不代表模式就是对的。花括号里只写数字和逗号哦。
最常用的几种次数,各有一个单字符的名字。
| 量词 | 含义 | 等价写法 |
|---|---|---|
* | 0 次以上 | {0,} |
+ | 1 次以上 | {1,} |
? | 0 次或 1 次(可有可无) | {0,1} |
re.findall(r"ab*", "a ab abbb") # ['a', 'ab', 'abbb']
re.findall(r"ab+", "a ab abbb") # ['ab', 'abbb']
re.findall(r"ab?", "a ab abbb") # ['a', 'ab', 'ab']
re.findall(r"colou?r", "color colour colouur") # ['color', 'colour']? 正适合“可有可无的字符”。名单里的手机号,分隔符有连字符、空格、句点,各不相同。把分隔符的位置写成 [-. ]?,三者有一个也行,完全没有也行。
re.findall(r"010[-. ]?\d{4}[-. ]?\d{4}", phonebook)
# ['010-0000-1234', '010 0000 9876', '010.0000.4321']照第 2 课学的,方括号里的 . 是真正的句点,- 在最前面,所以也是字符本身。
证据分析室
re.search(r"", text)
返回值
量词只作用于紧挨在前面的那一块。一块,指的是一个字符、一个方括号字符类,或者一个 \d 这样的简写。所以 ab+ 不是“ab 重复多次”,而是“a 后面跟着多个 b”。
re.findall(r"ab+", "abbb abab") # ['abbb', 'ab', 'ab']
re.fullmatch(r"ab+", "abab") # None
re.fullmatch(r"(ab)+", "abab") # <re.Match object; span=(0, 4), match='abab'>想让多个字符一起重复,就用括号把它们包成一块。(ab)+ 是“ab 出现 1 次以上”。括号除了打包以外还有一个重要的作用,第 5 课再学。
re.fullmatch 只有在整个字符串都符合模式时才匹配。具体区别第 4 课再讲。
证据分析室
re.search(r"", text)
返回值
量词默认是贪婪的(greedy):在允许的范围内,能拿多少就拿多少。
re.search(r"\d+", "tel 0100000") # <re.Match object; span=(4, 11), match='0100000'>
re.search(r"\d{2,4}", "123456") # <re.Match object; span=(0, 4), match='1234'>
re.findall(r"\d{2,4}", "123456") # ['1234', '56']\d{2,4} 两位也行,但它先拿走四位。只有后面的模式失败时,它才一个一个地吐回来。这种“吐回来再试一次”叫作回溯,到了第 8 课,它就是让事务所停摆的元凶。少拿一点的懒惰量词 +?,也到那时再学。
现在把整份名单里找到的东西收集起来。假设上面的名单存放在字符串 phonebook 里。
re.findall 把匹配到的文字以字符串列表的形式返回。这是模式里没有括号时的行为;有括号时怎样,第 5 课再看。
re.findall(r"\d{5}", phonebook) # ['04542', '04545', '04538', '04539']re.finditer 返回一个逐个取出 Match 对象的迭代器。连位置(span)都需要时就用它。
for m in re.finditer(r"\d{5}", phonebook):
print(m.group(), m.span())
# 04542 (22, 27)
# 04545 (50, 55)
# 04538 (78, 83)
# 04539 (106, 111)re.finditer。这里要小心的是空匹配。*、?、{,n} 都允许 0 次,所以在一个字符都没有的位置也能匹配成功。findall 也会把这种空匹配当作 '' 放进结果里。
re.findall(r"\d*", "a12b") # ['', '12', '', '']
re.findall(r"\d+", "a12b") # ['12']a 前面一个空匹配,然后是 12,b 前面一个空匹配,字符串末尾又一个空匹配。要收集什么东西时,就用不允许 0 次的 + 或 {1,}。
证据分析室
re.search(r"", text)
返回值
电话号码是:区号或开头 23 位,中间 34 位,最后 4 位。分隔符可有可无。邮政编码是五位数。
phones = re.findall(r"\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4}", phonebook)
print(phones)
# ['010-0000-1234', '010 0000 9876', '02-0000-5678', '010.0000.4321', '234-5678-9012']
postcodes = re.findall(r"\d{5}", phonebook)
print(postcodes) # ['04542', '04545', '04538', '04539']四个电话号码和四个邮政编码都收齐了。可第五个“电话号码” 234-5678-9012 是什么?花店老板看了一眼屏幕,摇摇头:“那不是电话号码,是我记下的银行卡号。”
re.search(r"\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4}", "yoon 1234-5678-9012")
# <re.Match object; span=(7, 20), match='234-5678-9012'>模式跳过了 1234 的第一个字符 1,从数字正中间的第 7 号位置开始匹配。这和 3-1 里的 23가4567 是同一种事故。
证据分析室
re.search(r"", text)
返回值
结案报告 — 从 OCR 名单中恢复出电话号码 4 条、邮政编码 4 条,已交给花店老板。银行卡号的一部分被误当成电话号码的问题,暂列为未解决。
所长盯着最后一行看了很久。“从数字中间开始了。这个模式不知道号码从哪儿开始、到哪儿结束。”下一课,就来学怎样把这个边界写进模式里。
\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4} 在 01000001234(没有分隔符的号码)里会找到什么,再运行看看。re.findall(r"\d?", "a1") 的结果。空字符串有几个?re.finditer 找出名单里的电话号码,每行打印出号码和它的起始位置。所长的笔记本
{m} 正好 m 次,{m,n} 是 m~n 次,{m,} 是 m 次以上,{,n} 是 0~n 次。逗号旁边不许有空格。
* 是 0 次以上,+ 是 1 次以上,? 是 0 次或 1 次。
量词只管紧挨在前面的那一块。ab+ 和 (ab)+ 不一样。
量词是贪婪的,能拿多少就拿多少。
re.findall 返回字符串列表,re.finditer 返回 Match 迭代器。
允许 0 次的模式会产生空匹配 ''。要收集东西就用 +。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。