高效的文件缓冲&在python中扫描大文件的方法

时间:2011-01-26 03:55:20

标签: python performance io bioinformatics fasta

我遇到的问题描述有点复杂,我会提供更完整的信息。对于不耐烦的人,这是我可以总结的最简单的方法:

  

什么是最快的(最少执行   时间)分割文本文件的方法   大小为N的所有(重叠)子串(绑定N,例如36)   同时抛出换行符。

我正在编写一个模块,用于解析基于FASTA ascii的基因组格式的文件。这些文件包含所谓的'hg18'人类参考基因组,如果您愿意,可以从UCSC genome browser(go slugs!)下载。

正如您将注意到的,基因组文件由chr [1..22] .fa和chr [XY] .fa组成,以及一组未在此模块中使用的其他小文件。

已经存在几个用于解析FASTA文件的模块,例如BioPython的SeqIO。 (对不起,我发布了一个链接,但我还没有这么做。)不幸的是,我能找到的每个模块都没有按照我要做的具体操作。

我的模块需要将基因组数据(例如,'CAGTACGTCAGACTATACGGAGCTA'可能是一条线)分成每个重叠的N长度子串。让我举一个例子,使用一个非常小的文件(实际的染色体文件长度在355到2000万个字符之间)和N = 8

>>>import cStringIO
>>>example_file = cStringIO.StringIO("""\
>header
CAGTcag
TFgcACF
""")
>>>for read in parse(example_file):
...    print read
...
CAGTCAGTF
AGTCAGTFG
GTCAGTFGC
TCAGTFGCA
CAGTFGCAC
AGTFGCACF

我发现的功能在我能想到的方法中具有绝对最佳性能:


def parse(file):
  size = 8 # of course in my code this is a function argument
  file.readline() # skip past the header
  buffer = ''
  for line in file:
    buffer += line.rstrip().upper()
    while len(buffer) >= size:
      yield buffer[:size]
      buffer = buffer[1:]

这很有效,但不幸的是,用这种方式解析人类基因组还需要大约1.5小时(见下面的注释)。也许这是我将要看到的最好的方法(一个完整的代码重构可能是有序的,但我想避免它,因为这种方法在代码的其他方面有一些非常具体的优势),但我以为我会把它转交给社区。

谢谢!

  • 注意,这次包括很多额外的计算,例如计算反向链读取和在大小约为5G的散列上进行散列表查找。

回答结论:事实证明,使用fileobj.read()然后操纵结果字符串(string.replace()等)花费的时间和内存相对较少程序的其余部分,所以我使用了这种方法。谢谢大家!

4 个答案:

答案 0 :(得分:4)

你可以mmap文件并用滑动窗口开始啄它吗?我写了一个非常小的愚蠢的小程序:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
sarnold  20919  0.0  0.0  33036  4960 pts/2    R+   22:23   0:00 /usr/bin/python ./sliding_window.py

使用636229字节的fasta文件(通过http://biostar.stackexchange.com/questions/1759找到)花费了.383秒。

#!/usr/bin/python

import mmap
import os

  def parse(string, size):
    stride = 8
    start = string.find("\n")
    while start < size - stride:
        print string[start:start+stride]
        start += 1

fasta = open("small.fasta", 'r')
fasta_size = os.stat("small.fasta").st_size
fasta_map = mmap.mmap(fasta.fileno(), 0, mmap.MAP_PRIVATE, mmap.PROT_READ)
parse(fasta_map, fasta_size)

答案 1 :(得分:3)

我怀疑问题是你有这么多数据以字符串格式存储,这对你的用例来说真的很浪费,你的实际内存耗尽并且交换掉了。 128 GB 足以避免这种情况......:)

由于您已在评论中指出您无论如何都需要存储其他信息,因此我将选择引用父字符串的单独类。我使用来自hg18的chromFa.zip的chr21.fa进行了一个简短的测试。该文件大约48MB,不到1M行。我这里只有1GB的内存,所以我只是丢弃后面的对象。因此,此测试不会显示碎片,缓存或相关问题,但我认为它应该是衡量解析吞吐量的良好起点:

import mmap
import os
import time
import sys

class Subseq(object):
  __slots__ = ("parent", "offset", "length")

  def __init__(self, parent, offset, length):
    self.parent = parent
    self.offset = offset
    self.length = length

  # these are discussed in comments:
  def __str__(self):
    return self.parent[self.offset:self.offset + self.length]

  def __hash__(self):
    return hash(str(self))

  def __getitem__(self, index):
    # doesn't currently handle slicing
    assert 0 <= index < self.length
    return self.parent[self.offset + index]

  # other methods

def parse(file, size=8):
  file.readline()  # skip header
  whole = "".join(line.rstrip().upper() for line in file)
  for offset in xrange(0, len(whole) - size + 1):
    yield Subseq(whole, offset, size)

class Seq(object):
  __slots__ = ("value", "offset")
  def __init__(self, value, offset):
    self.value = value
    self.offset = offset

def parse_sep_str(file, size=8):
  file.readline()  # skip header
  whole = "".join(line.rstrip().upper() for line in file)
  for offset in xrange(0, len(whole) - size + 1):
    yield Seq(whole[offset:offset + size], offset)

def parse_plain_str(file, size=8):
  file.readline()  # skip header
  whole = "".join(line.rstrip().upper() for line in file)
  for offset in xrange(0, len(whole) - size + 1):
    yield whole[offset:offset+size]

def parse_tuple(file, size=8):
  file.readline()  # skip header
  whole = "".join(line.rstrip().upper() for line in file)
  for offset in xrange(0, len(whole) - size + 1):
    yield (whole, offset, size)

def parse_orig(file, size=8):
  file.readline() # skip header
  buffer = ''
  for line in file:
    buffer += line.rstrip().upper()
    while len(buffer) >= size:
      yield buffer[:size]
      buffer = buffer[1:]

def parse_os_read(file, size=8):
  file.readline()  # skip header
  file_size = os.fstat(file.fileno()).st_size
  whole = os.read(file.fileno(), file_size).replace("\n", "").upper()
  for offset in xrange(0, len(whole) - size + 1):
    yield whole[offset:offset+size]

def parse_mmap(file, size=8):
  file.readline()  # skip past the header
  buffer = ""
  for line in file:
    buffer += line
    if len(buffer) >= size:
      for start in xrange(0, len(buffer) - size + 1):
        yield buffer[start:start + size].upper()
      buffer = buffer[-(len(buffer) - size + 1):]
  for start in xrange(0, len(buffer) - size + 1):
    yield buffer[start:start + size]

def length(x):
  return sum(1 for _ in x)

def duration(secs):
  return "%dm %ds" % divmod(secs, 60)


def main(argv):
  tests = [parse, parse_sep_str, parse_tuple, parse_plain_str, parse_orig, parse_os_read]
  n = 0
  for fn in tests:
    n += 1
    with open(argv[1]) as f:
      start = time.time()
      length(fn(f))
      end = time.time()
      print "%d  %-20s  %s" % (n, fn.__name__, duration(end - start))

  fn = parse_mmap
  n += 1
  with open(argv[1]) as f:
    f = mmap.mmap(f.fileno(), 0, mmap.MAP_PRIVATE, mmap.PROT_READ)
    start = time.time()
    length(fn(f))
    end = time.time()
  print "%d  %-20s  %s" % (n, fn.__name__, duration(end - start))


if __name__ == "__main__":
  sys.exit(main(sys.argv))

1  parse                 1m 42s
2  parse_sep_str         1m 42s
3  parse_tuple           0m 29s
4  parse_plain_str       0m 36s
5  parse_orig            0m 45s
6  parse_os_read         0m 34s
7  parse_mmap            0m 37s

前四个是我的代码,而orig是你的,后两个来自其他答案。

创建和收集用户定义的对象比元组或纯字符串成本高得多!这不应该是那么令人惊讶,但我没有意识到它会产生这么大的差别(比较#1和#3,它们在用户定义的类和元组之间真的只有不同)。您说您希望无论如何都要在字符串中存储附加信息(如offset)(如在parse和parse_sep_str情况下),因此您可以考虑在C扩展模块中实现该类型。如果你不想直接写C,请查看Cython和相关内容。

情况#1和#2是相同的:通过指向父字符串,我试图节省内存而不是处理时间,但是这个测试不能测量它。

答案 2 :(得分:3)

一些经典的IO绑定更改。

  • 使用较低级别的读取操作,如os.read,并读入大型固定缓冲区。
  • 使用线程/多处理,其中一个读取和缓冲,另一个进程。
  • 如果您有多个处理器/机器,则使用多处理/ mq来分配CPU上的处理ala map-reduce。

使用较低级别的读取操作不会重写那么多。其他的将是非常大的重写。

答案 3 :(得分:1)

我有一个处理文本文件的函数,并使用缓冲区进行读写和并行计算,并使用异步池进程。我有一个2核,8GB内存,带有gnu / linux的AMD,可以在不到1秒的时间内处理300000行,在大约4秒内处理1000000行,在大约20秒内处理大约450000行(大约220MB):

# -*- coding: utf-8 -*-
import sys
from multiprocessing import Pool

def process_file(f, fo="result.txt", fi=sys.argv[1]):
    fi = open(fi, "r", 4096)
    fo = open(fo, "w", 4096)
    b = []
    x = 0
    result = None
    pool = None
    for line in fi:
        b.append(line)
        x += 1
        if (x % 200000) == 0:
            if pool == None:
                pool = Pool(processes=20)
            if result == None:
                result = pool.map_async(f, b)
            else:
                presult = result.get()
                result = pool.map_async(f, b)
                for l in presult:
                    fo.write(l)
            b = []
    if not result == None:
        for l in result.get():
            fo.write(l)
    if not b == []:
        for l in b:
            fo.write(f(l))
    fo.close()
    fi.close()

第一个参数是rceive一行,处理和返回结果的函数将写入文件,下一个是输出文件,last是输入文件(如果在脚本文件中作为第一个参数接收,则不能使用last参数输入)。