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