Was ist der effizienteste Weg, um die erste und letzte Zeile einer Textdatei zu erhalten?

74

Ich habe eine Textdatei, die in jeder Zeile einen Zeitstempel enthält. Mein Ziel ist es, den Zeitbereich zu finden. Alle Zeiten sind in Ordnung, so dass die erste Zeile die früheste Zeit und die letzte Zeile die späteste Zeit ist. Ich brauche nur die allererste und letzte Zeile. Was wäre der effizienteste Weg, um diese Zeilen in Python zu bekommen?

Hinweis: Diese Dateien sind relativ groß, jeweils etwa 1 bis 2 Millionen Zeilen, und ich muss dies für mehrere hundert Dateien tun.

Pasbino
quelle

Antworten:

62

Dokumente für das io-Modul

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

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

Der variable Wert ist hier 1024: Er repräsentiert die durchschnittliche Zeichenfolgenlänge. Ich wähle zum Beispiel nur 1024. Wenn Sie eine Schätzung der durchschnittlichen Zeilenlänge haben, können Sie diesen Wert einfach mal 2 verwenden.

Da Sie keinerlei Ahnung von der möglichen Obergrenze für die Zeilenlänge haben, besteht die offensichtliche Lösung darin, die Datei zu durchlaufen:

for line in fh:
    pass
last = line

Sie müssen sich nicht um das Binärflag kümmern, das Sie einfach verwenden könnten open(fname).

ETA : Da Sie viele Dateien bearbeiten müssen, können Sie ein Beispiel für ein paar Dutzend Dateien erstellen random.sampleund diesen Code ausführen, um die Länge der letzten Zeile zu bestimmen. Mit einem a priori großen Wert der Positionsverschiebung (sagen wir 1 MB). Auf diese Weise können Sie den Wert für den gesamten Lauf schätzen.

SilentGhost
quelle
Solange die Zeilen nicht länger als 1024 Zeichen sind.
FogleBird
Es gibt keine Garantie dafür, dass die Zeilen nicht länger als 1024 Zeichen sind. Neben den Zeitstempeln in der Zeile gibt es möglicherweise noch anderen Müll.
Pasbino
@pasbino: Hast du einige obere gebunden?
SilentGhost
18
Die Verwendung fh.seek(-1024, os.SEEK_END)anstelle von fh.seek(-1024, 2)erhöht die Lesbarkeit.
Marsl
2
Folgendes ist nicht wahr: Sie müssen sich nicht mit dem Binärflag beschäftigen, das Sie nur verwenden könnten open(fname). Das Öffnen mit der bFlagge ist entscheidend. Wenn Sie open(fname)statt open(fname, 'rb')Sie erhalten io.UnsupportedOperation: kann nicht von Null verschiedene End-Relativ sucht .
patryk.beza
87

Um sowohl die erste als auch die letzte Zeile einer Datei zu lesen, können Sie ...

  • öffne die Datei, ...
  • ... lesen Sie die erste Zeile mit eingebauten readline(), ...
  • ... suchen (den Cursor bewegen) bis zum Ende der Datei, ...
  • ... treten Sie zurück, bis Sie auf EOL (Zeilenumbruch) stoßen und ...
  • ... lesen Sie die letzte Zeile von dort.
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)

Wechsel zu dem zweiten letzten Byte unmittelbar nachlaufZeilenUmbrüche zu verhindern Leerzeilen zu bewirken , zurückgeführt wird *.

Der aktuelle Offset wird jedes Mal, wenn ein Byte gelesen wird, um eins vorgeschoben, sodass der Rückschritt zwei Bytes gleichzeitig erfolgt, nach dem zuletzt gelesenen Byte und dem als nächstes zu lesenden Byte.

Der übergebene whenceParameter gibt an, fseek(offset, whence=0)dass fseeknach Positionsbytes offsetrelativ zu ... gesucht werden soll.

* Wie zu erwarten ist, besteht das Standardverhalten der meisten Anwendungen, einschließlich printund echo, darin, an jede geschriebene Zeile eine anzuhängen, und hat keine Auswirkung auf Zeilen, denen ein nachfolgendes Zeilenumbruchzeichen fehlt.


Effizienz

Jeweils 1-2 Millionen Zeilen und ich muss dies für mehrere hundert Dateien tun.

Ich habe diese Methode zeitlich festgelegt und mit der Top-Antwort verglichen.

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.

Millionen von Zeilen würden den Unterschied viel mehr erhöhen .

Exakt-Code für das Timing:

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.

Änderung

Eine komplexere und schwerer zu lesende Variante, um Kommentare und Probleme anzusprechen, die seitdem aufgeworfen wurden.

Fügt auch Unterstützung für Multibyte-Trennzeichen hinzu readlast(b'X<br>Y', b'<br>', fixed=False).

Bitte beachten Sie, dass diese Variation für große Dateien aufgrund der im Textmodus erforderlichen nicht relativen Offsets sehr langsam ist . Passen Sie es an Ihre Bedürfnisse an oder verwenden Sie es überhaupt nicht, da Sie es wahrscheinlich besser verwenden, wenn Sie f.readlines()[-1]Dateien im Textmodus öffnen.

#!/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="")
Trasp
quelle
4
Dies ist die prägnanteste Lösung, und ich mag es. Das Schöne daran, eine Blockgröße nicht zu erraten, ist, dass sie gut mit kleinen Testdateien funktioniert. Ich fügte ein paar Zeilen hinzu und wickelte es in eine Funktion ein, die ich liebevoll aufrufe tail_n.
MarkHu
1
Ich liebe es auf dem Papier, kann es aber nicht zum Laufen bringen. File "mapper1.2.2.py", line 17, in get_last_line f.seek(-2, 2) IOError: [Errno 22] Invalid argument
Loïc
2
Egal, die Datei war leer, derp. Beste Antwort trotzdem. +1
Loïc
2
Gemäß diesem Kommentar als Antwort, dies while f.read(1) != "\n":solltewhile f.read(1) != b"\n":
Artjom B.
4
Ebenfalls für den Datensatz: Wenn Sie die Ausnahme erhalten io.UnsupportedOperation: can't do nonzero end-relative seeks, müssen Sie dies in zwei Schritten tun: f.seek(size+offset,os.SEEK_SET)
Ermitteln Sie
25

Hier ist eine modifizierte Version der Antwort von SilentGhost, die macht, was Sie wollen.

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

Hier ist keine Obergrenze für die Zeilenlänge erforderlich.

mik01aj
quelle
10

Können Sie Unix-Befehle verwenden? Ich denke mit head -1und tail -n 1sind wahrscheinlich die effizientesten Methoden. Alternativ können Sie eine einfache verwenden fid.readline(), um die erste Zeile zu erhalten fid.readlines()[-1], aber das kann zu viel Speicherplatz beanspruchen.

beitar
quelle
Hmm, wäre das Erstellen eines Unterprozesses zum Ausführen dieser Befehle dann der effizienteste Weg?
Pasbino
10
Wenn Sie Unix haben, dann os.popen("tail -n 1 %s" % filename).read()bekommt die letzte Zeile schön.
Michael Dunn
1
+1 für Kopf -1 und Schwanz -1. fid.readlines () [- 1] ist keine gute Lösung für große Dateien.
Joao Figueiredo
os.popen("tail -n 1 %s" % filename).read()-> Veraltet seit Version 2.6
LarsVegas
6

Dies ist meine Lösung, die auch mit Python3 kompatibel ist. Es verwaltet auch Grenzfälle, aber es fehlt die Unterstützung von utf-16:

def tail(filepath):
    """
    @author Marco Sulla ([email protected])
    @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()

Es wird durch Trasps Antwort und AnotherParkers Kommentar inspiriert .

Marco Sulla
quelle
4

Öffnen Sie zuerst die Datei im Lesemodus. Verwenden Sie dann die readlines () -Methode, um zeilenweise zu lesen. Alle in einer Liste gespeicherten Zeilen. Jetzt können Sie Listenschnitte verwenden, um die ersten und letzten Zeilen der Datei abzurufen.

    a=open('file.txt','rb')
    lines = a.readlines()
    if lines:
        first_line = lines[:1]
        last_line = lines[-1]
Srinivasreddy Jakkireddy
quelle
1
Ich habe genau das gesucht, ich brauche keine erste und letzte Zeile, also geben die Zeilen [1, -2] den Text zwischen Titel und Fußzeile an.
Guneysos
4
Diese Option kann keine leeren Dateien verarbeiten.
Un33k
8
Und stürzt für sehr große Dateien ab
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()

Die forSchleife verläuft durch die Zeilen und xerhält die letzte Zeile bei der letzten Iteration.

Viper
quelle
Dies sollte die akzeptierte Antwort sein. Ich weiß nicht, warum in den anderen Antworten all das mit Low-Level-Io herumgespielt wird?
GreenAsJade
3
@GreenAsJade Mein Verständnis ist, dass das "Herumspielen" darin besteht, das Lesen der gesamten Datei von Anfang bis Ende zu vermeiden. Dies kann bei einer großen Datei ineffizient sein.
bli
3
with open("myfile.txt") as f:
    lines = f.readlines()
    first_row = lines[0]
    print first_row
    last_row = lines[-1]
    print last_row
Riccardo Volpe
quelle
Können Sie erklären, warum Ihre Lösung besser sein wird?
Zulu
Hallo, ich hatte das gleiche Bedürfnis, das letzte Komma auf der Ebene der letzten Zeile in einer Textdatei zu entfernen, und auf diese Weise habe ich es gelöst, um es leicht zu finden. Ich dachte damals, es zu teilen. Diese Lösung war einfach, praktisch und unmittelbar, aber ich weiß nicht, ob sie hinsichtlich der Effizienz die schnellste ist. Was kannst du mir darüber erzählen?
Riccardo Volpe
Nun, es muss die gesamte Datei lesen und verarbeiten, damit es am wenigsten effizient erscheint.
Rakslice
Ok ... also, wenn Sie die Stringlänge nicht kennen, welche wäre die beste Methode? Ich muss den anderen ausprobieren ( stackoverflow.com/a/3346492/2149425 ). Vielen Dank!
Riccardo Volpe
1
Verwenden Sie f.readlines()[-1]Insead der neuen Variablen. 0 = Erste Zeile , 1 = Zweite Zeile , -1 = Letzte Zeile , -2 = Zeile vor der letzten Zeile ...
BladeMight
2

Hier ist eine Erweiterung der Antwort von @ Trasp, die zusätzliche Logik für die Behandlung des Eckfalls einer Datei mit nur einer Zeile enthält. Es kann hilfreich sein, diesen Fall zu behandeln, wenn Sie wiederholt die letzte Zeile einer Datei lesen möchten, die ständig aktualisiert wird. Wenn Sie ohne dies versuchen, die letzte Zeile einer Datei zu erfassen, die gerade erstellt wurde und nur eine Zeile enthält, IOError: [Errno 22] Invalid argumentwird dies ausgelöst.

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
tony_tiger
quelle
2

Niemand erwähnte die Verwendung von umgekehrt:

f=open(file,"r")
r=reversed(f.readlines())
last_line_of_file = r.next()
Michael Meanswell
quelle
5
.readlines () liest alle Zeilen aus der Datei auf einmal in den Speicher - es ist keine Lösung für dieses Problem
Steve Mayne
1

Die erste Zeile zu bekommen ist trivial einfach. Angenommen , Sie kennen eine ungefähre Obergrenze für die Zeilenlänge, suchen Sie für die letzte Zeile einen gewissen Betrag , indemSEEK_END Sie das Ende der vorletzten Zeile finden und dann die letzte Zeile readline () .

msw
quelle
Ich habe keine ungefähre Obergrenze für die Zeilenlänge
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

Die obige Antwort ist eine modifizierte Version der obigen Antworten, die den Fall behandelt, dass die Datei nur eine Zeile enthält

user37940
quelle