<audio id="audio" title="06 | 转义：正则中转义需要注意哪些问题？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9d/87/9da3e8fc4abbc461e7fc165d498e4887.mp3"></audio>

你好，我是伟忠。今天我来和你聊聊转义。转义对我们来说都不算陌生，编程的时候，使用到字符串时，双引号里面如果再出现双引号，我们就可以通过转义来解决。就像下面这样：

```
str = &quot;How do you spell the word \&quot;regex\&quot;?&quot;

```

虽然转义在日常工作中比较常见，但正则中什么时候需要转义，什么时候不用转义，在真正使用的时候可能会遇到这些麻烦。所以我们很有必要来系统了解一下正则中的转义。

## 转义字符

首先我们说一下什么是转义字符（Escape Character）。它在维基百科中是这么解释的：

> 
在计算机科学与远程通信中，当转义字符放在字符序列中，它将对它后续的几个字符进行替代并解释。通常，判定某字符是否为转义字符由上下文确定。转义字符即标志着转义序列开始的那个字符。


这么说可能有点不好理解，我再来给你通俗地解释一下。转义序列通常有两种功能。第一种功能是编码无法用字母表直接表示的特殊数据。第二种功能是用于表示无法直接键盘录入的字符（如回车符）。

我们这节课说的就是第二种情况，转义字符自身和后面的字符看成一个整体，用来表示某种含义。最常见的例子是，C语言中用反斜线字符“\”作为转义字符，来表示那些不可打印的ASCII控制符。另外，在URI协议中，请求串中的一些符号有特殊含义，也需要转义，转义字符用的是百分号“%”。之所以把这个字符称为**转义字符**，是因为它后面的字符，不是原来的意思了。

在日常工作中经常会遇到转义字符，比如我们在shell中删除文件，如果文件名中有*号，我们就需要转义，此时我们能看出，使用了转义字符后，*号就能放进文件名里了。

```
rm access_log*    # 删除当前目录下 access_log 开头的文件
rm access_log\*   # 删除当前目录下名字叫 access_log* 的文件

```

再比如我们在双引号中又出现了双引号，这时候就需要转义了，转义之后才能正常表示双引号，否则会报语法错误。比如下面的示例，引号中的 Hello World! 也是含有引号的。

```
print &quot;tom said \&quot;Hello World!\&quot; to the crowd.&quot;

```

下面是一些常见的转义字符以及它们的含义。

<img src="https://static001.geekbang.org/resource/image/03/35/034b3619dab627fc96b85ac8de004b35.jpg" alt="">

## 字符串转义和正则转义

说完了转义字符，我们再来看一下正则中的转义。正则中也是使用反斜杠进行转义的。

一般来说，正则中 \d 代表的是单个数字，但如果我们想表示成 反斜杠和字母d，这时候就需要进行转义，写成 `\\d`，这个就表示反斜杠后面紧跟着一个字母d。

<img src="https://static001.geekbang.org/resource/image/11/e9/112c1fa35dbc600c3fc8e6562aea46e9.png" alt="">

刚刚的反斜杠和d是连续出现的两个字符，如果你想表示成反斜杠或d，可以用管道符号或中括号来实现，比如 \|d 或 [\d]。

<img src="https://static001.geekbang.org/resource/image/yy/35/yy70d6900cfbe786d64bdc7b5d87e735.png" alt="">

需要注意的是，如果你想用代码来测试这个，在程序中表示普通字符串的时候，我们如果要表示反斜杠，通常需要写成两个反斜杠，因为只写一个会被理解成“转义符号”，而不是反斜杠本身。

下面我给出使用 Python3 来测试的情况，你可以看一下。

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall('\\|d', 'a*b+c?\d123d\')  # 字符串没转义&quot;反斜杠&quot;
  File &quot;&lt;input&gt;&quot;, line 1
      re.findall('\\|d', 'a*b+c?\d123d\')
                                       ^
SyntaxError: EOL while scanning string literal

&gt;&gt;&gt; re.findall('\\|d', 'a*b+c?\\d123d\\')
[]


```

看到这里，你内心是不是有很多问号？为什么转义了还不行呢？我们来把正则表达式部分精简一下，看看两个反斜杠在正则中是什么意思。

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall('\\', 'a*b+c?\\d123d\\')
Traceback (most recent call last):
 省去部分信息
re.error: bad escape (end of pattern) at position 0

```

我们发现，正则部分写的两个反斜杠，Python3 处理的时候会报错，认为是转义字符，即认为是单个反斜杠，如果你再进一步测试在正则中写单个反斜杠，你会发现直接报语法错误，你可以自行尝试。

那如何在正则中正确表示“反斜杠”呢？答案是写四个反斜杠。

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall('\\\\', 'a*b+c?\\d123d\\')
['\\', '\\']

```

你可以想一下，为什么不是三个呢？后面的文本部分，也得要用四个反斜杠表示才是正确的么？到这里，你是不是发现，转义其实没那么简单。

我来给你详细解释一下这里面的过程，在程序使用过程中，从输入的字符串到正则表达式，其实有两步转换过程，分别是字符串转义和正则转义。

在正则中正确表示“反斜杠”具体的过程是这样子：我们输入的字符串，四个反斜杠 \\，经过第一步字符串转义，它代表的含义是两个反斜杠 \；这两个反斜杠再经过第二步**正则转义**，它就可以代表单个反斜杠 \了。

<img src="https://static001.geekbang.org/resource/image/67/07/6775aee46fbb24d7c261bd1fd929c407.png" alt="">

你可以用这个过程，推导一下两个和三个反斜杠的转换过程，这样你就会明白上面报错的原因了。

那在真正使用的时候，有没有更简单的方法呢？答案是有的，我们尽量使用原生字符串，在 Python 中，可以在正则前面加上小写字母 r 来表示。

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall(r'\\', 'a*b+c?\\d123d\\')
['\\', '\\']


```

这样看起来就简单很多，因为少了上面说的第一次转换。

## 正则中元字符的转义

在前面的内容中，我们讲了很多元字符，相信你一定都还记得。如果现在我们要查找比如星号（*）、加号（+）、问号（?）本身，而不是元字符的功能，这时候就需要对其进行转义，直接在前面加上反斜杠就可以了。这个转义就比较简单了，下面是一个示例。

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall('\+', '+')
['+']


```

### 括号的转义

在正则中方括号 [] 和 花括号 {} 只需转义开括号，但圆括号 () 两个都要转义。我在下面给了你一个比较详细的例子。

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall('\(\)\[]\{}', '()[]{}')
['()[]{}']
&gt;&gt;&gt; re.findall('\(\)\[\]\{\}', '()[]{}')  # 方括号和花括号都转义也可以
['()[]{}']

```

在正则中，圆括号通常用于分组，或者将某个部分看成一个整体，如果只转义开括号或闭括号，正则会认为少了另外一半，所以会报错。

括号的转义示例，你可以参考这里：[https://regex101.com/r/kJfvd6/1](https://regex101.com/r/kJfvd6/1)。

### 使用函数消除元字符特殊含义

我们也可以使用编程语言自带的转义函数来实现转义。下面我给出了一个在 Python里转义的例子，你可以看一下。

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.escape('\d')  # 反斜杠和字母d转义
'\\\\d'
&gt;&gt;&gt; re.findall(re.escape('\d'), '\d')
['\\d']

&gt;&gt;&gt; re.escape('[+]')  # 中括号和加号
'\\[\\+\\]'
&gt;&gt;&gt; re.findall(re.escape('[+]'), '[+]')
['[+]']


```

这个转义函数可以将整个文本转义，一般用于转义用户输入的内容，即把这些内容看成普通字符串去匹配，但你还是得好好注意一下，如果使用普通字符串查找能满足要求，就不要使用正则，因为它简单不容易出问题。下面是一些其他编程语言对应的转义函数，供你参考。

<img src="https://static001.geekbang.org/resource/image/a7/0c/a75a4f990d969a046a468e35dbe8ef0c.jpg" alt="">

## 字符组中的转义

讲完了元字符的转义，我们现在来看看字符组中的转义。书写正则的时候，在字符组中，如果有过多的转义会导致代码可读性差。在字符组里只有三种情况需要转义，下面我来给你讲讲具体是哪三种情况。

### 字符组中需要转义的有三种情况

1. 脱字符在中括号中，且在第一个位置需要转义：

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall(r'[^ab]', '^ab')  # 转义前代表&quot;非&quot;
['^']
&gt;&gt;&gt; re.findall(r'[\^ab]', '^ab')  # 转义后代表普通字符
['^', 'a', 'b']

```

1. 中划线在中括号中，且不在首尾位置：

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall(r'[a-c]', 'abc-')  # 中划线在中间，代表&quot;范围&quot;
['a', 'b', 'c']
&gt;&gt;&gt; re.findall(r'[a\-c]', 'abc-')  # 中划线在中间，转义后的
['a', 'c', '-']
&gt;&gt;&gt; re.findall(r'[-ac]', 'abc-')  # 在开头，不需要转义
['a', 'c', '-']
&gt;&gt;&gt; re.findall(r'[ac-]', 'abc-')  # 在结尾，不需要转义
['a', 'c', '-']


```

1. 右括号在中括号中，且不在首位：

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall(r'[]ab]', ']ab')  # 右括号不转义，在首位
[']', 'a', 'b']
&gt;&gt;&gt; re.findall(r'[a]b]', ']ab')  # 右括号不转义，不在首位
[]  # 匹配不上，因为含义是 a后面跟上b]
&gt;&gt;&gt; re.findall(r'[a\]b]', ']ab')  # 转义后代表普通字符
[']', 'a', 'b']

```

### 字符组中其它的元字符

一般来说如果我们要想将元字符（`.*+?`()之类）表示成它字面上本来的意思，是需要对其进行转义的，但如果它们出现在字符组中括号里，可以不转义。这种情况，一般都是单个长度的元字符，比如点号（`.`）、星号（`*`）、加号（`+`）、问号（`?`）、左右圆括号等。它们都不再具有特殊含义，而是代表字符本身。但如果在中括号中出现 \d 或 \w 等符号时，他们还是元字符本身的含义。

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall(r'[.*+?()]', '[.*+?()]')  # 单个长度的元字符 
['.', '*', '+', '?', '(', ')']
&gt;&gt;&gt; re.findall(r'[\d]', 'd12\\')  # \w，\d等在中括号中还是元字符的功能
['1', '2']  # 匹配上了数字，而不是反斜杠\和字母d

```

下面我来给你简单总结一下字符组中的转义情况，我们提到了三种必须转义的情况，其它情况不转义也能正常工作，但在实际操作过程中，如果遇到在中括号中使用这三个字符原本的意思，你可以都进行转义，剩下其它的元字符都不需要转义。

## 总结

好了，今天的内容讲完了，我来带你总结回顾一下。

正则中转义有些情况下会比较复杂，从录入的字符串文本，到最终的正则表达式，**经过了字符串转义和正则转义两个步骤。**元字符的转义一般在前面加反斜杠就行，方括号和花括号的转义一般转义开括号就可以，但圆括号两个都需要转义，我们可以借助编程语言中的转义函数来实现转义。另外我们也讲了字符组中三种需要转义的情况，详细的可以参考下面的脑图。

<img src="https://static001.geekbang.org/resource/image/44/22/442bbc5d31ed8499ff7d37151434e522.png" alt="">

## 思考题

通过今天的学习，不知道你对转义掌握的怎么样了呢？再来一个例子加深一下你的理解吧，文本部分是反斜杠，n，换行，反斜杠四个部分组成。正则部分分别是1到4个反斜杠和字母n，我用Python3写了对应的示例，相应的查找过程是这样子的。

```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall('\n', '\\n\n\\')
['\n']  # 找到了换行符
&gt;&gt;&gt; re.findall('\\n', '\\n\n\\')
['\n']  # 找到了换行符
&gt;&gt;&gt; re.findall('\\\n', '\\n\n\\')
['\n']  # 找到了换行符
&gt;&gt;&gt; re.findall('\\\\n', '\\n\n\\')
['\\n'] # 找到了反斜杠和字母n

```

例子虽然看上去简单，不过你能不能解释出这四个示例中的转义过程呢？

好了，今天的课程就结束了，希望可以帮助到你，也希望你在下方的留言区和我参与讨论，同时欢迎你把这节课分享给你的朋友或者同事，一起交流一下。
