全面了解 Python 中的反斜杆

本文全面介绍了 Python 中反斜杆(\)的用法,包括原始字符串和普通字符串,repr()str() ,\ 作为转义符,\ 作为续行符,\ 在字符串转义和正则表达式转义中的过程及注意事项等。阅读本文预计 6 min.

1. 前言

反斜杆 \ 看似非常普通简单,实际上并不简单。以前,自以为掌握了 \ 的用法,近日发现自己并没有真正掌握。即便现在,也不敢说自己真的彻底掌握了 \

意识到这个问题源于自己最近在极客时间上学习 《正则表达式入门课》 专栏中 06 | 转义:正则中转义需要注意哪些问题? 一篇,最后问了一个思考题:

文本部分是反斜杠,n,换行,反斜杠四个部分组成。正则部分分别是 1 到 4 个反斜杠和字母 n。例子虽然看上去简单,不过你能不能解释出这四个示例中的转义过程呢?

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

我学习这篇文章时,发现 \ 很多行为跟自己预期不一样,所以就是不断地去测试,去翻阅文档,最后也就有了这篇文章。

本文的目的就是总结自己所看的关于 \ 的资料,以及自己测试的过程及个人心得,帮助自己加深对于 \ 的理解。如有不当之处,还请各位读者斧正~

PS:《正则表达式入门课》 专栏讲解的很棒,如果能配合《精通正则表达式》中英文版看的话,更香!

2. Python 中的 \

Python 中 \ 主要有以下作用:

  1. \ 作为转义符。包括表示转义序列、转义转义字符本身,使得转义序列失效。
  2. 作为续行符。

下面我们一一来看。

2.1 \ 作为转义符

在 Python 字符串中,\ 后可以跟各种字符,以表示各种转义序列(escape sequence)。所以通常情况下,Python 解释器会把 \ 当做转义符,把 \ 及其后面的字符当做转义序列处理。如 \n 表示换行符,\t 表示制表符,看下面这个例子:

>>> print('a\nb\tc')
a
b       c

说明:这里 Python 字符串是 a\nb\tc,经过 Python 解释器转义后,\n 会被转义为换行符,\t 会被转义为制表符,所以才有了以上输出。
以上这个例子很好的说明了 Python 解释器是如何把 \ 作为转义字符。

Python 中的转义序列和 C 标准基本一致,有关 Python 转义序列的列表可以参看官方文档 String and Bytes literals

假如我们就想要字符串中表示 \ 本身,应该怎么做呢?
答案是用 \\,即两个反斜杆,转义转义字符,这样就是表示普通的字符 \ 。测试如下:

>>> print('\\')
\

同理,如果我们想让转义序列失效,只需要在转义序列前面多加一个 \ 即可,如 \n 表示换行符,\\n 表示字符 \ 和 字符 n 两个字符。测试如下:

>>> print('\n')


>>> len('\n')
1
>>> print('\\n')
\n
>>> len('\\n')
2

可以看到 \n 是 1 个字符,\\n 是 2 个字符。

至此,我们可以小结一下:

Python 解释器会把字符串中的 \ 当做转义符处理,包括表示转义序列、转义转义字符本身,使得转义序列失效。Python 解释器把 \ 及其后面的字符当做转义序列对待。如果我们想表示 \ 本身,那么需要用 \\。如果我们想让转义序列失效,那么就在原有的转义序列前多加一个 \,如:\\n

PS:转义符设计的目的是为了表示键盘难以键入或者难以表示的信息,如:用 \n 去表示换行。

2.2 \ 作为续行符

在 Python 中 \ 还有一个用法就是当做续行符用。我们知道一行代码太长,不利于阅读。那么如何实现把 1 行代码分割,分散到多行呢?Python 中 \ 可以实现这个功能。

看下面这个小例子:

>>> url = 'https://time.geekbang.org/column/article/\
... 252887?utm_source=geektime&utm_medium=pc&utm_campaign=265\
... &utm_term=pc_interstitial_372'
>>> print(url)
https://time.geekbang.org/column/article/252887?utm_source=geektime&utm_medium=pc&utm_campaign=265&utm_term=pc_interstitial_372

说明:这里 \ 被当做了续行符,用来把多行代码显式拼接成一行代码。通常用于一行代码过长,需要分割到多行的场景。

这里需要注意,\ 作为续行符时,\ 后面不能有任何字符。否则报 SyntaxError: unexpected character after line continuation character
如:

>>> print('hello'\)
  File "<stdin>", line 1
    print('hello'\)
                  ^
SyntaxError: unexpected character after line continuation character
>>> print('hello'\
... )
hello

2.3 普通字符串 VS 原生字符串

我们通常想写个 \,却要写成 \\,似乎有点麻烦,那有没有什么办法使得 Python 解释器不把 \ 当做转义符呢?

答案就是使用原生字符串(raw string),原生字符串很简单,就是在普通字符串前面加上前缀 rR 即可。如:想打印 \n 两个字符,使用普通字符串需要写成 print('\\n'),而使用原生字符串只需要写成 print(r'\n'),测试如下:

>>> print('\\n')
\n
>>> print(r'\n')
\n
>>> print(R'\n')
\n
>>> len('\\n')
2
>>> len(r'\n')
2

通过上面的例子,可以非常清楚的看到,原生字符串就是使得 \ 转义失效。有点像,加了 rR 前缀就相当于告诉 Python 解释器,这是原生字符串,里面的 \ 不用转义。

有些时候,原生字符串很有用,可以省去我们重复键入 \,如:

>>> print("this\text\is\not\what\it\seems")
this    ext\is
ot\what\it\seems
>>> print("this\\text\is\what\you\\need")
this\text\is\what\you\need
>>> print(r"this\text\is\not\what\it\seems")
this\text\is\not\what\it\seems

看到这里,你是不是觉得,\ 很简单,你列的这些我都会,你这篇文章没啥特别的?别急,继续往下面看。

2.4 Python 中 \ 一些让人头疼的细节

接下来这部分,就是我之前不太清楚或者说没有关注到的部分。

  1. 如果 Python 普通字符串中 \ 后跟的字符不是合法的转义序列,如:\d,Python 会怎么处理?
  2. 为什么我输入的是 '\d',输出的是 '\\d'
  3. Python 普通字符串和原生字符串末尾如果是奇数个 \,会发生什么?
  4. 原生字符串中还可以转义吗?

这部分将围绕这 4 个问题展开。
先看第一个问题,看下面的例子:

>>> s = '\d'
>>> s
'\\d'
>>> print(s)
\d
>>> s = '\\d'
>>> s
'\\d'
>>> print(s)
\d
>>> s = '\\\d'
>>> s
'\\\\d'
>>> print(s)
\\d

为什么 s = '\d' 时, s 得到的结果是 \\d,而 print(s) 得到的结果是 \d?下面的几个又怎么解释?你先自己思考下,看看能想明白每一步如何转义的不?

要解释上面的疑惑,就必须弄明白:

  1. 如果 Python 普通字符串中 \ 后跟的字符是非法的转义序列,如:\d,Python 会怎么处理?
  2. 为什么我输入的是 '\d',输出的是 '\\d'

首先,第 1 个问题,如果 Python 普通字符串中 \ 后跟的字符不是合法的转义序列,如:\d
答案:目前 Python 的做法是把 \ 保留在普通字符串中。 s = '\d 因为 \d 不是转义序列,所以 Python 解释器会把 \d 当做字符 \ 和字符 d 两个字符处理。

这与 C 标准是不一样的,C 语言中,如果 \ 和后面的字符无法构成合法转义序列,则 \ 会被忽略掉。测试如下:

Python 中测试

>>> print('\d')
\d
>>> len('\d')
2
>>> for i in '\d':
...     print(i)
...
\
d

C 语言中 \ 转义测试:

# include <stdio.h>

int main(void)
{
    printf("\d");

    return 0;
}

结果输出:
c_test.c: In function 'main':
c_test.c:5:12: warning: unknown escape sequence: '\d'
     printf("\d");
            ^~~~
d

注意:Python 官方文档说了,Python 3.6 开始,无法识别的转义序列将产生 DeprecationWarning,最终会变为 SyntaxError,原文如下:

Changed in version 3.6: Unrecognized escape sequences produce a DeprecationWarning. In a future Python version they will be a SyntaxWarning and eventually a SyntaxError.

**所以,如果你想表示 \ 本身,就用 \\,不要用 \d 来表示 \d,因为以后这样做是非法的!

接着我们看第 2 个问题,为什么我输入的是 '\d',输出的是 '\\d'
答案:在 Python 中有两个内置函数 str()repr(),它们分别对应 __str__()__repr__() 魔法方法。简单地说,str() 是对用户友好,repr() 是对编程者或者机器友好。看着有点绕,为了便于理解和说明,我们用原生字符串说明:

>>> r'\d'
'\\d'
>>> r'\\d'
'\\\\d'
>>>

我们可以看到,每个字符 \,在电脑中实际上是 \\,因为 \\ 才表示 \ 本身。这里我们只要明白这一点就可以了。

那么我们如何能够看到,\ 转移后的实际字符串呢?
通过用 print() 函数我们可以看到转移后的实际字符串:

>>> print('\\d')
\d
>>> print('\\\\d')
\\d

这个地方提到这个,只是为了说明一下内容:

>>> s = '\d'
>>> s
'\\d'
>>> print(s)
\d

因为 \d 不是合法的转义序列,故无法转义,所以 s 实际上是 \ 字符和 d 字符组成的字符串。所以输入 s 才得到了 \\d 这个对机器友好的输出,实际上它转义后就是代表 \d。而 print() 的结果是转义后的实际字符串,所以输出是 \d 2 个字符。

>>> s = '\\d'
>>> s
'\\d'
>>> print(s)
\d

这个输出的解释是: s = '\\d',首先 \\d 会被 Python 解释器转义为 \d 并赋值给变量 s,所以 s 实际上也是 \ 字符和 d 字符组成的字符串,剩下的同上解释。

>>> s = '\\\d'
>>> s
'\\\\d'
>>> print(s)
\\d

这个输出的解释是:s = '\\\d',首先 \\\d 会被 Python 解释器转义为 \\d 并赋值给变量 s,实际上是 \ 字符、\ 字符和 d 字符这 3 个字符组成的字符串。对机器友好时,每个 \ 会作为 \\ 输出,所以得到了 \\\\d,print(s)对用户友好,得到的就是 \\d

更多关于 __str__()__repr__() 的区别,可以参考这里:Difference between __str__ and __repr__?

写的有点啰嗦,可能也有点难懂,多看几遍,自己多练习练习,可以帮助我们更好的理解。

接下来我们解决剩下的 2 个问题。

  1. Python 普通字符串和原生字符串末尾如果是奇数个 \,会发生什么?
  2. 原生字符串中还可以转义吗?

针对问题 3,如果 Python 普通字符串和原生字符串末尾如果是奇数个 \,会发生什么?
答案: Python 解释器会报 EOL 语法错误。如下:

>>> print('hello\')
  File "<stdin>", line 1
    print('hello\')
                  ^
SyntaxError: EOL while scanning string literal
>>> print(r'hello\')
  File "<stdin>", line 1
    print(r'hello\')
                   ^
SyntaxError: EOL while scanning string literal

原因很简单,因为 Python 中字符串是以引号开始,引号结束的,奇数个 \ 结尾时,\ 会把后面的引号转义,使得引号不再是字符串结束的标识。字符缺乏了闭合引号,所以报错:EOL
我们可以这样测试一下:

>>> print('hello\'')
hello'
>>> print(r'hello\'')
hello\'

因此不管是普通字符串还是原生字符串都不能以奇数个 \ 结尾,否则会报 EOL 错误。

针对问题 4:原生字符串中还可以转义吗?
答案:可以“转义”,但是此时的“转义”有些特别,因为“转义”后 \ 还会保留在字符串中。其实我们上面已经用到了,原生字符串中,我们可以用 \ 转义引号,\等,但是 \ 依旧会保留在字符串中,如:

>>> print(r'\\')
\\
>>> len(r'\\')
2
>>> len(r'a\'b')
4
>>> print(r'a\'b')
a\'b

以上就是我总结的 Python 解释器对于普通字符串和原生字符中 \ 的处理。

这里小结一下:

  1. \ 作为转义符。包括表示转义序列、转义转义字符本身,使得转义序列失效。
  2. 作为续行符。
  3. 目前 Python 中对于无法识别的转义序列,会保留 \ 在字符串中,C 语言中则是会忽略 \。从 Python 3.6 开始,未来,Python 中无法识别的转义序列将报语法错误。因此要表示 \ 本身,请使用 \\
  4. 不管是普通字符串还是原生字符串都不能以奇数个 \ 结尾,否则会报 EOL 错误。
  5. 原生字符串中也可以“转义”,但是此时的“转义”有些特别,因为“转义”后 \ 还会保留在字符串中。

有了前面这么多的铺垫,下面我们可以进入到正则表达式中的转义了。

3. 正则表达式字符串中的 \

正则表达式字符串中的 \ 和 Python 字符串中的 \ 转义类似。他们的区别在于,正则表达式字符串的转义有 2 个水平,第 1 次是 Python 解释器层面的转义,第 2 次是 re 模块正则引擎的转义。

因此正则中要匹配 \ 本身,正则表达式字符串需要写成 '\\\\',如:

>>> re.findall('\\\\', 'a\\b')
['\\']  # 这里的输出结果是对机器友好,下面测试可知
>>> result = re.findall('\\\\', 'a\\b')
>>> print(result[0])
\

上面正则表达式字符串是 '\\\\',第 1 轮是 Python 解释器转义,转义后结果是 \\,再经过 re 正则引擎的转义,转移后结果是 \,所以正则表达式字符串 '\\\\' 最终匹配的是 \ 本身。

是不是觉得这样非常麻烦,而且容易出错,因为有 2 次转义。因此,在写正则表达式字符串时,使用原生字符串是非常推荐的写法!因为这样可以减少 Python 解释器转义这一步,只需要 re 模块正则引擎转义,即可。比如,我们想匹配 \ 本身,那么我们用原生正则表达式字符串就可以写成 r'\\'。简洁多了,测试一下:

>>> re.findall(r'\\', 'a\\b')
['\\']

完美!

使用原生字符串来写正则表达式还有一个好处:因为 Python 字符串转义和正则转义的转义序列是有区别的,比如:\d 在 Python 字符串中是表示 \d 两个字符,而不是转义序列,未来这么写会报错;但是 \d 在正则转移中,是一个转义序列,表示 0-9 任意一个数字。

因此,强烈建议使用原生字符串来写正则,不仅简洁,而且不存在风险!

文章到这里,基本明晰了。最后我们来解决开篇的那个问题吧:

文本部分是反斜杠,n,换行,反斜杠四个部分组成。正则部分分别是 1 到 4 个反斜杠和字母 n。例子虽然看上去简单,不过你能不能解释出这四个示例中的转义过程呢?

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

首先再次声明,强烈推荐正则表达式用原生字符串写。这里这么写只是为了帮助理解 \ 转义符,以及 \ 在正则表达式字符串中的 2 次转义。这里我列了一个表,来展示上面的转义过程:

初始正则表达式字符串Python 解释器转义后结果re 模块正则引擎转义后结果(即匹配的文本)
'\n'换行符换行符
'\\n''\n'换行符
'\\\n''\换行符'换行符
'\\\\n''\\n'\n\ 字符或 d 字符

里面第 3 个正则一开始我还不太明白,'\换行符' 经过正则引擎转义变成了 换行符,后面发现这个,就把它这样记住就好了,一般不会这么写的!把它和以下对比:

>>> re.findall('\\','\\')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  省略...
re.error: bad escape (end of pattern) at position 0

在上面这个例子中,第 1 步 Python 解释器转义肯定没有问题,正则表达式字符串经过第 1 步 Python 解释器转义的结果是, '\'

问题出在第 2 步,re 模块正则引擎转义报错。因为这里经过第 1 步转义后,只剩下一个 '\',正则引擎会尝试把它当做转义符,但是发现后面少了字符,是一个非常的转义序列,所以报错 bad escape

我们多测试几个看看:

>>> re.findall(r'\h','\\h')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  省略...
    raise source.error("bad escape %s" % escape, len(escape))
re.error: bad escape \h at position 0
>>>

这里我用了原生字符串写法,省去了 Python 解释器转义一步,发现,最后报错是 bad escape \h at position 0,也就是说, \h 是非法的转义序列。

因此我们得出结果:在正则引擎转义中,如果 \ 后面跟着非法的转义序列,将报错 bad escape

所以综上,我们写正则表达式时,强烈建议用 r 前缀,能够避免很多不必要的麻烦和难以预测的 Bug。

4. 总结

  1. \ 作为转义符。包括表示转义序列、转义转义字符本身,使得转义序列失效。
  2. \ 作为续行符。
  3. 目前 Python 中对于非法的转义序列,会保留 \ 在字符串中,C 语言中则是会忽略 \。从 Python 3.6 开始,未来,Python 中无法识别的转义序列将报语法错误。因此要表示 \ 本身,请使用 \\
  4. 不管是普通字符串还是原生字符串都不能以奇数个 \ 结尾,否则会报 EOL 错误。
  5. 原生字符串中也可以“转义”,但是此时的“转义”有些特别,因为“转义”后 \ 还会保留在字符串中。
  6. 正则表达式字符串的转义有 2 个水平,第 1 次是 Python 解释器层面的转义,第 2 次是 re 模块正则引擎的转义。
  7. 强烈建议用 r 前缀写正则表达式,省去 Python 解释器的转义。
  8. re 模块正则引擎对于非法的转义序列直接报错 bad escape

知识无穷无尽,点滴总结,聚沙成塔。以上就是分享的全部内容,如果不对之处,恳请斧正~

本文首发于本人公众号 No Bug编程笔记,如有转载请注明出处和作者,谢谢~

5. 巨人的肩膀

知识的学习建立在前人的基础之上,本文的学习总结,来源于以下资料:

  1. re — Regular expression operations
  2. The Backslash Plague
  3. Confused about backslashes in regular expressions
  4. Why do backslashes appear twice?
  5. Can’t escape the backslash with regex?
  6. String and Bytes literals
  7. Difference between __str__ and __repr__?

后记:
我从本硕药学零基础转行计算机,自学路上,走过很多弯路,也庆幸自己喜欢记笔记,把知识点进行总结,帮助自己成功实现转行。
2020下半年进入职场,深感自己的不足,所以2021年给自己定了个计划,每日学一技,日积月累,厚积薄发。
如果你想和我一起交流学习,欢迎大家关注我的微信公众号每日学一技,扫描下方二维码或者搜索每日学一技关注。
这个公众号主要是分享和记录自己每日的技术学习,不定期整理子类分享,主要涉及 C – > Python – > Java,计算机基础知识,机器学习,职场技能等,简单说就是一句话,成长的见证!
每日学一技

  • 30
    点赞
  • 84
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值