Эффективные методы буферизации и сканирования файлов для больших файлов в python

описание проблемы, с которой я сталкиваюсь, немного сложно, и я буду ошибаться на стороне предоставления более полной информации. Для нетерпеливых, вот самый краткий способ, которым я могу подвести итог:

что является самым быстрым (наименьшее выполнение time) способ разделения текстового файла на Все (перекрывающиеся) подстроки размера N (связанные N, например 36) при выбрасывании персонажей новой строки.

Я пишу модуль, который анализирует файлы в FASTA формат генома на основе ascii. Эти файлы содержат то, что известно как "hg18" геном ссылки человека, который вы можете скачать из УСК геноме браузера (go slugs! если хотите.

Как вы заметите, файлы генома состоят из chr[1..22].fa и chr[XY].fa, а также набор других небольших файлов, которые не используются в этом модуле.

уже существует несколько модулей для анализа файлов FASTA, таких как SeqIO BioPython. (Извините, я бы отправил ссылку, но я у меня пока нет на это оснований.) К сожалению, каждый модуль, который я смог найти, не выполняет конкретную операцию, которую я пытаюсь сделать.

мой модуль должен разделить данные генома (например, "CAGTACGTCAGACTATACGGAGCTA" может быть строкой) на каждую перекрывающуюся подстроку N-длины. Позвольте мне привести пример, используя очень маленький файл (фактические файлы хромосом имеют длину от 355 до 20 миллионов символов) и 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.заменить () и т. д.) заняло относительно мало времени и памяти по сравнению с остальной частью программы, и поэтому я использовал этот подход. Спасибо всем!

4 ответов


некоторые классические изменения, связанные с IO.

  • использовать нижний уровень читать как os.read и чтение в большой фиксированный буфер.
  • используйте threading / multiprocessing где одно читает и буферы и другие процессы.
  • если у вас есть несколько процессоров / машин, используйте multiprocessing / mq для разделения обработки по процессорам ALA map-reduce.

использование операции чтения нижнего уровня не было бы такой большой переписью. Остальные будут довольно большие изменения.


не могли бы вы 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)

Я подозреваю, что проблема в том, что у вас есть столько данные, хранящиеся в строковом формате, что действительно расточительно для вашего случая использования, что у вас заканчивается реальная память и thrashing swap. 128 ГБ должны будет достаточно, чтобы избежать этого... :)

Так как вы указали в комментариях, что вам все равно нужно хранить дополнительную информацию, отдельный класс, который ссылается на родительскую строку будет мой выбор. Я провел короткий тест с помощью chr21.ФА от хромфа.zip от hg18; файл составляет около 48 МБ и чуть менее 1 м строк. У меня только 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. Посмотрите на Cython и связанные, если вы не хотите писать C напрямую.

Случай #1 и #2 идентичны: указывая на родительскую строку, я пытался сохранить память, а не время обработки, но этот тест не измеряет это.

У меня есть функция для обработки текстового файла и использования буфера в чтении и записи и параллельных вычислениях с асинхронным пулом рабочих наборов процесса. У меня есть AMD из 2 ядер, 8GB RAM, с gnu / linux и может обрабатывать 300000 строк менее чем за 1 секунду, 1000000 строк примерно за 4 секунды и примерно 4500000 строк (более 220 Мб) примерно за 20 секунд:

# -*- 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-это файл ввода (вы не можете использовать последний аргумент, если вы получаете в качестве первого параметра в файле скрипта ввода).