为什么Python的原始字符串文字不能以单个反斜杠结尾?

263

从技术上讲,任何奇数个反斜杠,如文档中所述。

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

看起来解析器应该将原始字符串中的反斜杠视为普通字符(这不就是原始字符串的用途吗?),但我可能忽略了一些明显的东西。


13
看起来现在这是一个常见问题解答。当你提出问题时可能还不是。我知道你引用的文档基本上说了同样的事情,但我只是想添加另一份文档来源。 - oob
@oob,那个文档明确解释了它们主要用于正则表达式(不应以反斜杠结尾),而不是 Windows 路径,后者应该这样做。 - Josiah Yoder
另请参阅:Python:SyntaxError:EOL while scanning string literal,了解相关的错误消息和其他常见原因。 - Karl Knechtel
14个回答

179
关于Python原始字符串的整个误解在于,大多数人认为反斜杠(在原始字符串内)只是像其他所有字符一样的常规字符。但实际上不是这样。理解的关键在于这个Python教程序列:
当存在'r'或'R'前缀时,在反斜杠后面的字符将包含在字符串中而没有发生变化,并且所有反斜杠都保留在字符串中。
因此,任何跟随在反斜杠后面的字符都是原始字符串的一部分。一旦解析器进入一个原始字符串(非Unicode字符串)并遇到反斜杠,它就知道有两个字符(一个反斜杠和一个跟随其后的字符)。
这样:
- r'abc\d' 包括 a, b, c, \, d - r'abc\'d' 包括 a, b, c, \, ', d - r'abc\'' 包括 a, b, c, \, ' 还有:
- r'abc\' 包括 a, b, c, \, ' ,但现在没有终止引号。
最后的情况表明,根据文档,解析器现在找不到结束引号,因为您在上面看到的最后一个引号是字符串的一部分,即这里的反斜杠不能是最后一个,因为它将“吞噬”字符串结束字符。

11
我理解了这个机制,但为什么?为什么要这样实现?我不明白它背后的逻辑。上面的解释告诉我们,原始字符串本质上使引号内的所有内容都被视为其本身,但反斜杠不能出现在最后一个字符。那么为什么?这是为了确保它不能被用作文件路径字符串吗? - KH Kim
当我继续阅读页面时,发现它的目的是在字符串中有引号,但为什么我不能只放一个引号而非要在其前面加上反斜杠呢?我想这其中必定有原因,也许与正则表达式有关? - KH Kim
我认为如果与正则表达式无关,那么这是一个设计缺陷,因为还有其他选项可供选择,例如加倍引号,就像在大多数 .csv 文件中使用 "" 代替 "。 x = r"I have ""an apple""" 表示 I have "an apple"。一个问题是 Python 允许类似 a="a""b"a="a" "b" 的情况,导致 a="ab"。因此,要使用加倍引号,Python 需要禁止使用 a="a""b" 这种情况。 - KH Kim
1
我建议再加一个:r'abc\' 包括 a、b、c、\、\。 - Kelly Bundy
这是Python的许多耻辱之一,他们不理解这种误解,因此也无法修复它。如果我们必须写r'c:\\',那完全没问题,但是如果Python创建了这个字符串:"c:\",那当然是一个bug。 - undefined

165
在该部分我标记为粗体的解释了原因:
引号可以通过反斜杠转义,但是反斜杠仍然在字符串中;例如,r"\" " 是一个有效的字符串字面值,由两个字符组成:反斜杠和双引号; r"\" 不是一个有效的字符串字面值(即使原始字符串也不能以奇数个反斜杠结尾)。特别地,原始字符串不能以单个反斜杠结尾(因为反斜杠将转义以下的引号字符)。还要注意,单个反斜杠后跟换行符被解释为字符串的一部分,而不是行继续符。
因此,原始字符串并非完全原始,仍具有某些基本的反斜杠处理。

24
哇哦...这很奇怪。发现得好。r''' == "\'"是有意义的,但转义字符没有消失还是很奇怪的。 - cdleary
4
@ihightower 这可能适用于文件系统路径,但反斜杠还有其他用途。对于文件系统路径,请勿硬编码分隔符。使用'os.path.sep',或更好的是使用'os.path'的高级功能。(或者在可用时使用'pathlib') - oefe
13
注意:解决方法是使用相邻的文字拼接。r"foo\bar\baz" "\\"(如果含糊不清,请将其括在括号中)将在编译时创建一个单一的文字,其中第一部分是原始的,只有最后一小部分是非原始的,以允许尾随反斜杠。 - ShadowRanger
6
这段话的意思是,这个问题只是重复了问题本身(什么是允许的/有效的,什么是不允许的/无效的),而没有解释为什么会设计成这样。有一篇FAQ条目在某种程度上解释了为什么(原始字符串是为了特定的目的而设计的,在那个目的的语境下这样做是有意义的)。 - ShreevatsaR
11
那么原始字符串的意义是什么呢?看起来这是一个模糊的实现概念。 - Matthew James Briggs
显示剩余4条评论

37

就是这样!我认为这是 Python 中的小缺陷之一!

我不认为有什么很好的理由,但它绝对不是解析问题;使用 \ 作为最后一个字符来解析原始字符串非常容易。

问题在于,如果你允许 \ 作为原始字符串的最后一个字符,那么你将无法在原始字符串中放置 "。Python 选择了允许 " 而不是 \ 作为最后一个字符。

然而,这不应该造成任何麻烦。

如果你担心无法轻松编写像 c:\mypath\ 这样的 Windows 文件夹路径,那么不用担心,你可以使用 r"C:\mypath" 来表示它们,而如果你需要附加子目录名称,请不要使用字符串连接,因为这也不是正确的方式!使用 os.path.join

>>> import os
>>> os.path.join(r"C:\mypath", "subfolder")
'C:\\mypath\\subfolder'

3
不错的附属材料。 :-) 然而,有时你想要通过添加路径分隔符来区分文件路径和目录路径。os.path.join的好处在于它可以合并它们:assert os.path.join('/home/cdleary/', 'foo/', 'bar/') == '/home/cdleary/foo/bar/' - cdleary
3
是的,这只是为了告诉阅读代码的人你期望路径是一个目录还是一个文件。 - cdleary
7
或者您可以将它们表示为"c:/mypath",完全忘记反斜杠的问题 :-) - John Fouhy
1
当然,Python开发人员没有听说过以\?\开头的Windows UNC路径,而os.path.join也不支持它。 - Calmarius
1
问题在于,如果您允许\成为原始字符串中的最后一个字符,那么您将无法在原始字符串中放置“。似乎Python选择允许”而不是允许\作为最后一个字符。这并不是真正的一对一交换;如此,如果您想要原始字符串包含引号字符,则仍然必须包括文字反斜杠在引号之前。 - Karl Knechtel
显示剩余6条评论

32
为了使一个原始字符串以斜杠结尾,我建议您可以使用以下技巧:
>>> print r"c:\test"'\\'
test\

它使用了 Python 中字符串字面量的隐式连接,并将双引号括起来的一个字符串与由单引号括起来的另一个字符串连接起来。很丑,但是可行。


17

另一个小技巧是使用chr(92),因为它会被解析为 "\"。

我最近需要清理一段反斜杠字符串,以下代码解决了这个问题:

CleanString = DirtyString.replace(chr(92),'')

我意识到这并没有解决“为什么”的问题,但该帖子吸引了许多人寻找即时问题的解决方案。


但是如果原始字符串包含反斜杠呢? - Joseph Redfern
9
chr(92)相当晦涩难懂,最好使用"\\"(带反斜杠的非原始字符串)。 - clemep
关于如何创建一个只包含单个反斜杠的字符串,可以参考 https://dev59.com/cGIk5IYBdhLWcg3wn_j1。 - Karl Knechtel

9

由于在原始字符串中允许使用 \",因此它不能用于标识字符串文字的结尾。

为什么不在遇到第一个 " 时停止解析字符串文字?

如果是这样的话,那么 \" 就不能在字符串文字中使用。但实际上是可以使用的。


1
没错。Python 设计者可能评估了两种选择的可能性:在双引号原始字符串中的任何位置使用两个字符序列 \",或在双引号原始字符串末尾使用 \。使用统计数据必须支持任何位置的两个字符序列,而不是末尾的一个字符序列。 - hobs

4
r"'"

Instead of:

r'\'

原因为什么r'\'是语法错误的是因为虽然字符串表达式是原始的,但使用的引号(单引号或双引号)总是需要转义,否则它们将标记引号的结尾。因此,如果您想在单引号字符串中表示单引号,则除了使用\'之外没有其他方法。双引号也同样适用。

但是您可以使用:

r"'"

而不是:

r'\'
'\\'

2

另一位已删除回答的用户(不确定是否希望被认可)建议,Python语言设计者可以通过使用相同的解析规则并将转义字符扩展为原始形式来简化解析器设计(如果文本被标记为原始文本,则可以事后处理)。

我认为这是一个有趣的想法,并将其包含在社区wiki中以供后人参考。


但这可能会让您避免拥有两个单独的字符串文字解析器代码路径。 - cdleary

2
鉴于对Python原始字符串末尾奇数个反斜杠的任意限制引起的困惑,可以说这是一个设计错误或遗留问题,起源于希望拥有更简单的解析器。
虽然可以通过一些变通方法(例如r'C:\some\path' '\\')得到(在Python表示法中)'C:\\some\\path\\'或(直接显示)C:\some\path\),但需要这样做并不直观。为了比较,让我们来看看C++和Perl。
在C++中,我们可以直接使用原始字符串文字语法。
#include <iostream>

int main() {
    std::cout << R"(Hello World!)" << std::endl;
    std::cout << R"(Hello World!\)" << std::endl;
    std::cout << R"(Hello World!\\)" << std::endl;
    std::cout << R"(Hello World!\\\)" << std::endl;
}

获取以下输出:
Hello World!
Hello World!\
Hello World!\\
Hello World!\\\

如果我们想在字符串字面值中使用闭合分隔符(上面的),我们甚至可以以临时的方式扩展语法为R"delimiterString(quotedMaterial)delimiterString"。例如,R"asdf(some random delimiters: ( } [ ] { ) < > just for fun)asdf"会在输出中生成字符串some random delimiters: ( } [ ] { ) < > just for fun。(这不是一个很好的使用"asdf"吗!)
在Perl中,这段代码
my $str = q{This is a test.\\};
print ($str);
print ("This is another test.\n");

将输出以下内容:这是一个测试。\这是另一个测试。

将第一行替换为

my $str = q{This is a test.\};

会导致错误信息:在main.pl的第1行找不到字符串终止符"}"。

然而,Perl将预定界符\视为转义字符,并不会阻止用户在结果字符串末尾拥有奇数个反斜杠;例如,要在$str的末尾放置3个反斜杠\\\,只需以6个反斜杠结尾代码:my $str = q{This is a test.\\\\\\};。重要的是,在输入中我们需要将反斜杠加倍,但没有类似Python的看起来不一致的语法限制。


另一种看待这个问题的方式是,这三种语言使用不同的方法来处理转义字符和闭合分隔符之间的解析问题:
- Python:禁止在闭合分隔符之前有奇数个反斜杠;一个简单的解决方法是 `r'stringWithoutFinalBackslash' '\\'`。 - C++:允许在分隔符之间使用几乎任何内容。 - Perl:允许在分隔符之间使用几乎任何内容,但需要将反斜杠连续重复两次。
¹ 自定义的 `delimiterString` 本身不能超过16个字符长,但这几乎不是一个限制。
² 如果你需要分隔符本身,只需用 `\` 进行转义。
然而,公平地与Python进行比较,我们需要承认:(1)C++直到C++11才有这样的字符串字面量,并且以其难以解析而闻名;(2)Perl更难解析。

1

原始字符串

原始字符串的朴素想法是:

如果我在一对引号前面加上 r,那么我可以在引号之间放任何东西,并且它将意味着它本身。

不幸的是,这并不起作用,因为如果任何东西包含引号,原始字符串将在那个点结束。

很显然,我无法在固定的分隔符之间放置“任何我想要的内容”,因为其中一些可能看起来像终止分隔符,无论该分隔符是什么。

实际应用的原始字符串(变体1)

解决这个问题的一个可能方法是:

如果我在一对引号前面加上 r,那么我可以在引号之间放任何东西,只要它不包含引号,并且它将意味着它本身。

这个限制听起来很苛刻,但是一旦认识到 Python 提供了大量的引号,就可以通过这个规则适应大多数情况。以下都是有效的 Python 引号:

'
"
'''
"""

使用这么多不同的定界符,几乎任何字符都可以正常工作。唯一的例外是,如果字符串字面值应该包含所有允许的Python引用的完整列表。

现实世界中的原始字符串(变体2,如Python所示)

然而,Python采取了一种不同的方法,使用了上述规则的一个扩展版本。它有效地声明:

如果我在一对引号前面加上r,我可以在引号之间放任何我想要的东西,只要它不包含引号,它就会意味着它本身。即使我坚持要包含引号,那也是被允许的,但是我必须在引号前面加上反斜杠。

因此,Python方法从某种意义上说比上面的变体1更自由 - 但它会导致将闭合引号“错误”解释为字符串的一部分,如果最后一个目的字符是反斜杠。

变体2没有帮助:

  • 如果我想在字符串中包含引号,但不包含反斜杠,则允许的字符串字面量将不是我所需要的。但是,鉴于我可以使用三种不同的引号,我可能会选择其中一种,这样我的问题就解决了 - 所以这不是一个问题。
  • 问题的情况是这样的:如果我希望字符串以反斜杠结尾,则束手无策。我需要将包含反斜杠的非原始字符串字面量连接起来。

结论

写完这篇文章后,我认同其他几位发布者的观点,即变体1更容易理解和接受,因此更具有Python风格。这就是生活!


我同意你的分析,但它并没有真正回答这个问题 - 请记住这不是一个讨论论坛。 - Karl Knechtel
对我来说,这是关于这个主题迄今为止最好的答案。非常清晰明了。干得好。 - Crapicus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接