Принятый ответ не будет работать для случаев с большими файлами, которые не помещаются в памяти (что не редкость).
Как было отмечено другими, @ srohde answer выглядит хорошо, но у него есть следующие проблемы:
- открытие файла выглядит избыточно, когда мы можем передать объект файла и оставить его пользователю, чтобы решить, в какой кодировке он должен быть прочитан,
, даже если мы реорганизуем прием файлового объекта, он не будет работать для всех кодировок: мы можем выбрать файл с кодировкой utf-8
и содержимым, отличным от ascii, например
й
pass buf_size
равно 1
и будет иметь
UnicodeDecodeError: 'utf8' codec can't decode byte 0xb9 in position 0: invalid start byte
конечно, текст может быть больше, но buf_size
может быть подобрано, так что это приведет к запутанной ошибке, как указано выше,
- мы не можем указать пользовательский разделитель строк,
- мы не можем сохранить разделитель строк.
Итак, учитывая все эти проблемы, я написал отдельные функции:
- тот, который работает с байтовыми потоками,
- второй, который работает с текстовыми потоками и делегирует свой основной поток байтов первому и декодирует результирующие строки.
Прежде всего давайте определим следующие служебные функции:
ceil_division
для деления с потолком (в отличие от стандартного //
деления с полом, более подробную информацию можно найти в этой теме )
def ceil_division(left_number, right_number):
"""
Divides given numbers with ceiling.
"""
return -(-left_number // right_number)
split
для разделения строки заданным разделителем с правого конца с возможностью его сохранения:
def split(string, separator, keep_separator):
"""
Splits given string by given separator.
"""
parts = string.split(separator)
if keep_separator:
*parts, last_part = parts
parts = [part + separator for part in parts]
if last_part:
return parts + [last_part]
return parts
read_batch_from_end
для чтения пакета из правого конца двоичного потока
def read_batch_from_end(byte_stream, size, end_position):
"""
Reads batch from the end of given byte stream.
"""
if end_position > size:
offset = end_position - size
else:
offset = 0
size = end_position
byte_stream.seek(offset)
return byte_stream.read(size)
После этого мы можем определить функцию для чтения потока байтов в обратном порядке, как
import functools
import itertools
import os
from operator import methodcaller, sub
def reverse_binary_stream(byte_stream, batch_size=None,
lines_separator=None,
keep_lines_separator=True):
if lines_separator is None:
lines_separator = (b'\r', b'\n', b'\r\n')
lines_splitter = methodcaller(str.splitlines.__name__,
keep_lines_separator)
else:
lines_splitter = functools.partial(split,
separator=lines_separator,
keep_separator=keep_lines_separator)
stream_size = object_.seek(0, os.SEEK_END)
if batch_size is None:
batch_size = stream_size or 1
batches_count = ceil_division(stream_size, batch_size)
remaining_bytes_indicator = itertools.islice(
itertools.accumulate(itertools.chain([stream_size],
itertools.repeat(batch_size)),
sub),
batches_count)
try:
remaining_bytes_count = next(remaining_bytes_indicator)
except StopIteration:
return
def read_batch(position: int) -> bytes:
result = read_batch_from_end(object_,
size=batch_size,
end_position=position)
while result.startswith(lines_separator):
try:
position = next(remaining_bytes_indicator)
except StopIteration:
break
result = (read_batch_from_end(object_,
size=batch_size,
end_position=position)
+ result)
return result
batch = read_batch(remaining_bytes_count)
segment, *lines = lines_splitter(batch)
yield from reverse(lines)
for remaining_bytes_count in remaining_bytes_indicator:
batch = read_batch(remaining_bytes_count)
lines = lines_splitter(batch)
if batch.endswith(lines_separator):
yield segment
else:
lines[-1] += segment
segment, *lines = lines
yield from reverse(lines)
yield segment
и, наконец, функция для изменения текстового файла может быть определена как:
import codecs
def reverse_file(file, batch_size=None,
lines_separator=None,
keep_lines_separator=True):
encoding = file.encoding
if lines_separator is not None:
lines_separator = lines_separator.encode(encoding)
yield from map(functools.partial(codecs.decode,
encoding=encoding),
reverse_binary_stream(
file.buffer,
batch_size=batch_size,
lines_separator=lines_separator,
keep_lines_separator=keep_lines_separator))
Тесты
Подготовка
Я сгенерировал 4 файла, используя команду fsutil
:
- empty.txt без содержимого, размер 0MB
- tiny.txt размером 1 МБ
- small.txt с размером 10 МБ
- large.txt с размером 50 МБ
также я реорганизовал решение @srohde для работы с файловым объектом вместо пути к файлу.
Тестовый скрипт
from timeit import Timer
repeats_count = 7
number = 1
create_setup = ('from collections import deque\n'
'from __main__ import reverse_file, reverse_readline\n'
'file = open("{}")').format
srohde_solution = ('with file:\n'
' deque(reverse_readline(file,\n'
' buf_size=8192),'
' maxlen=0)')
azat_ibrakov_solution = ('with file:\n'
' deque(reverse_file(file,\n'
' lines_separator="\\n",\n'
' keep_lines_separator=False,\n'
' batch_size=8192), maxlen=0)')
print('reversing empty file by "srohde"',
min(Timer(srohde_solution,
create_setup('empty.txt')).repeat(repeats_count, number)))
print('reversing empty file by "Azat Ibrakov"',
min(Timer(azat_ibrakov_solution,
create_setup('empty.txt')).repeat(repeats_count, number)))
print('reversing tiny file (1MB) by "srohde"',
min(Timer(srohde_solution,
create_setup('tiny.txt')).repeat(repeats_count, number)))
print('reversing tiny file (1MB) by "Azat Ibrakov"',
min(Timer(azat_ibrakov_solution,
create_setup('tiny.txt')).repeat(repeats_count, number)))
print('reversing small file (10MB) by "srohde"',
min(Timer(srohde_solution,
create_setup('small.txt')).repeat(repeats_count, number)))
print('reversing small file (10MB) by "Azat Ibrakov"',
min(Timer(azat_ibrakov_solution,
create_setup('small.txt')).repeat(repeats_count, number)))
print('reversing large file (50MB) by "srohde"',
min(Timer(srohde_solution,
create_setup('large.txt')).repeat(repeats_count, number)))
print('reversing large file (50MB) by "Azat Ibrakov"',
min(Timer(azat_ibrakov_solution,
create_setup('large.txt')).repeat(repeats_count, number)))
Примечание : Я использовал класс collections.deque
для выпуска генератора.
Выходы
Для PyPy 3.5 в Windows 10:
reversing empty file by "srohde" 8.31e-05
reversing empty file by "Azat Ibrakov" 0.00016090000000000028
reversing tiny file (1MB) by "srohde" 0.160081
reversing tiny file (1MB) by "Azat Ibrakov" 0.09594989999999998
reversing small file (10MB) by "srohde" 8.8891863
reversing small file (10MB) by "Azat Ibrakov" 5.323388100000001
reversing large file (50MB) by "srohde" 186.5338368
reversing large file (50MB) by "Azat Ibrakov" 99.07450229999998
Для CPython 3.5 в Windows 10:
reversing empty file by "srohde" 3.600000000000001e-05
reversing empty file by "Azat Ibrakov" 4.519999999999958e-05
reversing tiny file (1MB) by "srohde" 0.01965560000000001
reversing tiny file (1MB) by "Azat Ibrakov" 0.019207699999999994
reversing small file (10MB) by "srohde" 3.1341862999999996
reversing small file (10MB) by "Azat Ibrakov" 3.0872588000000007
reversing large file (50MB) by "srohde" 82.01206720000002
reversing large file (50MB) by "Azat Ibrakov" 82.16775059999998
Итак, как мы видим, он работает как оригинальное решение, но является более общим и свободным от перечисленных выше недостатков.
Реклама
Я добавил это в 0.3.0
версию lz
пакета (требуется Python 3.5 +), в котором есть много хорошо протестированных функциональных / итерационных утилит.
Может использоваться как
import io
from lz.iterating import reverse
...
with open('path/to/file') as file:
for line in reverse(file, batch_size=io.DEFAULT_BUFFER_SIZE):
print(line)
Он поддерживает все стандартные кодировки (может быть, за исключением utf-7
, поскольку мне трудно определить стратегию для генерации кодируемых строк).