> For the complete documentation index, see [llms.txt](https://meixuhong.gitbook.io/pythonspider/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://meixuhong.gitbook.io/pythonspider/1.-zhun-bei-gong-zuo/3.-shi-yong-python-de-zheng-ze-biao-da-shi-guo-lv-zi-fu-chuan.md).

# 1.3 使用Python的正则表达式过滤字符串

字符串是编程时涉及到的最多的一种数据结构，对字符串进行操作的需求几乎无处不在。比如判断一个字符串是否是合法的Email地址，虽然可以编程提取@前后的子串，再分别判断是否是单词和域名，但这样做不但麻烦，而且代码难以复用。本文为学习[廖雪峰的教程](https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386832260566c26442c671fa489ebc6fe85badda25cd000#0)与[崔庆才的教程](https://germey.gitbooks.io/python3webspider/content/3.3-正则表达式.html)总结输出。

## 1. 正则表达式规则

正则表达式是一种用来匹配**字符串**的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则，凡是符合规则的字符串，我们就认为它**匹配**了，否则，该字符串就是不合法的。

所以我们判断一个字符串是否是合法的Email的方法是：

* 创建一个匹配Email的正则表达式；
* 用该正则表达式去匹配用户的输入来判断是否合法。

因为正则表达式也是用字符串表示的，所以，我们要首先了解如何用字符来描述字符。

在正则表达式中，如果直接给出字符，就是精确匹配。

### 1.1 单个字符匹配

1. 用`\d`可以匹配**一个数字**.
2. 用`\w`可以匹配**一个字母或数字**
   * `00\d`可以匹配`007`，但无法匹配`00A`；
   * `\d\d\d`可以匹配`010`；
   * `\w\w\d`可以匹配`py3`；
3. `.`可以匹配**任意字符**

   `py.`可以匹配`pyc`、`pyo`、`py!`等等。

### 1.2 联合前置正则表达式字符匹配

上面三个为匹配单个字符，直接进行单字符匹配。如果要匹配变长的字符，在正则表达式中，需要使用特殊字符来匹配，如`*`,`+`,`?`,`{}`，这些特殊字符**必须需要与前面的最近的一个正则表达式字符一起使用**。

1. 使用`*`匹配**任意个字符**(包括0个)，单个`*`无效，需要与它前面的正则表达式字符(**最近的那个**)一起使用
2. 使用`+`匹配**至少一个**字符，单个`+`无效，需要与它前面的正则表达式字符(**最近的那个**)一起使用
3. 使用`?`匹配**0个或1个字符**，单个`?`无效，需要与它前面的正则表达式字符(**最近的那个**)一起使用
4. 使用`{n}`匹配**n个字符**，单个`{ }`无效，需要与它前面的正则表达式字符(**最近的那个**)一起使用
5. 使用`{n,m}`匹配**n-m**个字符，单个`{n,m}`无效，需要与它前面的正则表达式字符(**最近的那个**)一起使用

来看一个复杂的例子：`\d{3}\s+\d{3,8}`，从左到右解读一下：

* `\d{3}`表示匹配**3个数字**，例如`010`,其中`{}`与`\d`一起使用才有效
* `\s+`表示以匹配**至少一个空格**（也包括Tab等空白符），其中`+`与`\s`一起使用才有效
* `\d{3,8}`表示匹配**3-8个数字**，例如`1234567`，其中`{3,8}`与`\d`一起使用才有效

综合起来，上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。

### 1.3 转义字符匹配

上面的例子如果要匹配`010-12345`这样的号码呢？由于`-`是特殊字符，在正则表达式中，要用`\`转义，所以，上面的正则是`\d{3}\-\d{3,8}`。

但是，仍然无法匹配`010 - 12345`，因为带有空格。所以我们需要更复杂的匹配方式，如`\d{3}\s\-\s\d{3,8}`。

### 1.4 范围匹配

1. 使用`[ ]`表示匹配某范围中一个字符
   * `[0-9a-zA-Z\_]`可以匹配**一个** *数字、字母或者下划线*，其中`0-9`表示任意一个数字，`a-z`表示任意一个小写字母，`A-Z`表示任意一个大写字母，整个正则表示要么匹配**一个数字**，要么匹配**一个小写字母**，要么匹配**一个大写字母**
   * `[0-9a-zA-Z\_]+`可以匹配**至少由一个** *数字、字母或者下划线*组成的字符串，比如`a100`，`0_Z`，`Py3000`等等；
   * `[a-zA-Z\_][0-9a-zA-Z\_]*`可以匹配以字母或者下划线开头，后接任意一个由数字、字母或者下划线组成的字符串，也就是Python的合法变量。**注意，这个正则表达式中的最后一个**`*`**只对它最近的正则式也就是**`[0-9a-zA-Z]`**生效**。
   * `[^xyz]`匹配的是**非xyz**中的任一字符
2. 使用`^`表示**行**的开头

   `^\d`表示必须以数字开头的行
3. 使用`$`表示**行**的结束

   `\d$`表示必须以数字结尾的行
4. 使用`|`表示**或匹配**

   `A|B`表示匹配**A或者B**，如`(P|p)ython`可以匹配`Python`或者`python`。

## 2. 在Python中使用正则表达式

Python安装好后，有自带的`re`模块进行正则匹配，包含所有正则表达式的功能，无需额外安装别的库。

### 2.1 re模块

有了准备知识，我们就可以在Python中使用正则表达式了。由于Python的字符串本身也用\转义，所以要特别注意：

```python
s = 'ABC\\-001' # Python的字符串
# 对应的正则表达式字符串变成：
# 'ABC\-001'
```

因此我们强烈建议使用Python的**r前缀**，就不用考虑转义的问题了：

```python
s = r'ABC\-001' # Python的字符串
# 对应的正则表达式字符串不变：
# 'ABC\-001'
```

先看看如何判断正则表达式是否匹配：

```python
>>> import re
>>> re.match(r'^\d{3}\-\d{3,8}$', '010-12345')
<_sre.SRE_Match object at 0x1026e18b8>
>>> re.match(r'^\d{3}\-\d{3,8}$', '010 12345')
>>>
```

`match()`方法判断是否匹配，如果匹配成功，返回一个`Match`对象，否则返回`None`。常见的判断方法就是：

```python
test = '用户输入的字符串'
if re.match(r'正则表达式', test):
    print 'ok'
else:
    print 'failed'
```

### 2.2 切分字符串

用正则表达式切分字符串比用固定的字符更灵活，请看正常的切分代码：

```python
>>> 'a b   c'.split(' ')
['a', 'b', '', '', 'c']
```

嗯，无法识别连续的空格，用正则表达式试试：

```python
>>> re.split(r'\s+', 'a b   c')
['a', 'b', 'c']
```

无论多少个空格都可以正常分割。加入`,`试试：

```python
>>> re.split(r'[\s\,]+', 'a,b, c  d')
['a', 'b', 'c', 'd']
```

再加入`;`试试：

```python
>>> re.split(r'[\s\,\;]+', 'a,b;; c  d')
['a', 'b', 'c', 'd']
```

如果用户输入了一组标签，下次记得用正则表达式来把不规范的输入转化成正确的数组。

### 2.3 字符串分组

除了简单地判断是否匹配之外，正则表达式还有提取子串的强大功能。用`()`表示的就是要提取的分组（Group）。比如：

`^(\d{3})-(\d{3,8})$`分别定义了两个组，第一组`(\d{3})`，第二组`\d{3,8}$`, 整个表达式可用于直接从匹配的字符串中提取出区号和本地号码：

```python
>>> m = re.match(r'^(\d{3})\-(\d{3,8})$', '010-12345')
>>> m
<_sre.SRE_Match object at 0x1026fb3e8>
>>> m.group(0)
'010-12345'
>>> m.group(1)
'010'
>>> m.group(2)
'12345'
```

如果正则表达式中定义了组，就可以在`Match`对象上用`group()`方法提取出子串来。注意到`group(0)`永远是原始字符串，`group(1)`、`group(2)`……表示第1、2、……个子串。

同时注意，`'^(\d{3})\-(\d{3,8})$'`表示的是**以三个数字开头，紧跟一个**`-`**符号，然后以**`3到8`**个数字为结尾**，**其中前面三个数字放在**`group 1`**中**，**后面**`3到8`**个数字放在**`group 2`**中**。

提取子串非常有用。来看一个更凶残的例子：

```python
>>> t = '19:05:30'
>>> m = re.match(r'^(0[0-9]|1[0-9]|2[0-3]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$', t)
>>> m.groups()
('19', '05', '30')
```

这个正则表达式可以直接识别合法的时间。但是有些时候，用正则表达式也无法做到完全验证，比如识别日期：

```python
'^(0[1-9]|1[0-2]|[0-9])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$'
```

对于`'2-30'`，`'4-31'`这样的非法日期，用正则还是识别不了，或者说写出来非常困难，这时就需要程序配合识别了。

### 2.4 贪婪匹配

需要特别指出的是，正则匹配**默认是贪婪匹配**，也就是匹配尽可能多的字符。举例如下，匹配出数字后面的`0`：

```python
>>> re.match(r'^(\d+)(0*)$', '102300').groups()
('102300', '')
```

由于`\d+`采用贪婪匹配，直接把后面的`0`全部匹配了，结果`0*`只能匹配空字符串了。

必须让`\d+`采用非贪婪匹配（也就是尽可能少匹配），才能把后面的`0`匹配出来，加个`?`就可以让`\d+`采用非贪婪匹配：

```python
>>> re.match(r'^(\d+?)(0*)$', '102300').groups()
('1023', '00')
```

**这是因为**`?`**本来就是指匹配0个或者1个。**

### 2.5 正则表达式预编译

当我们在Python中使用正则表达式时，re模块内部会干两件事情：

1. 编译正则表达式，如果正则表达式的字符串本身不合法，会报错；
2. 用编译后的正则表达式去匹配字符串。

如果一个正则表达式要重复使用几千次，出于效率的考虑，我们可以预编译该正则表达式，接下来重复使用时就不需要编译这个步骤了，直接匹配：

```python
>>> import re
# 编译:
>>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
# 使用：
>>> re_telephone.match('010-12345').groups()
('010', '12345')
>>> re_telephone.match('010-8086').groups()
('010', '8086')
```

编译后生成`Regular Expression`对象，由于该对象自己包含了正则表达式，所以调用对应的方法时不用给出正则字符串。

练习：写一个验证Email地址的正则表达式。

版本一应该可以验证出类似的Email：

```python
someone@gmail.com
bill.gates@microsoft.com
h23384-hd@test.com
hasd89_234f@test.org
```

正则表达式：

```
'^[0-9a-zA-Z\.\_\-]+\@[0-9a-zA-Z\-\_]+\.[a-zA-Z]+$'
```

版本二可以验证并提取出带名字的Email地址：

```
<Tom Paris> tom@voyager.org
```

Python处理正则表达式：

```python
import re

mystr = '<Tom Paris> tom@voyager.org'

#使用()来分组，具体原则参考上面章节
match_str = re.match(r'(^\<[\w\s]+\>)\s+([0-9a-zA-Z\.\_\-]+\@[0-9a-zA-Z\-\_]+\.[a-zA-Z]+)',mystr)

name = match_str.group(1)
email = match_str.group(2)

print(name)
print(email)
```

### 2.6 search()函数替换match()函数

`match()` 方法是从字符串的开头开始匹配，一旦开头不匹配，那么整个匹配就失败了，更适合**检查某段字符串是否存在于目标字符串中**。

如：

```python
import re

content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'
result = re.match('Hello.*?(\d+).*?Demo', content)
print(result)

####### 运行结果#######
None
```

运行结果为`None`，因为这段字符串并没有以`Hello`开头，即使查找的字符串包含在目标字符串中。如果使用`search()`函数的话，就能正常匹配挑选出字符串了。

```python
import re

content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'
result = re.search('Hello.*?(\d+).*?Demo', content)
print(result)

####### 运行结果#######
<_sre.SRE_Match object; span=(13, 53), match='Hello 1234567 World_This is a Regex Demo'>
```

### 2.7 findall()查找所有内容

上面的例子通过`search()`函数可以返回**匹配正则表达式的第一个内容**，但是如果我们想要获取匹配正则表达式的所有内容的话怎么办？这时就需要借助于 `findall()` 方法了。 `findall()` 方法会搜索整个字符串然后返回匹配正则表达式的所有内容。将以下面的例子进行讲解。

## 3. 使用Python正则表达式解析网页

基于上面的这些总结，可以进行一个简单的网页解析测试。如从下面的HTML代码中提取*齐秦*的*往事如风*。

```python
import re

html = '''<div id="songs-list">
    <h2 class="title">经典老歌</h2>
    <p class="introduction">
        经典老歌列表
    </p>
    <ul id="list" class="list-group">
        <li data-view="2">一路上有你</li>
        <li data-view="7">
            <a href="/2.mp3" singer="任贤齐">沧海一声笑</a>
        </li>
        <li data-view="4" class="active">
            <a href="/3.mp3" singer="齐秦">往事随风</a>
        </li>
        <li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
        <li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
        <li data-view="5">
            <a href="/6.mp3" singer="邓丽君"><i class="fa fa-user"></i>但愿人长久</a>
        </li>
    </ul>
</div>'''

result = re.search(r'<li.*?active.*?singer="(.*?)">(.*?)</a>', html, re.S)
if result:
    print(result.group(1), result.group(2))

####### 运行结果#######
$ python test.py
齐秦 往事随风
```

`search()`函数的三个参数：

1. `r'<li.*?active.*?singer="(.*?)">(.*?)</a>'`为正则表达式，
   * 其中`<li`会匹配上所有**li**标签。
   * `.*?`表示匹配**任意个（0\~n个）任意(包括空格,=等等)**&#x5B57;符，且`?`指定了匹配为**非贪婪模式**，与后续的正则表达式配合使用，表示**匹配到一个这样的字符串即可**。即需要将`<li.*?active`一起看，能匹配上`<li data-view="4" class="active`直到其遇到`active`时这段正则表达式匹配结束。
   * `singer="(.*?)`匹配`single`字符串，`.*?`同上匹配**任意个任意字符，一旦匹配到为止不再继续贪婪匹配更多**，同时将`.*?`用括号`()`括起来，用于将它的值保存到`group()`中。
   * `">`匹配冒号，右破折号
   * `(.*?)</a>`匹配`</a>`前的**任意个任意字符，一旦匹配到为止不再继续贪婪匹配更多**，用括号括起来以后，获取的值将会被保存在`group()`中。
2. `html`为待解析的字符串变量
3. `re.S`为固定用法，用于处理换行，因为`html`中有换行字符，所以需要添加该参数。

这个例子中我们使用了`search()`函数进行单一字符匹配，如果需要获取到所有*歌手*与所有*歌名*的话，可以使用`findall()`进行匹配。

```python
import re

html = '''<div id="songs-list">
    <h2 class="title">经典老歌</h2>
    <p class="introduction">
        经典老歌列表
    </p>
    <ul id="list" class="list-group">
        <li data-view="2">一路上有你</li>
        <li data-view="7">
            <a href="/2.mp3" singer="任贤齐">沧海一声笑</a>
        </li>
        <li data-view="4" class="active">
            <a href="/3.mp3" singer="齐秦">往事随风</a>
        </li>
        <li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
        <li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
        <li data-view="5">
            <a href="/6.mp3" singer="邓丽君"><i class="fa fa-user"></i>但愿人长久</a>
        </li>
    </ul>
</div>'''

results = re.findall(r'<li.*?singer="(.*?)">(.*?)</a>', html,re.S)
# print(results)
for result in results:
    print(result)

####### 运行结果#######
$ python test.py
('任贤齐', '沧海一声笑')
('齐秦', '往事随风')
('beyond', '光辉岁月')
('陈慧琳', '记事本')
('邓丽君', '<i class="fa fa-user"></i>但愿人长久')
```

### Tips

可以使用下述网站进行正则表达式的验证。

<https://regex101.com/> <http://tool.oschina.net/regex/>
