Який найефективніший спосіб отримати перший і останній рядки текстового файлу?


74

У мене є текстовий файл, який містить позначку часу в кожному рядку. Моя мета - знайти діапазон часу. Весь час в порядку, тому перший рядок буде найранішим, а останній рядок - останнім часом. Мені потрібен лише найперший і останній рядок. Який найефективніший спосіб отримати ці рядки в python?

Примітка: Ці файли мають відносно велику довжину, приблизно 1-2 мільйони рядків кожен, і я повинен це зробити для кількох сотень файлів.

Відповіді:


63

документи для модуля io

with open(fname, 'rb') as fh:
    first = next(fh).decode()

    fh.seek(-1024, 2)
    last = fh.readlines()[-1].decode()

Значення змінної тут дорівнює 1024: воно представляє середню довжину рядка. Я вибираю 1024 лише для прикладу. Якщо у вас є оцінка середньої довжини рядка, ви можете просто використати це значення, помножене на 2.

Оскільки ви взагалі не уявляєте про можливу верхню межу довжини рядка, очевидним рішенням було б прокрутити файл:

for line in fh:
    pass
last = line

Вам не потрібно турбуватися про двійковий прапор, який ви можете просто використовувати open(fname).

ETA : Оскільки у вас є багато файлів, над якими можна працювати, ви можете створити зразок з декількох десятків файлів, використовуючи random.sampleта запустивши на них цей код, щоб визначити довжину останнього рядка. З апріорно великим значенням зміщення позиції (скажімо, 1 МБ). Це допоможе вам оцінити значення за весь пробіг.


Поки рядки не мають більше 1024 символів.
FogleBird

Немає гарантії, що рядки мають довжину не більше 1024 символів, крім позначок часу на рядку можуть бути інші сміття.
pasbino

@pasbino: у вас є якась верхня межа?
SilentGhost

18
Використання fh.seek(-1024, os.SEEK_END)замість fh.seek(-1024, 2)збільшує читабельність.
марсл

2
Наступне не відповідає дійсності: Вам не потрібно турбуватися про двійковий прапор, який ви могли б просто використовувати open(fname). Відкриття з bпрапором має вирішальне значення. Якщо ви використовуєте open(fname)замість цього, open(fname, 'rb')ви отримаєте io.UnsupportedOperation: неможливо виконати ненульові кінцеві відносні пошуки .
patryk.beza

87

Щоб прочитати перший і останній рядки файлу, ви можете ...

  • відкрити файл, ...
  • ... прочитати перший рядок, використовуючи вбудований readline(), ...
  • ... шукати (рухати курсор) до кінця файлу, ...
  • ... крокуйте назад, поки не зустрінете EOL (розрив рядка) і ...
  • ... прочитайте звідти останній рядок.
def readlastline(f):
    f.seek(-2, 2)              # Jump to the second last byte.
    while f.read(1) != b"\n":  # Until EOL is found ...
        f.seek(-2, 1)          # ... jump back, over the read byte plus one more.
    return f.read()            # Read all data from this point on.
    
with open(file, "rb") as f:
    first = f.readline()
    last = readlastline(f)

Перейти безпосередньо до другого останнього байта, щоб запобігти поверненню символів нового рядка до порожніх рядків *.

Поточне зміщення зміщується вперед на одиницю кожного разу, коли читається байт, тому крок назад здійснюється за два байти за раз, минувши недавно прочитаний байт і байт для читання наступного.

whenceРухаючись параметр fseek(offset, whence=0)вказує , що fseekслід прагнути до позиції offsetбайтів по відношенню до ...

* Як і слід було очікувати, оскільки поведінка за замовчуванням для більшості додатків, включаючи printі echo, додає по одному до кожного написаного рядка і не впливає на рядки, що відсутній у символі нового рядка.


Ефективність

1-2 мільйони рядків кожен, і я повинен зробити це для кількох сотень файлів.

Я приурочив цей метод до часу і порівняв його з найпопулярнішою відповіддю.

10k iterations processing a file of 6k lines totalling 200kB: 1.62s vs 6.92s.
100 iterations processing a file of 6k lines totalling 1.3GB: 8.93s vs 86.95.

Мільйони ліній призведе до збільшення різниці набагато більше.

Точний код, який використовується для хронометражу:

with open(file, "rb") as f:
    first = f.readline()     # Read and store the first line.
    for last in f: pass      # Read all lines, keep final value.

Поправка

Складніший і важчий для читання варіант розгляду коментарів та проблем, порушених з того часу.

Також додана підтримка мультібайтних роздільників, readlast(b'X<br>Y', b'<br>', fixed=False).

Зверніть увагу, що ця варіація дійсно повільна для великих файлів через не відносні зміщення, необхідні в текстовому режимі. Змініть свої потреби або взагалі не використовуйте їх, оскільки вам, мабуть, краще використовувати f.readlines()[-1]файли, відкриті в текстовому режимі.

#!/bin/python3

from os import SEEK_END

def readlast(f, sep, fixed=True):
    r"""Read the last segment from a file-like object.

    :param f: File to read last line from.
    :type  f: file-like object
    :param sep: Segment separator (delimiter).
    :type  sep: bytes, str
    :param fixed: Treat data in ``f`` as a chain of fixed size blocks.
    :type  fixed: bool
    :returns: Last line of file.
    :rtype: bytes, str
    """
    bs   = len(sep)
    step = bs if fixed else 1
    if not bs:
        raise ValueError("Zero-length separator.")
    try:
        o = f.seek(0, SEEK_END)
        o = f.seek(o-bs-step)    # - Ignore trailing delimiter 'sep'.
        while f.read(bs) != sep: # - Until reaching 'sep': Read sep-sized block
            o = f.seek(o-step)   #  and then seek to the block to read next.
    except (OSError,ValueError): # - Beginning of file reached.
        f.seek(0)
    return f.read()

def test_readlast():
    from io import BytesIO, StringIO
    
    # Text mode.
    f = StringIO("first\nlast\n")
    assert readlast(f, "\n") == "last\n"
    
    # Bytes.
    f = BytesIO(b'first|last')
    assert readlast(f, b'|') == b'last'
    
    # Bytes, UTF-8.
    f = BytesIO("X\nY\n".encode("utf-8"))
    assert readlast(f, b'\n').decode() == "Y\n"
    
    # Bytes, UTF-16.
    f = BytesIO("X\nY\n".encode("utf-16"))
    assert readlast(f, b'\n\x00').decode('utf-16') == "Y\n"
  
    # Bytes, UTF-32.
    f = BytesIO("X\nY\n".encode("utf-32"))
    assert readlast(f, b'\n\x00\x00\x00').decode('utf-32') == "Y\n"
    
    # Multichar delimiter.
    f = StringIO("X<br>Y")
    assert readlast(f, "<br>", fixed=False) == "Y"
    
    # Make sure you use the correct delimiters.
    seps = { 'utf8': b'\n', 'utf16': b'\n\x00', 'utf32': b'\n\x00\x00\x00' }
    assert "\n".encode('utf8' )     == seps['utf8']
    assert "\n".encode('utf16')[2:] == seps['utf16']
    assert "\n".encode('utf32')[4:] == seps['utf32']
    
    # Edge cases.
    edges = (
        # Text , Match
        (""    , ""  ), # Empty file, empty string.
        ("X"   , "X" ), # No delimiter, full content.
        ("\n"  , "\n"),
        ("\n\n", "\n"),
        # UTF16/32 encoded U+270A (b"\n\x00\n'\n\x00"/utf16)
        (b'\n\xe2\x9c\x8a\n'.decode(), b'\xe2\x9c\x8a\n'.decode()),
    )
    for txt, match in edges:
        for enc,sep in seps.items():
            assert readlast(BytesIO(txt.encode(enc)), sep).decode(enc) == match

if __name__ == "__main__":
    import sys
    for path in sys.argv[1:]:
        with open(path) as f:
            print(f.readline()    , end="")
            print(readlast(f,"\n"), end="")

4
Це найкоротше рішення, і мені це подобається. Приємна частина не вгадування великого розміру полягає в тому, що він добре працює з невеликими тестовими файлами. Я додав кілька рядків і завернув у функцію, яку я залюбки називаю tail_n.
MarkHu

1
Я люблю це на папері, але не можу працювати. File "mapper1.2.2.py", line 17, in get_last_line f.seek(-2, 2) IOError: [Errno 22] Invalid argument
Loïc

2
Неважливо, файл порожній, дерп. Краща відповідь у будь-якому випадку. +1
Лоїк,

2
Відповідно до цього коментаря як відповіді, це while f.read(1) != "\n":повинно бутиwhile f.read(1) != b"\n":
Артжом Б.

4
Також для запису: Якщо ви отримаєте виняток io.UnsupportedOperation: can't do nonzero end-relative seeks, ви повинні зробити це у два етапи: спочатку знайти довжину файлу, потім додати зміщення, а потім передати цеf.seek(size+offset,os.SEEK_SET)
AnotherParker

25

Ось модифікована версія відповіді SilentGhost, яка буде робити те, що ви хочете.

with open(fname, 'rb') as fh:
    first = next(fh)
    offs = -100
    while True:
        fh.seek(offs, 2)
        lines = fh.readlines()
        if len(lines)>1:
            last = lines[-1]
            break
        offs *= 2
    print first
    print last

Тут не потрібно верхньої межі довжини рядка.


10

Чи можете ви використовувати команди unix? Я думаю , використовуючи head -1і tail -n 1, ймовірно, найбільш ефективні методи. Крім того, ви можете використовувати простий, fid.readline()щоб отримати перший рядок і fid.readlines()[-1], але це може зайняти занадто багато пам'яті.


Хм, тоді створення підпроцесу для виконання цих команд буде найефективнішим способом?
pasbino

10
Якщо у вас є unix, тоді os.popen("tail -n 1 %s" % filename).read()останній рядок отримується чудово.
Michael Dunn,

1
+1 для голови -1 і хвоста -1. fid.readlines () [- 1] не є гарним рішенням для величезних файлів.
Joao Figueiredo

os.popen("tail -n 1 %s" % filename).read()-> Не застосовується з версії 2.6
LarsVegas,

6

Це моє рішення, сумісне також з Python3. Він також управляє прикордонними справами, але він втрачає підтримку utf-16:

def tail(filepath):
    """
    @author Marco Sulla (marcosullaroma@gmail.com)
    @date May 31, 2016
    """

    try:
        filepath.is_file
        fp = str(filepath)
    except AttributeError:
        fp = filepath

    with open(fp, "rb") as f:
        size = os.stat(fp).st_size
        start_pos = 0 if size - 1 < 0 else size - 1

        if start_pos != 0:
            f.seek(start_pos)
            char = f.read(1)

            if char == b"\n":
                start_pos -= 1
                f.seek(start_pos)

            if start_pos == 0:
                f.seek(start_pos)
            else:
                char = ""

                for pos in range(start_pos, -1, -1):
                    f.seek(pos)

                    char = f.read(1)

                    if char == b"\n":
                        break

        return f.readline()

Це ispired від відповіді Trasp в і коментарі AnotherParker в .


4

Спочатку відкрийте файл у режимі читання. Потім використовуйте метод readlines () для читання рядків за рядком. Всі рядки, що зберігаються у списку. Тепер ви можете використовувати фрагменти списку, щоб отримати перший і останній рядки файлу.

    a=open('file.txt','rb')
    lines = a.readlines()
    if lines:
        first_line = lines[:1]
        last_line = lines[-1]

1
Я шукав саме це, мені не потрібні перший та останній рядки, тому рядки [1, -2] дають текст між заголовком та колонтитулом.
guneysus

4
Цей параметр не може обробляти порожні файли.
un33k

8
І аварії для дуже великих файлів
akarapatis

4
w=open(file.txt, 'r')
print ('first line is : ',w.readline())
for line in w:  
    x= line
print ('last line is : ',x)
w.close()

forПетля проходить через лінію і xотримує останній рядок на останню ітерацію.


Це має бути прийнятою відповіддю. Я не знаю, чому в інших відповідях все це возиться з низьким рівнем io?
GreenAsJade

3
@GreenAsJade Я розумію, що "возитися" полягає у тому, щоб не читати весь файл від початку до кінця. Це може бути неефективним для великого файлу.
бли

3
with open("myfile.txt") as f:
    lines = f.readlines()
    first_row = lines[0]
    print first_row
    last_row = lines[-1]
    print last_row

Чи можете ви пояснити, чому ваше рішення буде кращим?
Зулу

Привіт, я опинився в тій же необхідності, щоб видалити останню кому на рівні останнього рядка в текстовому файлі, і таким чином я вирішив легко її знайти; Тоді я подумав поділитися цим. Це рішення було простим, практичним і негайним, але я не знаю, чи є воно найшвидшим з точки зору ефективності. Що ви можете мені сказати про це?
Ріккардо Вольпе

Ну, він повинен прочитати та обробити весь файл, тому це здається найменш ефективним способом.
rakslice

Гаразд ... так, якщо ви не знаєте довжину рядка, який найкращий метод? Мені потрібно спробувати інший ( stackoverflow.com/a/3346492/2149425 ). Дякую!
Ріккардо Вольпе,

1
використовувати f.readlines()[-1]інсеад нової змінної. 0 = перший рядок , 1 = другий рядок , -1 = останній рядок , -2 = рядок перед останнім рядком ...
BladeMight

2

Ось розширення відповіді @ Trasp, яке має додаткову логіку для обробки кутового випадку файлу, який має лише один рядок. Це може бути корисним для розгляду цієї справи, якщо ви неодноразово бажаєте прочитати останній рядок файлу, який постійно оновлюється. Без цього, якщо ви спробуєте захопити останній рядок щойно створеного файлу, який має лише один рядок, IOError: [Errno 22] Invalid argumentбуде піднято.

def tail(filepath):
    with open(filepath, "rb") as f:
        first = f.readline()      # Read the first line.
        f.seek(-2, 2)             # Jump to the second last byte.
        while f.read(1) != b"\n": # Until EOL is found...
            try:
                f.seek(-2, 1)     # ...jump back the read byte plus one more.
            except IOError:
                f.seek(-1, 1)
                if f.tell() == 0:
                    break
        last = f.readline()       # Read last line.
    return last

2

Ніхто не згадував використання зворотного:

f=open(file,"r")
r=reversed(f.readlines())
last_line_of_file = r.next()

5
.readlines () прочитає всі рядки з файлу в пам’ять за один раз - це не вирішення цієї проблеми
Стів Мейн,

1

Отримати перший рядок тривіально просто. Для останнього рядка, припускаючи, що ви знаєте приблизну верхню межу довжини рядка, os.lseek деяку суму від SEEK_ENDзнаходження другого до останнього рядка, що закінчується, а потім readline () останнього рядка.


У мене немає приблизної верхньої межі довжини лінії
pasbino

1
with open(filename, "rb") as f:#Needs to be in binary mode for the seek from the end to work
    first = f.readline()
    if f.read(1) == '':
        return first
    f.seek(-2, 2)  # Jump to the second last byte.
    while f.read(1) != b"\n":  # Until EOL is found...
        f.seek(-2, 1)  # ...jump back the read byte plus one more.
    last = f.readline()  # Read last line.
    return last

Наведена вище відповідь є модифікованою версією вищезазначених відповідей, яка обробляє випадок, що у файлі є лише один рядок

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.