【问题标题】:Decoding a Python 2 `tempfile` with python-future使用 python-future 解码 Python 2 `tempfile`
【发布时间】:2016-01-16 03:08:22
【问题描述】:

我正在尝试编写一个与 Python 2/3 兼容的例程来获取 CSV 文件,将其从 latin_1 解码为 Unicode,并以稳健、可扩展的方式将其提供给 csv.DictReader

  • 为了支持 Python 2/3,我使用 python-future,包括从 builtins 导入 open,并导入 unicode_literals 以实现一致的行为
  • 我希望通过使用tempfile.SpooledTemporaryFile 溢出到磁盘来处理异常大的文件
  • 我使用io.TextIOWrapper 处理从latin_1 编码解码,然后再输入DictReader

这在 Python 3 下一切正常。

问题是TextIOWrapper 期望包装一个符合BufferedIOBase 的流。不幸的是,在 Python 2 下,虽然我已经导入了 Python 3 风格的 open,但原版 Python 2 tempfile.SpooledTemporaryFile 当然仍然返回 Python 2 cStringIO.StringO,而不是 TextIOWrapper 要求的 Python 3 io.BytesIO .

我能想到这些可能的方法:

  1. 将 Python 2 cStringIO.StringO 包装为 Python 3 样式的 io.BytesIO。我不确定如何解决这个问题 - 我需要编写这样的包装器还是已经存在?
  2. 找到一个 Python 2 替代方案来包装 cStringIO.StringO 流进行解码。我还没有找到。
  3. 取消SpooledTemporaryFile,完全在内存中解码。 CSV 文件需要多大才能完全在内存中运行才能成为一个问题?
  4. 取消SpooledTemporaryFile,并实现我自己的溢出到磁盘。这将允许我从 python-future 调用 open,但我宁愿不这样做,因为它会非常乏味并且可能不太安全。

最好的前进方式是什么?我错过了什么吗?


进口:

from __future__ import (absolute_import, division,
                    print_function, unicode_literals)
from builtins import (ascii, bytes, chr, dict, filter, hex, input,  # noqa
                  int, map, next, oct, open, pow, range, round,  # noqa
                  str, super, zip)  # noqa
import csv
import tempfile
from io import TextIOWrapper
import requests

初始化:

...
self._session = requests.Session()
...

例行公事:

def _fetch_csv(self, path):
    raw_file = tempfile.SpooledTemporaryFile(
        max_size=self._config.get('spool_size')
    )
    csv_r = self._session.get(self.url + path)
    for chunk in csv_r.iter_content():
        raw_file.write(chunk)
    raw_file.seek(0)
    text_file = TextIOWrapper(raw_file._file, encoding='latin_1')
    return csv.DictReader(text_file)

错误:

...in _fetch_csv
    text_file = TextIOWrapper(raw_file._file, encoding='utf-8')
AttributeError: 'cStringIO.StringO' object has no attribute 'readable'

【问题讨论】:

  • 我遇到了类似的问题,没有找到比为 Python 2 和 3 编写单独的代码然后检测您正在运行的版本更好的解决方案。
  • @cbare 是的,我怀疑它可能归结为这一点。您有机会发布您的解决方案吗?

标签: python python-2.7 python-3.x character-encoding temporary-files


【解决方案1】:

不确定这是否有用。这种情况与你的情况只是模糊的相似。

我想使用NamedTemporaryFile 创建一个以 UTF-8 编码的 CSV,并具有操作系统本机行结尾,可能不完全是standard,但使用 Python 3 样式 io.open 很容易适应。

难点在于 Python 2 中的 NamedTemporaryFile 打开一个字节流,导致problems with line endings。我确定的解决方案是创建临时文件,然后关闭它并使用 io.open 重新打开,我认为它比 Python 2 和 3 的单独案例要好一些。最后一部分是出色的 backports.csv 库,它在 Python 2 中提供了 Python 3 风格的 CSV 处理。

from __future__ import absolute_import, division, print_function, unicode_literals
from builtins import str
import csv, tempfile, io, os
from backports import csv

data = [["1", "1", "John Coltrane",  1926],
        ["2", "1", "Miles Davis",    1926],
        ["3", "1", "Bill Evans",     1929],
        ["4", "1", "Paul Chambers",  1935],
        ["5", "1", "Scott LaFaro",   1936],
        ["6", "1", "Sonny Rollins",  1930],
        ["7", "1", "Kenny Burrel",   1931]]

## create CSV file
with tempfile.NamedTemporaryFile(delete=False) as temp:
    filename = temp.name

with io.open(filename, mode='w', encoding="utf-8", newline='') as temp:
    writer = csv.writer(temp, quoting=csv.QUOTE_NONNUMERIC, lineterminator=str(os.linesep))
    headers = ['X', 'Y', 'Name', 'Born']
    writer.writerow(headers)
    for row in data:
        print(row)
        writer.writerow(row)

【讨论】:

  • 此方法实际上使所有NamedTemporaryFile 的检查无效,这些检查旨在确保没有其他人声称该文件名。您还可以使用tempfile.mktemp(),其文档字符串指出“不应使用此函数”。一个可能更好的方法是通过 fileno 重新打开文件,而不关闭它。一旦我测试了一下,我会把它作为一个单独的答案发布。
  • @MarSoft 我不确定我是否理解。 NamedTemporaryFile 是否为您提供除唯一命名文件之外的任何保证?在文件关闭之前您是否拥有独占访问权限,然后您会丢失?我很想知道我是否对上述方法提出了问题。
  • 您可以在文档中查找tempfile.mktemp(),它会为临时文件生成一个名称,但不会创建该文件。该功能已被弃用,因为它存在安全风险:This function is unsafe and should not be used. The file name refers to a file that did not exist at some point, but by the time you get around to creating it, someone else may have beaten you to the punch. 我现在将在单独的答案中添加更多详细信息。
  • UPD:刚刚注意到您指示NamedTemporaryFile 不要删除该文件。那么你可能没有那种安全风险。尽管在某些平台(如nt)上可能会在其他用户的文件不再打开时删除它..
【解决方案2】:

@cbare 的方法可能应该避免。它确实有效,但它会发生以下情况:

  1. 我们使用tempfile.NamedTemporaryFile() 来创建临时文件。然后我们记住它的名字。
  2. 我们留下with 声明并且该文件已关闭。
  3. 现在文件已关闭(但未删除),我们再次打开它并使用io.open() 打开它。

乍一看还不错,第二眼看起来也不错。但我不确定在某些平台上(如nt)是否可以在未打开时删除其他用户的文件 - 然后再次创建它但可以访问其内容。如果这是不可能的,请有人纠正我。

以下是我的建议:

# Create temporary file
with tempfile.NamedTemporaryFile() as tf_oldstyle:
    # get its file descriptor - note that it will also work with tempfile.TemporaryFile
    # which has no meaningful name at all
    fd = tf_oldstyle.fileno()
    # open that fd with io.open, using desired mode (could use binary mode or whatever)
    tf = io.open(fd, 'w+', encoding='utf-8', newline='')
    # note we don't use a with statement here, because this fd will be closed once we leave the outer with block
    # now work with the tf
    writer = csv.writer(tf, ...)
    writer.writerow(...)

# At this point, fd is closed, and the file is deleted.

或者我们可以直接使用tempfile.mkstemp(),它将创建文件并将其名称和fd作为一个元组返回——尽管使用*TemporaryFile可能在平台之间更安全且可移植。

fd, name = tempfile.mkstemp()
try:
    tf = io.open(fd, 'w+', encoding='utf-8', newline='')
    writer = csv.writer(tf, ...)
    writer.writerow(...)
finally:
    os.close(fd)
    os.unlink(name)

并回答有关 SpooledTemporaryFile 的原始问题

我会尝试在 python2 下继承 SpooledTemporaryFile 并覆盖其 rollover 方法。

警告:这未经测试。

import io
import sys
import tempfile

if sys.version_info >= (3,):
    SpooledTemporaryFile = tempfile.SpooledTemporaryFile
else:
    class SpooledTemporaryFile(tempfile.SpooledTemporaryFile):
        def __init__(self, max_size=0, mode='w+b', **kwargs):
            # replace cStringIO with io.BytesIO or io.StringIO
            super(SpooledTemporaryFile, self).__init__(max_size, mode, **kwargs)
            if 'b' in mode:
                self._file = io.BytesIO()
            else:
                self._file = io.StringIO(newline='\n')  # see python3's tempfile sources for reason

        def rollover(self):
            if self._rolled:
                return
            # call super's implementation and then replace underlying file object
            super(SpooledTemporaryFile, self).rollover()
            fd = self._file.fileno()
            name = self._file.name
            mode = self._file.mode
            delete = self._file.delete
            pos = self._file.tell()
            # self._file is a tempfile._TemporaryFileWrapper.
            # It caches methods so we cannot just replace its .file attribute,
            # so let's create another _TemporaryFileWrapper
            file = io.open(fd, mode)
            file.seek(pos)
            self._file = tempfile._TemporaryFileWrapper(file, name, delete)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多