Wie springe ich zu einer bestimmten Zeile in einer riesigen Textdatei?

107

Gibt es Alternativen zum folgenden Code:

startFromLine = 141978 # or whatever line I need to jump to

urlsfile = open(filename, "rb", 0)

linesCounter = 1

for line in urlsfile:
    if linesCounter > startFromLine:
        DoSomethingWithThisLine(line)

    linesCounter += 1

Wenn ich eine große Textdatei (~15MB)mit Zeilen unbekannter, aber unterschiedlicher Länge verarbeite und zu einer bestimmten Zeile springen muss, welche Nummer kenne ich im Voraus? Ich fühle mich schlecht, wenn ich sie einzeln verarbeite, wenn ich weiß, dass ich mindestens die erste Hälfte der Datei ignorieren kann. Suchen Sie nach einer eleganteren Lösung, wenn es welche gibt.

user63503
quelle
Woher wissen Sie, dass die erste Hälfte der Datei keine "\ n" ist, während die zweite Hälfte eine einzelne Zeile ist? Warum fühlst du dich schlecht dabei?
Andrew Dalke
7
Ich denke, dass der Titel irreführend ist - tbh 15MB ist nicht wirklich "riesige Textdatei", um es gelinde
auszudrücken

Antworten:

30

Zeilencache :

Mit dem linecacheModul kann eine beliebige Zeile aus einer Python-Quelldatei abgerufen werden, während versucht wird, die Verwendung mithilfe eines Caches intern zu optimieren. Dies ist der übliche Fall, in dem viele Zeilen aus einer einzelnen Datei gelesen werden. Dies wird vom tracebackModul verwendet, um Quellzeilen zur Aufnahme in den formatierten Traceback abzurufen ...

John Ellinwood
quelle
164
Ich habe gerade den Quellcode dieses Moduls überprüft: Die gesamte Datei wird im Speicher gelesen! Daher würde ich diese Antwort definitiv ausschließen, um schnell auf eine bestimmte Zeile in einer Datei zugreifen zu können.
MiniQuark
MiniQuark, ich habe es versucht, es funktioniert tatsächlich und sehr schnell. Ich muss sehen, was passiert, wenn ich auf diese Weise gleichzeitig an einem Dutzend Dateien arbeite. Finden Sie heraus, an welchem ​​Punkt mein System stirbt.
user63503
5
Der virtuelle Speichermanager Ihres Betriebssystems hilft einiges, sodass das Einlesen großer Dateien in den Speicher möglicherweise nicht langsam ist, wenn Sie nicht viele Seitenfehler erzeugen :) Im Gegenteil, tun Sie dies auf "dumme Weise" und weisen Sie viele, viele zu der Erinnerung kann blitzschnell sein. Ich habe den Artikel des dänischen FreeBSD-Entwicklers Poul-Henning Kamp genossen: queue.acm.org/detail.cfm?id=1814327
Morten Jensen
13
versuchen Sie 100G-Datei, es ist scheiße. Ich muss f.tell (), f.seek (), f.readline () verwenden
bis zum
114

Sie können nicht weitermachen, ohne die Datei mindestens einmal eingelesen zu haben, da Sie nicht wissen, wo sich die Zeilenumbrüche befinden. Sie könnten so etwas tun wie:

# Read in the file once and build a list of line offsets
line_offset = []
offset = 0
for line in file:
    line_offset.append(offset)
    offset += len(line)
file.seek(0)

# Now, to skip to line n (with the first line being line 0), just do
file.seek(line_offset[n])
Adam Rosenfield
quelle
2
+1, aber pass auf, dass dies nur nützlich ist, wenn er zu mehreren zufälligen Zeilen springt! aber wenn er nur zu einer Zeile springt, dann ist das verschwenderisch
hasen
3
+1: Wenn sich die Datei nicht ändert, kann der Zeilennummernindex ausgewählt und wiederverwendet werden, wodurch sich die anfänglichen Kosten für das Scannen der Datei weiter amortisieren.
S.Lott
OK, nachdem ich dorthin gesprungen bin, wie würde ich dann ab dieser Position Zeile für Zeile verarbeiten?
user63503
8
Beachten Sie Folgendes (insbesondere unter Windows): Öffnen Sie die Datei vorsichtig im Binärmodus oder verwenden Sie alternativ offset = file.tell (). Im Textmodus unter Windows ist die Zeile ein Byte kürzer als die unformatierte Länge auf der Festplatte (\ r \ n ersetzt durch \ n)
Brian
2
@photographer: Verwenden Sie read () oder readline (). Sie beginnen an der aktuellen Position, die durch seek festgelegt wurde.
S.Lott
22

Sie haben nicht wirklich so viele Optionen, wenn die Zeilen unterschiedlich lang sind. Leider müssen Sie die Zeichen am Zeilenende verarbeiten, um zu wissen, wann Sie zur nächsten Zeile übergegangen sind.

Sie können dies jedoch drastisch beschleunigen UND die Speichernutzung reduzieren, indem Sie den letzten Parameter in "Öffnen" auf etwas ändern, das nicht 0 ist.

0 bedeutet, dass der Dateilesevorgang ungepuffert ist, was sehr langsam und festplattenintensiv ist. 1 bedeutet, dass die Datei zeilengepuffert ist, was eine Verbesserung wäre. Alles über 1 (z. B. 8 KB, dh 8096 oder höher) liest Teile der Datei in den Speicher. Sie greifen immer noch über zu for line in open(etc):, aber Python geht immer nur ein bisschen auf einmal und verwirft jeden gepufferten Block nach seiner Verarbeitung.

Jarret Hardie
quelle
6
8K ist 8192, vielleicht ist es besser, 8 << 10 zu schreiben, um auf der sicheren Seite zu sein. :)
Entspannen Sie am
Wissen Sie zufällig, dass die Puffergröße für Bytes angegeben ist? Was sind geeignete Formate? Könnte ich '8k' schreiben? Oder sollte es '8096' sein?
user63503
1
HAHAHA ... muss Freitag sein ... Ich kann eindeutig nicht rechnen. Die Puffergröße ist in der Tat eine Ganzzahl, die Bytes ausdrückt. Schreiben Sie also 8192 (nicht 8096 :-)) und nicht 8
Jarret Hardie
Es ist mir ein Vergnügen - ich hoffe es klappt. Auf einem modernen System können Sie die Puffergröße wahrscheinlich erheblich erhöhen. 8k ist aus irgendeinem Grund, den ich nicht identifizieren kann, nur ein Überbleibsel in meinem Gedächtnis.
Jarret Hardie
Ich habe hier einige Tests durchgeführt und es auf -1 zu setzen (os default, oft 8k, aber oft schwer zu sagen), scheint so schnell wie möglich zu sein. Ein Teil davon könnte sein, dass ich auf einem virtuellen Server teste.
Oscar Smith
12

Ich bin wahrscheinlich von reichlich Widder verwöhnt, aber 15 M sind nicht riesig. readlines() Mit Dateien dieser Größe lese ich normalerweise in den Speicher . Der Zugriff auf eine Leitung danach ist trivial.

SilentGhost
quelle
Warum ich etwas zögerte, die gesamte Datei zu lesen - möglicherweise laufen mehrere dieser Prozesse, und wenn ein Dutzend davon 12 Dateien mit jeweils 15 MB liest, kann dies nicht gut sein. Aber ich muss es testen, um herauszufinden, ob es funktionieren wird. Danke dir.
user63503
4
Hrm, und was ist, wenn es sich um eine 1-GB-Datei handelt?
Noah
@photographer: Selbst "mehrere" Prozesse, die 15-MB-Dateien einlesen, sollten auf einem typischen modernen Computer keine Rolle spielen (natürlich abhängig davon, was Sie genau damit machen).
Jacob Gabrielson
Jacob, ja, ich sollte es einfach versuchen. Die Prozesse werden wochenlang auf einer virtuellen Maschine ausgeführt, wenn vm nicht abgestürzt ist. Leider ist es beim letzten Mal nach 6 Tagen abgestürzt. Ich muss dort weitermachen, wo es plötzlich aufgehört hat. Ich muss noch herausfinden, wo ich es gefunden habe.
user63503
@Noah: aber es ist nicht! Warum gehst du nicht weiter? Was ist, wenn Datei 128 TB? Dann könnten viele Betriebssysteme dies nicht unterstützen. Warum nicht das Problem so lösen, wie es kommt?
SilentGhost
7

Ich bin überrascht, dass niemand Islice erwähnt hat

line = next(itertools.islice(Fhandle,index_of_interest,index_of_interest+1),None) # just the one line

oder wenn Sie den gesamten Rest der Datei möchten

rest_of_file = itertools.islice(Fhandle,index_of_interest)
for line in rest_of_file:
    print line

oder wenn Sie jede zweite Zeile aus der Datei möchten

rest_of_file = itertools.islice(Fhandle,index_of_interest,None,2)
for odd_line in rest_of_file:
    print odd_line
Joran Beasley
quelle
5

Da es keine Möglichkeit gibt, die Länge aller Zeilen zu bestimmen, ohne sie zu lesen, haben Sie keine andere Wahl, als alle Zeilen vor Ihrer Startzeile zu durchlaufen. Alles, was Sie tun können, ist, es schön aussehen zu lassen. Wenn die Datei wirklich sehr groß ist, möchten Sie möglicherweise einen generatorbasierten Ansatz verwenden:

from itertools import dropwhile

def iterate_from_line(f, start_from_line):
    return (l for i, l in dropwhile(lambda x: x[0] < start_from_line, enumerate(f)))

for line in iterate_from_line(open(filename, "r", 0), 141978):
    DoSomethingWithThisLine(line)

Hinweis: Der Index ist bei diesem Ansatz Null.


quelle
4

Wenn Sie nicht die gesamte Datei im Speicher lesen möchten, müssen Sie möglicherweise ein anderes Format als Nur-Text erstellen.

Natürlich hängt alles davon ab, was Sie versuchen und wie oft Sie über die Datei springen.

Wenn Sie beispielsweise mehrmals in derselben Datei zu Zeilen springen und wissen, dass sich die Datei während der Arbeit nicht ändert, können Sie Folgendes tun:
Gehen Sie zunächst die gesamte Datei durch und zeichnen Sie das " Suchort "einiger Schlüsselzeilennummern (z. B. je 1000 Zeilen).
Wenn Sie dann Zeile 12005 möchten, springen Sie zur Position 12000 (die Sie aufgezeichnet haben), lesen Sie dann 5 Zeilen und Sie werden Sie kennen Ich bin in der Linie 12005 und so weiter

hasen
quelle
3

Wenn Sie die Position in der Datei (statt der Zeilennummer) im Voraus kennen, können Sie mit file.seek () zu dieser Position wechseln .

Bearbeiten : Sie können die Funktion linecache.getline (Dateiname, Leinen) verwenden, die den Inhalt der Zeile Leinen zurückgibt, jedoch erst, nachdem die gesamte Datei in den Speicher gelesen wurde. Gut, wenn Sie zufällig auf Zeilen aus der Datei zugreifen (wie Python selbst möglicherweise einen Traceback drucken möchte), aber nicht gut für eine 15-MB-Datei.

Noah
quelle
Ich würde definitiv keinen Linecache für diesen Zweck verwenden, da er die gesamte Datei im Speicher liest, bevor die angeforderte Zeile zurückgegeben wird.
MiniQuark
Ja, es klang zu gut um wahr zu sein. Ich wünschte immer noch, es gäbe ein Modul, um dies effizient zu erledigen, würde aber stattdessen eher die file.seek () -Methode verwenden.
Noah
3

Was generiert die Datei, die Sie verarbeiten möchten? Wenn Sie dies kontrollieren, können Sie zum Zeitpunkt des Anhängens der Datei einen Index erstellen (welche Zeile sich an welcher Position befindet). Die Indexdatei kann eine feste Zeilengröße haben (mit Leerzeichen oder 0 aufgefüllte Zahlen) und ist definitiv kleiner. Und kann so schnell gelesen und verarbeitet werden.

  • Welche Linie willst du?
  • Berechnen Sie den Byte-Offset der entsprechenden Zeilennummer in der Indexdatei (möglich, da die Zeilengröße der Indexdatei konstant ist).
  • Verwenden Sie seek oder was auch immer, um direkt zu springen und die Zeile aus der Indexdatei abzurufen.
  • Analysieren, um den Byte-Offset für die entsprechende Zeile der tatsächlichen Datei zu erhalten.
kamathln
quelle
3

Ich hatte das gleiche Problem (muss aus einer großen dateispezifischen Zeile abgerufen werden).

Sicherlich kann ich jedes Mal alle Datensätze in der Datei durchgehen und stoppen, wenn der Zähler gleich der Zielzeile ist, aber es funktioniert nicht effektiv in einem Fall, in dem Sie mehrere bestimmte Zeilen erhalten möchten. Dies führte dazu, dass das Hauptproblem gelöst wurde - wie direkt zum erforderlichen Speicherort der Datei umgegangen werden kann.

Ich fand die nächste Entscheidung heraus: Zuerst vervollständigte ich das Wörterbuch mit der Startposition jeder Zeile (Schlüssel ist die Zeilennummer und die wertkumulierte Länge der vorherigen Zeilen).

t = open(file,’r’)
dict_pos = {}

kolvo = 0
length = 0
for each in t:
    dict_pos[kolvo] = length
    length = length+len(each)
    kolvo = kolvo+1

letztendlich Zielfunktion:

def give_line(line_number):
    t.seek(dict_pos.get(line_number))
    line = t.readline()
    return line

t.seek (line_number) - Befehl, der das Bereinigen der Datei bis zum Zeilenbeginn ausführt. Wenn Sie also das nächste Mal eine Readline festschreiben, erhalten Sie Ihre Zielzeile.

Mit diesem Ansatz habe ich einen erheblichen Teil der Zeit gespart.

user3810114
quelle
3

Sie können mmap verwenden, um den Versatz der Linien zu ermitteln. MMap scheint der schnellste Weg zu sein, eine Datei zu verarbeiten

Beispiel:

with open('input_file', "r+b") as f:
    mapped = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ)
    i = 1
    for line in iter(mapped.readline, ""):
        if i == Line_I_want_to_jump:
            offsets = mapped.tell()
        i+=1

Verwenden Sie dann f.seek (Offsets), um zu der gewünschten Zeile zu gelangen

George
quelle
2

Enthalten die Zeilen selbst Indexinformationen? Wenn der Inhalt jeder Zeile so etwas wie " <line index>:Data" war, seek()könnte der Ansatz verwendet werden, um eine binäre Suche in der Datei durchzuführen, selbst wenn die Menge vonData variabel ist. Sie würden versuchen, den Mittelpunkt der Datei zu erreichen, eine Zeile lesen, prüfen, ob der Index höher oder niedriger als der gewünschte ist usw.

Ansonsten ist das Beste, was Sie tun können, nur readlines(). Wenn Sie nicht alle 15 MB lesen möchten, können Sie das sizehintArgument verwenden, um mindestens viele readline()s durch eine geringere Anzahl von Aufrufen zu zu ersetzen readlines().

DNS
quelle
2

Wenn Sie mit einer Textdatei arbeiten und auf einem Linux-System basieren , können Sie die Linux-Befehle verwenden.
Für mich hat das gut funktioniert!

import commands

def read_line(path, line=1):
    return commands.getoutput('head -%s %s | tail -1' % (line, path))

line_to_jump = 141978
read_line("path_to_large_text_file", line_to_jump)
HongKun Yoo
quelle
Natürlich ist es nicht kompatibel mit Windows oder einer Art Linux-Shells, die Head / Tail nicht unterstützen.
Wizmann
Ist das schneller als in Python?
Shamoon
Kann dies mehrere Zeilen bekommen?
Shamoon
1

Hier ist ein Beispiel mit 'readlines (sizehint)', um einen Teil der Zeilen gleichzeitig zu lesen. DNS wies auf diese Lösung hin. Ich habe dieses Beispiel geschrieben, weil die anderen Beispiele hier einzeilig sind.

def getlineno(filename, lineno):
    if lineno < 1:
        raise TypeError("First line is line 1")
    f = open(filename)
    lines_read = 0
    while 1:
        lines = f.readlines(100000)
        if not lines:
            return None
        if lines_read + len(lines) >= lineno:
            return lines[lineno-lines_read-1]
        lines_read += len(lines)

print getlineno("nci_09425001_09450000.smi", 12000)
Andrew Dalke
quelle
0

Keine der Antworten ist besonders zufriedenstellend. Hier ist ein kleiner Ausschnitt, der Ihnen helfen soll.

class LineSeekableFile:
    def __init__(self, seekable):
        self.fin = seekable
        self.line_map = list() # Map from line index -> file position.
        self.line_map.append(0)
        while seekable.readline():
            self.line_map.append(seekable.tell())

    def __getitem__(self, index):
        # NOTE: This assumes that you're not reading the file sequentially.  
        # For that, just use 'for line in file'.
        self.fin.seek(self.line_map[index])
        return self.fin.readline()

Anwendungsbeispiel:

In: !cat /tmp/test.txt

Out:
Line zero.
Line one!

Line three.
End of file, line four.

In:
with open("/tmp/test.txt", 'rt') as fin:
    seeker = LineSeekableFile(fin)    
    print(seeker[1])
Out:
Line one!

Dies beinhaltet viele Dateisuchen, ist jedoch nützlich für Fälle, in denen Sie nicht die gesamte Datei in den Speicher einpassen können. Es führt einen ersten Lesevorgang durch, um die Zeilenpositionen abzurufen (es liest also die gesamte Datei, behält aber nicht alles im Speicher), und dann führt jeder Zugriff eine Dateisuche nach der Tatsache durch.

Ich biete das obige Snippet unter der MIT- oder Apache-Lizenz nach Ermessen des Benutzers an.

Joseph Catrambone
quelle
-1

Kann diese Funktion verwenden, um Zeile n zurückzugeben:

def skipton(infile, n):
    with open(infile,'r') as fi:
        for i in range(n-1):
            fi.next()
        return fi.next()
ksed
quelle
Diese Logik funktioniert nicht, wenn es fortlaufende leere Zeilen gibt, fi.next () überspringt alle leeren Zeilen auf einmal, sonst ist es gut :)
Anvesh Yalamarthy
Das OP erwähnt nicht, dass die Zeilen Zeilen mit nicht standardmäßigen Zeilenumbrüchen haben. In diesem Fall müssten Sie jede Zeile mit mindestens einer if-Anweisung für die teilweisen Zeilenumbrüche analysieren.
ksed