Come saltare a una riga particolare in un enorme file di testo?


107

Esistono alternative al codice seguente:

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

Se sto elaborando un file di testo enorme (~15MB)con righe di lunghezza sconosciuta ma diversa e devo saltare a una riga particolare quale numero conosco in anticipo? Mi sento male elaborandoli uno per uno quando so che potrei ignorare almeno la prima metà del file. Alla ricerca di una soluzione più elegante se ce n'è.


Come fai a sapere che la prima metà del file non è un mucchio di "\ n" mentre la seconda metà è una singola riga? Perché ti senti male per questo?
Andrew Dalke

7
Penso che il titolo sia fuorviante - tbh 15MB non è davvero un "enorme file di testo", per non dire altro ...
pms

Risposte:


30

linecache :

Il linecachemodulo consente di ottenere qualsiasi riga da un file sorgente Python, tentando di ottimizzarla internamente, utilizzando una cache, il caso comune in cui molte righe vengono lette da un singolo file. Viene utilizzato dal tracebackmodulo per recuperare le righe di origine da includere nel traceback formattato ...


165
Ho appena controllato il codice sorgente di questo modulo: l'intero file viene letto in memoria! Quindi escluderei definitivamente questa risposta allo scopo di accedere rapidamente a una determinata riga in un file.
MiniQuark

MiniQuark, l'ho provato, funziona davvero e molto velocemente. Avrò bisogno di vedere cosa succede se lavoro su una dozzina di file contemporaneamente in questo modo, scoprire a che punto il mio sistema muore.
user63503

5
Il gestore della memoria virtuale del tuo sistema operativo aiuta un bel po ', quindi leggere file di grandi dimensioni in memoria potrebbe non essere lento se non stai generando molti errori di pagina :) Al contrario, farlo in "modo stupido" e allocare un sacco e un sacco della memoria può essere incredibilmente veloce. Mi è piaciuto l'articolo dello sviluppatore danese di FreeBSD Poul-Henning Kamp su di esso: queue.acm.org/detail.cfm?id=1814327
Morten Jensen

13
prova il file 100G, fa schifo. devo usare f.tell (), f.seek (), f.readline ()
whi

114

Non puoi andare avanti senza leggere il file almeno una volta, poiché non sai dove sono le interruzioni di riga. Potresti fare qualcosa come:

# 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])

2
+1, ma attenzione che questo è utile solo se salterà su più righe casuali! ma se salta solo su una riga, allora è uno spreco
hasen

3
+1: Inoltre, se il file non cambia, l'indice del numero di riga può essere decapato e riutilizzato, ammortizzando ulteriormente il costo iniziale della scansione del file.
S.Lott

OK, dopo essere saltato lì come potrei elaborare quindi riga per riga a partire da questa posizione?
user63503

8
Una cosa da notare (in particolare su Windows): fai attenzione ad aprire il file in modalità binaria, o in alternativa usa offset = file.tell (). In modalità testo su Windows, la riga sarà un byte più corta della sua lunghezza non elaborata sul disco (\ r \ n sostituita da \ n)
Brian

2
@photographer: usa read () o readline (), iniziano dalla posizione corrente impostata da seek.
S.Lott

22

Non hai molte opzioni se le righe sono di lunghezza diversa ... purtroppo devi elaborare i caratteri finali della riga per sapere quando sei passato alla riga successiva.

Puoi, tuttavia, accelerare drasticamente questo E ridurre l'utilizzo della memoria modificando l'ultimo parametro in "open" con qualcosa che non è 0.

0 significa che l'operazione di lettura del file è senza buffer, che è molto lenta e richiede molto disco. 1 significa che il file ha un buffer di riga, il che sarebbe un miglioramento. Qualunque cosa al di sopra di 1 (diciamo 8k .. cioè: 8096, o superiore) legge parti del file in memoria. Puoi ancora accedervi attraverso for line in open(etc):, ma python va solo un po 'alla volta, scartando ogni pezzo memorizzato nel buffer dopo che è stato elaborato.


6
8K è 8192, forse meglio scrivere 8 << 10 per andare sul sicuro. :)
rilassati il

Sapete per caso che la dimensione del buffer è specificata in byte? Quali sono i formati appropriati? Potrei scrivere "8k"? O dovrebbe essere "8096"?
user63503

1
HAHAHA ... deve essere venerdì ... Chiaramente non posso fare matematica. La dimensione del buffer è effettivamente un numero intero che esprime byte, quindi scrivi 8192 (non 8096 :-)), invece di 8
Jarret Hardie

Piacere mio - spero che funzioni. Su un sistema moderno, puoi probabilmente aumentare un po 'la dimensione del buffer. 8k è solo un residuo nella mia memoria per qualche motivo che non riesco a identificare.
Jarret Hardie

Ho fatto alcuni test qui e impostandolo su -1 (predefinito del sistema operativo, spesso 8k, ma spesso difficile da dire), sembra essere il più veloce possibile. Detto questo, parte di ciò potrebbe essere dovuto al fatto che sto testando su un server virtuale.
Oscar Smith

12

Probabilmente sono viziato dall'abbondante montone, ma 15 M non è enorme. Leggere nella memoria con readlines() è ciò che faccio di solito con file di queste dimensioni. Accedere a una riga dopo è banale.


Perché ero leggermente riluttante a leggere l'intero file: potrei avere molti di quei processi in esecuzione, e se una dozzina di questi leggesse 12 file da 15 MB ciascuno, potrebbe non essere buono. Ma ho bisogno di provarlo per scoprire se funzionerà. Grazie.
user63503

4
Hrm, e se fosse un file da 1 GB?
Noah

@photographer: anche "diversi" processi che leggono in file da 15 MB non dovrebbero avere importanza su una tipica macchina moderna (dipende, ovviamente, esattamente da cosa stai facendo con loro).
Jacob Gabrielson

Jacob, sì, dovrei solo provare. Il processo (i) è / sono in esecuzione su una macchina virtuale per settimane se vm non è andato in crash. Purtroppo l'ultima volta si è bloccato dopo 6 giorni. Devo continuare da dove si è interrotto improvvisamente. Devo ancora capire come trovare dove è stato lasciato.
user63503

@ Noah: ma non lo è! Perché non vai oltre? Cosa succede se il file 128 TB? Di quanto molti sistemi operativi non sarebbero in grado di supportarlo. Perché non risolvere il problema man mano che vengono?
SilentGhost

7

Sono sorpreso che nessuno abbia menzionato Islice

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

o se vuoi tutto il resto del file

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

o se vuoi ogni altra riga dal file

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

5

Poiché non c'è modo di determinare la lunghezza di tutte le linee senza leggerle, non hai altra scelta che ripetere tutte le linee prima della linea di partenza. Tutto quello che puoi fare è farlo sembrare carino. Se il file è davvero enorme, potresti voler utilizzare un approccio basato sul generatore:

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)

Nota: l'indice è a base zero in questo approccio.


4

Se non si desidera leggere l'intero file in memoria, potrebbe essere necessario creare un formato diverso dal testo normale.

ovviamente tutto dipende da cosa stai cercando di fare e da quanto spesso salterai attraverso il file.

Ad esempio, se salti a righe molte volte nello stesso file e sai che il file non cambia mentre ci lavori, puoi farlo:
Innanzitutto, passa attraverso l'intero file e registra il " seek-location "di alcuni numeri di linea chiave (come, sempre 1000 linee),
quindi se vuoi la linea 12005, salta alla posizione di 12000 (che hai registrato) poi leggi 5 linee e ti riconoscerai sei in linea 12005 e così via


3

Se conosci in anticipo la posizione nel file (piuttosto il numero di riga), puoi usare file.seek () per andare a quella posizione.

Modifica : puoi usare la funzione linecache.getline (filename, lineno) , che restituirà il contenuto della riga lineno, ma solo dopo aver letto l'intero file in memoria. Buono se stai accedendo in modo casuale a righe dall'interno del file (come lo stesso Python potrebbe voler fare per stampare un traceback) ma non va bene per un file da 15 MB.


Sicuramente non userei linecache per questo scopo, perché legge l'intero file in memoria prima di restituire la riga richiesta.
MiniQuark

Sì, sembrava troppo bello per essere vero. Vorrei ancora che ci fosse un modulo per farlo in modo efficiente, ma tendo invece a usare il metodo file.seek ().
Noah

3

Cosa genera il file che vuoi elaborare? Se è qualcosa sotto il tuo controllo, potresti generare un indice (quale riga è in quale posizione) nel momento in cui il file viene aggiunto. Il file indice può avere una dimensione di riga fissa (spazio riempito o 0 numeri riempiti) e sarà sicuramente più piccolo. E così può essere letto ed elaborato rapidamente.

  • Quale linea vuoi ?.
  • Calcola l'offset di byte del numero di riga corrispondente nel file indice (possibile perché la dimensione della riga del file indice è costante).
  • Usa seek o qualsiasi altra cosa per saltare direttamente per ottenere la riga dal file index.
  • Analizza per ottenere l'offset di byte per la riga corrispondente del file effettivo.

3

Ho avuto lo stesso problema (è necessario recuperare dalla riga specifica del file enorme).

Sicuramente, posso ogni volta scorrere tutti i record nel file e fermarlo quando counter sarà uguale alla linea di destinazione, ma non funziona in modo efficace nel caso in cui si desideri ottenere un numero plurale di righe specifiche. Ciò ha causato la risoluzione del problema principale: come gestire direttamente la posizione del file necessaria.

Ho scoperto la decisione successiva: in primo luogo ho completato il dizionario con la posizione iniziale di ogni riga (la chiave è il numero di riga e il valore - lunghezza cumulata delle righe precedenti).

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

in ultima analisi, la funzione obiettivo:

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

t.seek (line_number) - comando che esegue l'eliminazione del file fino all'inizio della riga. Quindi, se si esegue il commit successivo di readline, si ottiene la riga di destinazione.

Utilizzando tale approccio ho risparmiato una parte significativa del tempo.


3

Puoi usare mmap per trovare l'offset delle linee. MMap sembra essere il modo più veloce per elaborare un file

esempio:

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

quindi usa f.seek (offset) per spostarti sulla linea che ti serve


2

Le righe stesse contengono informazioni sull'indice? Se il contenuto di ogni riga fosse qualcosa come " <line index>:Data", allora l' seek()approccio potrebbe essere usato per fare una ricerca binaria nel file, anche se la quantità di Dataè variabile. Cerchi il punto medio del file, leggi una riga, controlla se il suo indice è superiore o inferiore a quello che desideri, ecc.

Altrimenti, il meglio che puoi fare è semplicemente readlines(). Se non vuoi leggere tutti i 15 MB, puoi usare l' sizehintargomento per sostituire almeno molti messaggi readline()con un numero inferiore di chiamate a readlines().


2

Se hai a che fare con un file di testo e basato su un sistema Linux , puoi usare i comandi di Linux.
Per me ha funzionato bene!

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)

ovviamente non è compatibile con Windows o qualche tipo di shell Linux che non supporta head / tail.
Wizmann

È più veloce che farlo in Python?
Shamoon

Può avere più righe?
Shamoon

1

Ecco un esempio che usa 'readlines (sizehint)' per leggere un pezzo di righe alla volta. Il DNS ha indicato quella soluzione. Ho scritto questo esempio perché gli altri esempi qui sono orientati su una riga.

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)

0

Nessuna delle risposte è particolarmente soddisfacente, quindi ecco un piccolo snippet per aiutarti.

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()

Utilizzo di esempio:

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!

Ciò comporta l'esecuzione di molte ricerche di file, ma è utile nei casi in cui non è possibile inserire l'intero file in memoria. Esegue una lettura iniziale per ottenere le posizioni della riga (quindi legge l'intero file, ma non lo mantiene tutto in memoria), quindi ogni accesso esegue una ricerca del file dopo il fatto.

Offro lo snippet di cui sopra con la licenza MIT o Apache a discrezione dell'utente.


-1

Può usare questa funzione per restituire la riga n:

def skipton(infile, n):
    with open(infile,'r') as fi:
        for i in range(n-1):
            fi.next()
        return fi.next()

Questa logica non funziona se ci sono righe vuote continue, fi.next () salta tutte le righe vuote contemporaneamente, altrimenti va bene :)
Anvesh Yalamarthy

L'OP non menziona che le linee hanno linee con interruzioni di riga non standard. In tal caso, dovresti analizzare ogni riga con almeno un'istruzione if per le interruzioni di riga parziali.
ksed
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.