¿Cómo saltar a una línea en particular en un archivo de texto enorme?

107

¿Hay alguna alternativa al siguiente código?

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

Si estoy procesando un archivo de texto enorme (~15MB)con líneas de longitud desconocida pero diferente, y necesito saltar a una línea en particular, ¿qué número sé de antemano? Me siento mal procesándolos uno por uno cuando sé que podría ignorar al menos la primera mitad del archivo. Buscando una solución más elegante si la hay.

usuario63503
fuente
¿Cómo sabe que la primera mitad del archivo no es un montón de "\ n" mientras que la segunda mitad es una sola línea? ¿Por qué te sientes mal por esto?
Andrew Dalke
7
Creo que el título es engañoso - tbh 15MB no es realmente un "archivo de texto enorme", por decir lo menos ...
pms

Respuestas:

30

linecache :

El linecachemódulo permite obtener cualquier línea de un archivo fuente de Python, mientras se intenta optimizar internamente, usando una caché, el caso común donde se leen muchas líneas de un solo archivo. Esto es utilizado por el tracebackmódulo para recuperar líneas de origen para su inclusión en el rastreo con formato ...

John Ellinwood
fuente
164
Acabo de comprobar el código fuente de este módulo: ¡todo el archivo se lee en la memoria! Por lo tanto, definitivamente descartaría esta respuesta con el propósito de acceder rápidamente a una línea determinada en un archivo.
MiniQuark
MiniQuark, lo probé, realmente funciona y muy rápido. Necesitaré ver qué sucede si trabajo en una docena de archivos al mismo tiempo de esta manera, averiguar en qué punto muere mi sistema.
user63503
5
El administrador de memoria virtual de su sistema operativo ayuda bastante, por lo que la lectura de archivos grandes en la memoria puede no ser lenta si no genera muchas fallas en las páginas :) Al contrario, hacerlo de la "manera estúpida" y asignar montones y montones de memoria puede ser increíblemente rápido. Disfruté del artículo del desarrollador danés de FreeBSD Poul-Henning Kamp al respecto: queue.acm.org/detail.cfm?id=1814327
Morten Jensen
13
prueba el archivo 100G, apesta. tengo que utilizar f.tell (), f.seek (), f.readline ()
WHI
114

No puede avanzar sin leer el archivo al menos una vez, ya que no sabe dónde están los saltos de línea. Podrías hacer algo como:

# 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
fuente
2
+1, ¡pero ten en cuenta que esto solo es útil si va a saltar a varias líneas aleatorias! pero si solo está saltando a una línea, entonces esto es un desperdicio
hasen
3
+1: Además, si el archivo no cambia, el índice del número de línea puede ser decapado y reutilizado, amortizando aún más el costo inicial de escanear el archivo.
S.Lott
OK, después de saltar allí, ¿cómo procesaría línea por línea comenzando desde esta posición?
user63503
8
Una cosa a tener en cuenta (particularmente en Windows): tenga cuidado de abrir el archivo en modo binario, o alternativamente use offset = file.tell (). En modo de texto en Windows, la línea será un byte más corta que su longitud sin procesar en el disco (\ r \ n reemplazado por \ n)
Brian
2
@photographer: Use read () o readline (), comienzan desde la posición actual establecida por seek.
S.Lott
22

Realmente no tiene tantas opciones si las líneas son de diferente longitud ... lamentablemente necesita procesar los caracteres finales de línea para saber cuándo ha progresado a la siguiente línea.

Sin embargo, puede acelerar drásticamente esto Y reducir el uso de memoria cambiando el último parámetro a "abrir" a algo que no sea 0.

0 significa que la operación de lectura de archivos no tiene búfer, lo cual es muy lento y requiere mucho disco. 1 significa que el archivo está almacenado en búfer de línea, lo que sería una mejora. Cualquier cosa por encima de 1 (digamos 8k, es decir: 8096 o superior) lee fragmentos del archivo en la memoria. Aún puede acceder a él for line in open(etc):, pero Python solo va un poco a la vez, descartando cada fragmento almacenado en búfer después de su procesamiento.

Jarret Hardie
fuente
6
8K es 8192, quizás sea mejor escribir 8 << 10 para estar seguro. :)
relájese
¿Sabe por casualidad que el tamaño del búfer se especifica en bytes? ¿Cuáles son los formatos apropiados? ¿Podría escribir '8k'? ¿O debería ser '8096'?
user63503
1
JAJAJA ... debe ser viernes ... Claramente no puedo hacer matemáticas. El tamaño del búfer es de hecho un entero que expresa bytes, así que escriba 8192 (no 8096 :-)), en lugar de 8
Jarret Hardie
Es un placer, espero que funcione. En un sistema moderno, probablemente pueda aumentar bastante el tamaño del búfer. 8k es solo un vestigio en mi memoria por alguna razón que no puedo identificar.
Jarret Hardie
He hecho algunas pruebas aquí, y establecerlo en -1 (por defecto, a menudo 8k, pero a menudo es difícil de decir), parece ser lo más rápido posible. Dicho esto, parte de eso puede deberse a que estoy probando en un servidor virtual.
Oscar Smith
12

Probablemente me eche a perder la abundante ram, pero 15 M no es enorme. Leer en la memoria readlines() es lo que suelo hacer con archivos de este tamaño. Acceder a una línea después de eso es trivial.

SilentGhost
fuente
Por qué dudaba un poco en leer el archivo completo: es posible que tenga varios de esos procesos en ejecución, y si una docena de ellos leen 12 archivos de 15 MB cada uno, podría no ser bueno. Pero necesito probarlo para saber si funcionará. Gracias.
user63503
4
Hrm, ¿y si es un archivo de 1GB?
Noah
@photographer: incluso "varios" procesos que se leen en archivos de 15 MB no deberían importar en una máquina moderna típica (dependiendo, por supuesto, de lo que esté haciendo exactamente con ellos).
Jacob Gabrielson
Jacob, sí, debería intentarlo. Los procesos se están ejecutando en una máquina virtual durante semanas si la máquina virtual no se bloquea. Desafortunadamente, la última vez se bloqueó después de 6 días. Necesito continuar desde donde se detuvo de repente. Todavía necesito averiguar cómo encontrar dónde se dejó.
user63503
@Noah: ¡pero no lo es! ¿Por qué no vas más lejos? ¿Y si archivo de 128 TB? De lo que muchos sistemas operativos no podrían admitirlo. ¿Por qué no resolver el problema a medida que surgen?
SilentGhost
7

Me sorprende que nadie mencione a Islice

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

o si quieres el resto del archivo

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

o si quieres cualquier otra línea del archivo

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

Dado que no hay forma de determinar la longitud de todas las líneas sin leerlas, no tiene más remedio que recorrer todas las líneas antes de la línea de partida. Todo lo que puede hacer es hacer que se vea bien. Si el archivo es realmente grande, es posible que desee utilizar un enfoque basado en generador:

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: el índice es cero según este enfoque.


fuente
4

Si no desea leer todo el archivo en la memoria ... es posible que deba crear algún formato que no sea texto sin formato.

por supuesto, todo depende de lo que intente hacer y de la frecuencia con la que salte por el archivo.

Por ejemplo, si va a saltar a las líneas muchas veces en el mismo archivo y sabe que el archivo no cambia mientras trabaja con él, puede hacer esto:
Primero, pase por todo el archivo y registre el " seek-location "de algunos números de línea clave (por ejemplo, cada 1000 líneas),
luego , si quieres la línea 12005, salta a la posición 12000 (que has registrado), luego lee 5 líneas y sabrás que estás en la línea 12005 y así sucesivamente

hasen
fuente
3

Si conoce de antemano la posición en el archivo (en lugar del número de línea), puede usar file.seek () para ir a esa posición.

Editar : puede usar la función linecache.getline (nombre de archivo, lineno) , que devolverá el contenido de la línea lineno, pero solo después de leer todo el archivo en la memoria. Bueno si está accediendo aleatoriamente a líneas desde dentro del archivo (como Python podría querer hacer para imprimir un rastreo) pero no es bueno para un archivo de 15 MB.

Noé
fuente
Definitivamente no usaría linecache para este propósito, porque lee todo el archivo en la memoria antes de devolver la línea solicitada.
MiniQuark
Sí, sonaba demasiado bueno para ser verdad. Todavía desearía que hubiera un módulo para hacer esto de manera eficiente, pero tiendo a usar el método file.seek () en su lugar.
Noah
3

¿Qué genera el archivo que desea procesar? Si es algo que está bajo su control, puede generar un índice (qué línea está en qué posición) en el momento en que se agrega el archivo. El archivo de índice puede tener un tamaño de línea fijo (con espacios rellenados o con 0 números rellenos) y definitivamente será más pequeño. Y así se puede leer y procesar rápidamente.

  • ¿Qué línea quieres ?.
  • Calcule el desplazamiento de bytes del número de línea correspondiente en el archivo de índice (posible porque el tamaño de línea del archivo de índice es constante).
  • Use seek o lo que sea para saltar directamente y obtener la línea del archivo de índice.
  • Analizar para obtener el desplazamiento de bytes para la línea correspondiente del archivo real.
kamathln
fuente
3

He tenido el mismo problema (necesito recuperar una línea específica de archivo enorme).

Seguramente, siempre puedo ejecutar todos los registros en el archivo y detenerlo cuando el contador sea igual a la línea de destino, pero no funciona de manera efectiva en el caso de que desee obtener un número plural de filas específicas. Eso provocó que se resolviera el problema principal: cómo manejar directamente al lugar de archivo necesario.

Descubrí la siguiente decisión: en primer lugar, completé el diccionario con la posición inicial de cada línea (la clave es el número de línea y el valor: longitud acumulada de las líneas anteriores).

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

en última instancia, función de objetivo:

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

t.seek (line_number): comando que ejecuta la poda del archivo hasta el inicio de la línea. Entonces, si confirma readline, obtendrá su línea objetivo.

Usando este enfoque, he ahorrado una parte importante de tiempo.

user3810114
fuente
3

Puede usar mmap para encontrar el desplazamiento de las líneas. MMap parece ser la forma más rápida de procesar un archivo

ejemplo:

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

luego use f.seek (compensaciones) para moverse a la línea que necesita

Jorge
fuente
2

¿Las líneas en sí contienen información de índice? Si el contenido de cada línea fuera algo así como " <line index>:Data", entonces el seek()enfoque podría usarse para hacer una búsqueda binaria a través del archivo, incluso si la cantidad de Dataes variable. Buscaría el punto medio del archivo, leería una línea, verificaría si su índice es mayor o menor que el que desea, etc.

De lo contrario, lo mejor que puede hacer es simplemente readlines(). Si no desea leer todos los 15 MB, puede usar el sizehintargumento para reemplazar al menos muchos correos electrónicos readline()con un número menor de llamadas a readlines().

DNS
fuente
2

Si está tratando con un archivo de texto y está basado en un sistema Linux , puede usar los comandos de Linux.
Para mí, ¡esto funcionó bien!

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
fuente
por supuesto, no es compatible con Windows o algún tipo de shell de Linux que no admite head / tail.
Wizmann
¿Es esto más rápido que hacerlo en Python?
Shamoon
¿Puede obtener varias líneas?
Shamoon
1

Aquí hay un ejemplo en el que se usa 'readlines (sizehint)' para leer un fragmento de líneas a la vez. DNS señaló esa solución. Escribí este ejemplo porque los otros ejemplos aquí están orientados a una sola línea.

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
fuente
0

Ninguna de las respuestas es particularmente satisfactoria, por lo que aquí hay un pequeño fragmento para ayudar.

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

Uso de ejemplo:

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!

Esto implica realizar muchas búsquedas de archivos, pero es útil para los casos en los que no puede caber todo el archivo en la memoria. Hace una lectura inicial para obtener las ubicaciones de las líneas (por lo que lee todo el archivo, pero no lo guarda todo en la memoria), y luego cada acceso realiza una búsqueda de archivo después del hecho.

Ofrezco el fragmento anterior bajo la licencia MIT o Apache a discreción del usuario.

Joseph Catrambone
fuente
-1

Puede usar esta función para devolver la línea n:

def skipton(infile, n):
    with open(infile,'r') as fi:
        for i in range(n-1):
            fi.next()
        return fi.next()
ksed
fuente
Esta lógica no funciona si hay líneas vacías continuas, fi.next () omite todas las líneas vacías a la vez, de lo contrario es bueno :)
Anvesh Yalamarthy
El OP no menciona que las líneas tienen líneas con saltos de línea no estándar. En ese caso, tendría que analizar cada línea con al menos una instrucción if para los saltos de línea parciales.
Publicado el