在python 3中以二进制模式打开带有通用换行符的文件

时间:2019-05-30 00:24:24

标签: python python-3.x

(最终)我们正在将应用程序升级到Python 3。

我们必须升级的一件事是使用常规换行符重写CSV文件。

原始(python 2)代码如下:


import csv

IN_PATH = 'in.csv'
OUT_PATH = 'out.csv'

# Opens the original file in 'text mode' (which has no effect on Python 2)
# and with 'universal newlines',
# meaning \r, \n, and \r\n all get treated as line separators.
with open(IN_PATH, 'rU') as in_csv:
    with open(OUT_PATH, 'w') as out_csv:
        csv_reader = csv.reader(in_csv)
        csv_writer = csv.writer(out_csv)

        for tupl in csv_reader:
            csv_writer.writerow(tupl)

这些CSV文件是用户提供的。这意味着:

  • 我们无法控制它们使用的换行符,因此我们需要处理所有这些换行符。
  • 在此过程的此阶段,我们尚不知道文件的编码。

由于我们不知道编码,因此无法将字节串解码为文本。

要在Python 3上实现此功能,首先我们将其更改为使用io.open(),它与py3的open()主要兼容。现在我们不能再使用“文本模式”,因为在Python 3上需要解码字节串,而且我们不知道编码。

但是,使用“二进制模式”意味着我们不能再使用通用换行符,因为它仅在文本模式下可用。


# Opens the original file in 'binary mode'
# (because we don't know the encoding, so we can't decode it)
# FIXME: How to get universal newline support?
with io.open(IN_PATH, 'rb') as in_csv:
    with io.open(OUT_PATH, 'wb') as out_csv:

请注意,尽管python 3不再支持U模式字符,但默认情况下在文本模式下它确实使用通用换行符。似乎没有任何办法可以在二进制模式下使用通用换行符。

我们如何使此代码在Python 3中工作?

3 个答案:

答案 0 :(得分:3)

TLDR:在python3上将ASCII与代理转义一起使用:

def text_open(*args, **kwargs):
    return open(*args, encoding='ascii', errors='surrogateescape', **kwargs)

如果您仅知道部分编码(例如ASCII \r\n),recommended approach将对未知代码点使用代理转义:

  

如果需要更改文件但不知道该怎么办   文件的编码?如果您知道编码是ASCII兼容的,   只想检查或修改ASCII部分,就可以打开文件   使用surrogateescape错误处理程序:

这使用保留的占位符将未知字节嵌入文本流中。例如,字节b'\x99'成为“ unicode”代码点'\udc99'。这适用于读写,可以保留任意嵌入式数据。

公共行尾(\n\r\r\n)均以ASCII定义。因此,将ASCII编码与代理转义一起使用就足够了。

对于兼容性代码,最简单的方法是分别提供不同版本的Python 2和Python 3。 open与大多数用例足够相似,您只需要插入代理转义处理即可。

if sys.version_info[0] == 3:
    def text_open(*args, **kwargs):
        return open(*args, encoding='ascii', errors='surrogateescape', **kwargs)
else:
    text_open = open

这允许在不知道确切编码的情况下使用通用换行符。您可以使用它直接读取或转录文件:

with text_open(IN_PATH, 'rU') as in_csv:
    with text_open(OUT_PATH, 'wU') as out_csv:
        for line in in_csv:
            out_csv.write(line)

如果您需要进一步格式化csv模块,则text_open提供的文本流也足够了。要处理非ASCII分隔符/填充/引号,请将其从字节串转换为适当的代理。

if sys.version_info[0] == 3:
    def surrogate_escape(symbol):
        return symbol.decode(encoding='ascii', errors='surrogateescape')
else:
    surrogate_escape = lambda x: x

Dezimeter = surrogate_escape(b'\xA9\x87')

答案 1 :(得分:1)

我认为在Python 3中没有内置的方法可以完成您想要的操作。不知道编码,您只能确定自己有一堆字节-您不确定哪一个字节它们表示字符\r\n

您的Python 2代码可能正在使用根据sys.getdefaultencoding()的系统默认编码来通知内置的通用换行规范化器(不要引述我,我也没有看过实现),并且如果您系统就像我的,大概是ascii

幸运的是,我认为大多数编码(包括utf-8)仅在其高阶字符(在ascii范围内)的映射上有所不同。因此,对于所有常见编码,使字节10表示\n13表示\r并不是一个可怕的假设-这意味着您可以通过读取来自己进行替换输入逐字节(或者使用滑动的两个字节窗口)。

警告:我尚未详尽测试以下代码的行为,以了解类似\r\r\r之类的重复序列或诸如\n\r之类的怪异事物,因此尽管它可以合理地处理这些问题,但也可能无法。请对您自己的数据进行测试。

from __future__ import print_function

import io
import six  # optional (but hugely helpful for a 2 to 3 port)


def normalize(prev, curr):
    ''' Given current and previous bytes, get tuple of bytes that should be written

    :param prev: The byte just before the read-head
    :type  prev: six.binary_type
    :param curr: The byte at the read-head
    :type  curr: six.binary_type
    :returns   : A tuple containing 0, 1, or 2 bytes that should be written
    :rtype     : Tuple[six.binary_type]
    '''
    R = six.binary_type(b'\r')
    N = six.binary_type(b'\n')
    if   curr == R:         # if we find R, can't dump N yet because it might be start of RN sequence and we must consume N too
        return ()
    elif curr == N:         # if we find N, doesn't matter what previous byte was - dump N
        return (N,)
    elif prev == R:         # we know current not N or R; if previous byte was R - dump N, then the current byte
        return (N, curr)
    else:                   # we know current not N or R and prev not R - dump the current byte
        return (curr,)


if __name__ == '__main__':

    IN_PATH = 'in.csv'
    OUT_PATH = 'out.csv'

    with io.open(IN_PATH, mode='rb') as in_csv:
        with io.open(OUT_PATH, mode='wb') as out_csv:
            prev = None                                 # at start, there is no previous byte
            curr = six.binary_type(in_csv.read(1))      # at start, the current byte is the input file's first byte
            while curr:                                 # loop over all bytes in the input file
                for byte in normalize(prev, curr):      # loop over all bytes returned from the normalizing function
                    print(repr(byte))                   # debugging
                    out_csv.write(byte)                 # write each byte to the output file
                prev = curr                             # update value of previous byte
                curr = six.binary_type(in_csv.read(1))  # update value of current byte

使用我创建的输入文件(使用Python 3),这在Python 2.7.16和3.7.3上都对我有效:

import io

with io.open('in.csv', mode='wb', encoding='latin-1') as fp:
    fp.write('à,b,c\n')
    fp.write('1,2,3\r')
    fp.write('4,5,6\r\n')
    fp.write('7,8,9\r')
    fp.write('10,11,12\n')
    fp.write('13,14,15')

它也可以使用encoding='UTF-8'(应该使用)。

没有必要像我一样使用six.binary_type(),但是我发现它在写交叉版本代码时特别提醒了我正在使用的数据的语义,尤其是。

我花了一段时间试图弄清楚是否有比手动检查所有字节更好的方法,但是没有成功。如果还有其他人找到方法,我有兴趣看到它!

答案 2 :(得分:0)

Python 3中的open函数具有参数newline。将其设置为None会启用通用换行模式。

import csv

IN_PATH = 'in.csv'
OUT_PATH = 'out.csv'

with open(IN_PATH, 'r', newline=None) as in_csv:
    with open(OUT_PATH, 'w') as out_csv:
        csv_reader = csv.reader(in_csv)
        csv_writer = csv.writer(out_csv)

        for tupl in csv_reader:
            csv_writer.writerow(tupl)

示例:

示例文件:

a,b,c\n
1,2,3\r
4,5,6\r\n
7,8,9

示例代码:

with open('file.csv', 'r', newline=None) as fp:
    reader = csv.reader(fp)
    for line in reader:
        print(line)

# prints:
['a', 'b', 'c']
['1', '2', '3']
['4', '5', '6']
['7', '8', '9']
相关问题