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

字符串是编程时涉及到的最多的一种数据结构，对字符串进行操作的需求几乎无处不在。比如判断一个字符串是否是合法的Email地址，虽然可以编程提取@前后的子串，再分别判断是否是单词和域名，但这样做不但麻烦，而且代码难以复用。本文为学习[廖雪峰的教程](https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386832260566c26442c671fa489ebc6fe85badda25cd000#0)与[崔庆才的教程](https://germey.gitbooks.io/python3webspider/content/3.3-%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F.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/>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET 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?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
