Быстрый доступ к строке в файле Python

У меня есть таблица ASCII в файле, из которого я хочу прочитать определенный набор строк (например, строки 4003-4005). Проблема в том, что этот файл может быть очень длинным (например, от 100 тысяч до миллионов строк), и я хотел бы сделать это как можно быстрее.

Плохое решение: прочитайте весь файл и перейдите к этим строкам,

f = open('filename')
lines = f.readlines()[4003:4005]

Лучшее решение: enumerate по каждой строке, чтобы оно не было в памяти (a la qaru.site/info/48967/...)

f = open('filename')
lines = []
for i, line in enumerate(f):
    if i >= 4003 and i <= 4005: lines.append(line)
    if i > 4005: break                                    # @Wooble

Лучшее решение?

Но это все равно требует перехода через каждую строку. Есть ли лучший способ (с точки зрения скорости/эффективности) доступа к определенной строке? Должен ли я использовать linecache, хотя я могу получить доступ к файлу только один раз (обычно)?

Вместо этого использовать двоичный файл, и в этом случае было бы проще пропустить опцию, но я бы предпочел избежать этого.

Ответы

Ответ 1

Я бы просто использовал itertools.islice. Использование islice над итерируемым, как и дескриптор файла, означает, что весь файл никогда не считывается в память, а первые 4002 строки отбрасываются как можно быстрее. Вы могли бы даже бросить две строки, которые вам нужны, в список довольно дешево (при условии, что сами строки не очень длинны). Затем вы можете выйти из блока with, закрыв дескриптор файла.

from itertools import islice
with open('afile') as f:
    lines = list(islice(f, 4003, 4005))
do_something_with(lines)

Update

Но святая корова быстрее linecache для множественного доступа. Я создал миллионный файл для сравнения islice и linecache, а linecache удалил его.

>>> timeit("x=islice(open('afile'), 4003, 4005); print next(x) + next(x)", 'from itertools import islice', number=1)
4003
4004

0.00028586387634277344
>>> timeit("print getline('afile', 4003) + getline('afile', 4004)", 'from linecache import getline', number=1)
4002
4003

2.193450927734375e-05

>>> timeit("getline('afile', 4003) + getline('afile', 4004)", 'from linecache import getline', number=10**5)
0.14125394821166992
>>> timeit("''.join(islice(open('afile'), 4003, 4005))", 'from itertools import islice', number=10**5)
14.732316970825195

Постоянно повторно импортировать и перечитывать файл:

Это не практический тест, но даже повторный импорт linecache на каждом шаге он только второй медленнее, чем islice.

>>> timeit("from linecache import getline; getline('afile', 4003) + getline('afile', 4004)", number=10**5)
15.613967180252075

Заключение

Да, linecache быстрее, чем islice для всех, но постоянно воссоздает linecache, но кто это делает? Для вероятных сценариев (чтение только нескольких строк, один раз и чтение нескольких строк один раз) linecache выполняется быстрее и представляет собой краткий синтаксис, но синтаксис islice довольно чист и быстр, а также не читает весь файл в память. В среде с жесткой оболочкой решение islice может быть правильным выбором. Для очень высоких требований к скорости, linecache может быть лучшим выбором. Практически, однако, в большинстве сред оба раза достаточно малы, это почти не имеет значения.

Ответ 2

Основная проблема здесь заключается в том, что разрывы строк ничем не отличаются от любого другого персонажа. Таким образом, ОС не имеет возможности пропускать в эту строку.

Тем не менее, есть несколько вариантов, но для каждого из них вы должны так или иначе жертвовать.

Вы уже указали первый: используйте двоичный файл. Если у вас фиксированная длина строки, вы можете seek впереди line * bytes_per_line байт и перейти непосредственно к этой строке.

Следующая опция будет использовать индекс: создать второй файл, и в каждой строке этого файла индекса напишите байтовый индекс строки в вашем файле данных. Доступ к файлу данных теперь включает в себя две операции поиска (пропустить до line индекса, а затем пропустить до index_value в файле данных), но он все равно будет довольно быстрым. Плюс: сохранит дисковое пространство, потому что линии могут иметь разную длину. Минус: вы не можете коснуться файла данных с помощью редактора.

Еще один вариант: (я думаю, я бы пошел с этим) - использовать только один файл, но начинать каждую строку с номера строки и своего рода разделителя. (например, 4005: моя строка данных). Теперь вы можете использовать измененную версию двоичного поиска https://en.wikipedia.org/wiki/Binary_search_algorithm для поиска своей линии. Это займет около log(n) операций поиска, где n - общее количество строк. Плюс: вы можете редактировать файл и экономить место по сравнению с линиями фиксированной длины. И это все еще очень быстро. Даже для миллиона линий это всего лишь около 20 операций поиска, которые происходят в кратчайшие сроки. Минус: самая сложная из этих возможностей. (Но интересно делать;)

EDIT: Еще одно решение: Разделите свой файл во многих смарительных. Если у вас очень длинные строки, это может быть не более одной строки для каждого файла. Но тогда я бы поставил их в группы в папках, например, например. 4/0/05. Но даже при более коротких строках разделите свой файл на: пусть говорят грубо - 1 мб кусков, назовите их 1000.txt, 2000.txt и прочитайте один (или два), соответствующий вашей линии, полностью должен быть довольно быстрым, очень легко реализовать.