Pandon pandas: чтение пропущенных файлов

Я часто имею дело с таблицами ascii, содержащими несколько столбцов (обычно менее 10) и до десятков миллионов строк. Они похожи

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 

У меня есть несколько кодов питона, которые читают, управляют и сохраняют файлы. Я всегда использовал numpy.loadtxt и numpy.savetxt для этого. Но numpy.loadtxt занимает не менее 5-6 ГБ оперативной памяти, чтобы читать файл ascii 1Gb.

Вчера я обнаружил Pandas, который разрешил почти все мои проблемы: pandas.read_table вместе с numpy.savetxt улучшил скорость выполнения (из 2) моих скриптов в 3 или 4 раза, при этом очень эффективная память.

Все хорошо до момента, когда я пытаюсь прочитать в файле, который содержит несколько прокомментированных строк в начале. Строка doc (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() 

read_csv и read_table имеют параметр comment , который пропускает байты, начиная с символа комментария до конца строки. Если нужно пропустить всю строку, это не совсем правильно, потому что синтаксический анализатор будет думать, что в нем видна строка без полей, а затем в конечном итоге увидит допустимую строку данных и запутается.

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

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

Реализация этой скважины потребует погружения в код tokenizer C. Это не так плохо, как может показаться.

Я нашел компактное решение, создав класс, наследующий 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() для итерации по .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 

Вместо наследования вы также можете использовать делегирование, это зависит от вашего вкуса.

EDIT Я попробовал много других способов улучшить производительность. Однако это сложная работа. Я пытался

  • threading: Прочитайте файл вперед в одном потоке с низкоуровневой io-операцией и большими кусками, разделите его на строки, запустите их и получите только из очереди на next()
  • то же самое с многопроцессорной обработкой
  • аналогичный многопоточный подход, но с использованием readlines(size_hint)
  • mmap для чтения из файла

Первые три подхода на удивление были медленнее, поэтому никакой выгоды. Использование mmap значительно улучшило производительность. Вот код:

 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 

Я также выполнил проверку комментариев только до тех пор, пока не будет найдена первая строка без комментария. Это соответствует вашему решению и еще больше повышает производительность.

Однако для mmap s существуют некоторые ограничения. Если размеры файлов огромны, убедитесь, что у вас достаточно ОЗУ. Если вы работаете с 32-битной ОС, вы не сможете читать файлы размером более 2 ГБ.