Python 编解码器行尾标识符

9

看起来Python的UTF-8编码(codecs包)将Unicode字符28、29和30解释为换行符。为什么?我该如何防止这种情况发生?

示例代码:

with open('unicodetest.txt', 'w') as f:
  f.write('a'+chr(28)+'b'+chr(29)+'c'+chr(30)+'d'+chr(31)+'e')
with open('unicodetest.txt', 'r') as f:
  for i,l in enumerate(f):
    print i, l
# prints "0 abcde" with special characters in between.

这里的重点是它像我预期的那样将其作为一行读取。现在,当我使用codecs以UTF-8格式读取时,它会将其解释为多行。

import codecs
with codecs.open('unicodetest.txt', 'r', 'UTF-8') as f:
  for i,l in enumerate(f):
    print i, l
# 0 a
# 1 b
# 2 c
# 3 de
# (again with the special characters after each a, b, c, d

第28到31个字符按照顺序描述为“信息分隔符四”至“一”。两件事情引起了我的注意:1)28到30被解释为换行符,2)31没有。这是预期的行为吗?哪里可以找到将哪些字符解释为换行符的定义?是否有方法不将它们解释为换行符?
谢谢。
编辑忘记复制codecs.open中的“UTF-8”参数。现在,我问题中的代码已经得到更正。

如果你以 'rb' 模式打开文件会发生什么? - unutbu
没有区别。 - Paul
2
@Paul,如果您愿意,可以回答自己的问题并接受它。 - Alastair McCormack
1个回答

6

这是一个很棒的问题。

使用open()codecs.open()打开文件会有所不同。前者操作字节串,后者操作Unicode字符串。在Python中,它们表现不同

这个问题也在Python Issue 7643, 什么是Unicode换行符?中提出。讨论和对Unicode字符数据库的引用非常有趣。Issue 7643还提供了这个简洁的代码片段来演示差异:

for s in '\x0a\x0d\x1c\x1d\x1e':
  print u'a{}b'.format(s).splitlines(1), 'a{}b'.format(s).splitlines(1)

但归根结底就是这样。

要确定字节字符串中的字节是否为换行符(或空格),Python使用ASCII控制字符的规则。按照这个标准,字节10和13是换行符(Python将字节13后跟10视为单个换行符)。

但是,要确定Unicode字符串中的字符是否为换行符,Python遵循Unicode字符数据库的字符分类规则,该规则在UAX#44中有记录,并且在UAX#14 Line Breaking Algorithm,section 5 Line Breaking Properties中有记录。根据Issue 7643,这些文档识别了三个字符属性,将其识别为Python目的的换行符:

  • 一般类别Zl“行分隔符”
  • 一般类别Zp“段落分隔符”
  • 双向类别B“段落分隔符”
字符28(0x001C),29(0x001D)和30(0x001E)具有这些字符属性。字符31(0x001F)没有。为什么?这是Unicode技术委员会的问题。但在ASCII中,这些字符被称为“文件分隔符”,“组分隔符”,“记录分隔符”和“单元分隔符”。使用制表符文本数据文件作为比较,前三个至少表示与换行符一样的分隔,而第四个可能类似于制表符。
您可以在Objects/unicodeobject.c中查看实际定义这三个Unicode字符作为Python Unicode字符串中换行符的代码。查找数组ascii_linebreak[]。该数组是unicode.splitlines()的实现基础。不同的代码支持str.splitlines()。我认为,但还没有在Python源代码中跟踪它,使用codecs.open()打开的文件上的enumerate()是基于unicode.splitlines()实现的。
您问:“如何防止它这样做?”我没有看到任何使splitlines()行为不同的方法。但是,您可以将文件作为字节流打开,使用str.splitlines()行为读取行,然后将每行解码为UTF-8以用作Unicode字符串:
with open('unicodetest.txt', 'r') as f:
  for i,l in enumerate(f):
    print i, l.decode('UTF-8')
# prints "0 abcde" with special characters in between.

我假设你正在使用Python 2.x版本,而不是3.x版本。我的答案基于Python 2.7。


1
谢谢。这很详细。还有感谢您的解决方案。很有道理。 - Paul

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