1

我经常处理包含几列(通常少于 10 列)和多达数千万行的 ascii 表。他们看起来像

176.792 -2.30523 0.430772 32016 1 1 2 
177.042 -1.87729 0.430562 32016 1 1 1
177.047 -1.54957 0.431853 31136 1 1 1
...
177.403 -0.657246 0.432905 31152 1 1 1

我有许多读取、操作和保存文件的 python 代码。我一直使用numpy.loadtxtnumpy.savetxt做到这一点。但numpy.loadtxt至少需要 5-6Gb RAM 才能读取 1Gb ascii 文件。

昨天我发现了 Pandas,它几乎解决了我所有的问题:pandas.read_table同时numpy.savetxt将我的脚本的执行速度(2 个)提高了 3 或 4 倍,同时非常节省内存。

一切都很好,直到我尝试读取开头包含一些注释行的文件。文档字符串 (v=0.10.1.dev_f73128e) 告诉我不支持行注释,这可能会出现。我认为这会很棒:我真的很喜欢在numpy.loadtxt. 对这将如何变得可用有任何想法吗?有可能跳过这些行也很好(文档声明它们将作为empy返回)

不知道我的文件中有多少注释行(我处理了来自不同人的数千条注释),现在我打开文件,计算文件开头以注释开头的行数:

def n_comments(fn, comment):
    with open(fname, 'r') as f:
        n_lines = 0
        pattern = re.compile("^\s*{0}".format(comment))
        for l in f:
            if pattern.search(l) is None:
                break
            else:
                n_lines += 1
    return n_lines

接着

pandas.read_table(fname, skiprows=n_comments(fname, '#'), header=None, sep='\s')

有没有更好的方法(可能在熊猫中)来做到这一点?

最后,在发布之前,我查看了一些代码pandas.io.parsers.py以了解pandas.read_table引擎盖下的工作原理,但我迷路了。谁能指出我实现文件读取的地方?

谢谢

EDIT2:我想得到一些改进,摆脱if@ThorstenKranz 的第二个实现中的一些FileWrapper,但几乎没有得到任何改进

class FileWrapper(file):
    def __init__(self, comment_literal, *args):
        super(FileWrapper, self).__init__(*args)
        self._comment_literal = comment_literal
        self._next = self._next_comment

    def next(self):
        return self._next()

    def _next_comment(self):
        while True:
            line = super(FileWrapper, self).next()
            if not line.strip()[0] == self._comment_literal:
                self._next = self._next_no_comment
                return line
    def _next_no_comment(self):
        return super(FileWrapper, self).next()
4

2 回答 2

3

read_csvread_table有一个comment选项可以跳过从注释字符开始到行尾的字节。如果需要跳过行,这是不正确的,因为解析器会认为它看到的是没有字段的行,然后最终看到有效的数据行并感到困惑。

我建议使用您的解决方法来确定要在文件中手动跳过的行数。当整行是注释时,如果有一个选项可以自动跳过行,那就太好了:

https://github.com/pydata/pandas/issues/2685

实现这一点需要深入了解 C 标记器代码。它并不像听起来那么糟糕。

于 2013-01-11T14:57:15.980 回答
2

我通过创建一个类继承找到了一个紧凑的解决方案file

import pandas as pd

class FileWrapper(file):
    def __init__(self, comment_literal, *args):
        super(FileWrapper, self).__init__(*args)
        self._comment_literal = comment_literal

    def next(self):
        while True:
            line = super(FileWrapper, self).next()
            if not line.startswith(self._comment_literal):
                return line

df = pd.read_table(FileWrapper("#", "14276661.txt", "r"), delimiter=" ", header=None)

Atm, pandas (0.8.1) 只使用.next()- 方法来迭代类似文件的对象。在我的示例中,我们可以重载此方法并仅返回那些不以专用注释文字开头的行"#"

对于输入文件:

176.792 -2.30523 0.430772 32016 1 1 2 
# 177.042 -1.87729 0.430562 32016 1 1 1
177.047 -1.54957 0.431853 31136 1 1 1
177.403 -0.657246 0.432905 31152 1 1 1

我们得到

>>> df
       X.1       X.2       X.3    X.4  X.5  X.6  X.7
0  176.792 -2.305230  0.430772  32016    1    1    2
1  177.047 -1.549570  0.431853  31136    1    1    1
2  177.403 -0.657246  0.432905  31152    1    1    1

并且对于

176.792 -2.30523 0.430772 32016 1 1 2 
177.042 -1.87729 0.430562 32016 1 1 1
177.047 -1.54957 0.431853 31136 1 1 1
177.403 -0.657246 0.432905 31152 1 1 1

我们得到

>>> df
       X.1       X.2       X.3    X.4  X.5  X.6  X.7
0  176.792 -2.305230  0.430772  32016    1    1    2
1  177.042 -1.877290  0.430562  32016    1    1    1
2  177.047 -1.549570  0.431853  31136    1    1    1
3  177.403 -0.657246  0.432905  31152    1    1    1

除了继承,您还可以使用委托,这取决于您的喜好。

编辑 我尝试了许多其他方法来提高性能。不过,这是一项艰巨的工作。我试过了

  • 线程:使用低级 io 操作和大块在一个线程中提前读取文件,将其拆分为行,将这些排入队列并且仅从队列中获取next()
  • 与多处理相同
  • 类似的多线程方法,但使用readlines(size_hint)
  • mmap从文件中读取

令人惊讶的是,前三种方法速度较慢,因此没有任何好处。使用ammap显着提高了性能。这是代码:

class FileWrapper(file):
    def __init__(self, comment_literal, *args):
        super(FileWrapper, self).__init__(*args)
        self._comment_literal = comment_literal
        self._in_comment = True
        self._prepare()

    def __iter__(self):
        return self

    def next(self):
        if self._in_comment:
            while True:
                line = self._get_next_line()
                if line == "":
                    raise StopIteration()
                if not line[0] == self._comment_literal:
                    self._in_comment = False
                    return line
        line = self._get_next_line()
        if line == "":
            raise StopIteration()
        return line

    def _get_next_line(self):
        return super(FileWrapper, self).next()

    def _prepare(self):
        pass

class MmapWrapper(file):
    def __init__(self, fd, comment_literal = "#"):
        self._mm = mmap.mmap(fd, 0, prot=mmap.PROT_READ)
        self._comment_literal = comment_literal
        self._in_comment = True

    def __iter__(self):
        return self #iter(self._mm.readline, "")#self

    def next(self):
        if self._in_comment:
            while True:
                line = self._mm.readline()
                if line == "":
                    raise StopIteration()
                if not line[0] == self._comment_literal:
                    self._in_comment = False
                    return line
        line = self._mm.readline()
        if line == "":
            raise StopIteration()
        return line

if __name__ == "__main__":
    t0 = time.time()    
    for i in range(10):    
        with open("1gram-d_1.txt", "r+b") as f:
            df1 = pd.read_table(MmapWrapper(f.fileno()), delimiter="\t", header=None)
    print "mmap:", time.time()-t0

    t0 = time.time()    
    for i in range(10):    
        df2 = pd.read_table(FileWrapper("#", "1gram-d_1.txt", "r"), delimiter="\t", header=None)
    print "Unbuffered:", time.time()-t0

    print (df1==df2).mean()

给出作为输出

mmap: 35.3251504898
Unbuffered: 41.3274121284
X.1    1
X.2    1
X.3    1
X.4    1

我还实施了评论检查,直到找到第一个非评论行。这符合您的解决方案并进一步提高了性能。

但是,对于mmaps,存在一些限制。如果文件很大,请确保有足够的 RAM。如果您使用的是 32 位操作系统,您将无法读取大于 2GB 的文件。

于 2013-01-11T13:51:12.883 回答