Búsqueda eficiente de la última línea en un archivo de texto

Necesito extraer la última línea de una serie de archivos de texto muy grandes (varios cientos de megabytes) para obtener ciertos datos. Actualmente, estoy usando python para recorrer todas las líneas hasta que el archivo esté vacío y luego procese la última línea devuelta, pero estoy seguro de que hay una forma más eficiente de hacerlo.

¿Cuál es la mejor manera de recuperar solo la última línea de un archivo de texto usando python?

No es el camino directo, pero probablemente mucho más rápido que una simple implementación de Python:

line = subprocess.check_output(['tail', '-1', filename]) 
 with open('output.txt', 'r') as f: lines = f.read().splitlines() last_line = lines[-1] print last_line 

Utilice el método de seek del archivo con un desplazamiento negativo y whence=os.SEEK_END para leer un bloque desde el final del archivo. Busca en el bloque los últimos caracteres de final de línea y toma todos los caracteres que se encuentran después. Si no hay un final de línea, retroceda más y repita el proceso.

 def last_line(in_file, block_size=1024, ignore_ending_newline=False): suffix = "" in_file.seek(0, os.SEEK_END) in_file_length = in_file.tell() seek_offset = 0 while(-seek_offset < in_file_length): # Read from end. seek_offset -= block_size if -seek_offset > in_file_length: # Limit if we ran out of file (can't seek backward from start). block_size -= -seek_offset - in_file_length if block_size == 0: break seek_offset = -in_file_length in_file.seek(seek_offset, os.SEEK_END) buf = in_file.read(block_size) # Search for line end. if ignore_ending_newline and seek_offset == -block_size and buf[-1] == '\n': buf = buf[:-1] pos = buf.rfind('\n') if pos != -1: # Found line end. return buf[pos+1:] + suffix suffix = buf + suffix # One-line file. return suffix 

Tenga en cuenta que esto no funcionará en cosas que no admiten la seek , como stdin o sockets. En esos casos, estás atascado leyendo todo (como lo hace el comando tail ).

Si sabes la longitud máxima de una línea, puedes hacerlo

 def getLastLine(fname, maxLineLength=80): fp=file(fname, "rb") fp.seek(-maxLineLength-1, 2) # 2 means "from the end of the file" return fp.readlines()[-1] 

Esto funciona en mi máquina de Windows. Pero no sé qué sucede en otras plataformas si abre un archivo de texto en modo binario. El modo binario es necesario si desea utilizar seek ().

Busque hasta el final del archivo menos 100 bytes o menos. Haga una lectura y busque una nueva línea. Si aquí no hay una nueva línea, busque otros 100 bytes más o menos. Hacer espuma, enjuagar, repetir. Eventualmente encontrarás una nueva línea. La última línea comienza inmediatamente después de esa nueva línea.

En el mejor de los casos, solo hace una lectura de 100 bytes.

Si puede elegir una longitud de línea máxima razonable, puede buscar casi el final del archivo antes de comenzar a leer.

 myfile.seek(-max_line_length, os.SEEK_END) line = myfile.readlines()[-1] 

¿Podría cargar el archivo en un mmap y luego usar mmap.rfind (string [, start [, end]]) para encontrar el segundo último carácter EOL en el archivo? Una búsqueda a ese punto en el archivo debería apuntarle a la última línea que pensaría.

La ineficiencia aquí no se debe realmente a Python, sino a la naturaleza de cómo se leen los archivos. La única forma de encontrar la última línea es leer el archivo y encontrar los finales de línea. Sin embargo, la operación de búsqueda se puede usar para saltar a cualquier desplazamiento de bytes en el archivo. Por lo tanto, puede comenzar muy cerca del final del archivo y agarrar trozos cada vez más grandes según sea necesario hasta que se encuentre el último final de línea:

 from os import SEEK_END def get_last_line(file): CHUNK_SIZE = 1024 # Would be good to make this the chunk size of the filesystem last_line = "" while True: # We grab chunks from the end of the file towards the beginning until we # get a new line file.seek(-len(last_line) - CHUNK_SIZE, SEEK_END) chunk = file.read(CHUNK_SIZE) if not chunk: # The whole file is one big line return last_line if not last_line and chunk.endswith('\n'): # Ignore the trailing newline at the end of the file (but include it # in the output). last_line = '\n' chunk = chunk[:-1] nl_pos = chunk.rfind('\n') # What's being searched for will have to be modified if you are searching # files with non-unix line endings. last_line = chunk[nl_pos + 1:] + last_line if nl_pos == -1: # The whole chunk is part of the last line. continue return last_line 

Aquí hay una solución ligeramente diferente. En lugar de multilínea, me centré solo en la última línea, y en lugar de un tamaño de bloque constante, tengo un tamaño de bloque dynamic (doble). Ver comentarios para más información.

 # Get last line of a text file using seek method. Works with non-constant block size. # IDK if that speed things up, but it's good enough for us, # especially with constant line lengths in the file (provided by len_guess), # in which case the block size doubling is not performed much if at all. Currently, # we're using this on a textfile format with constant line lengths. # Requires that the file is opened up in binary mode. No nonzero end-rel seeks in text mode. REL_FILE_END = 2 def lastTextFileLine(file, len_guess=1): file.seek(-1, REL_FILE_END) # 1 => go back to position 0; -1 => 1 char back from end of file text = file.read(1) tot_sz = 1 # store total size so we know where to seek to next rel file end if text != b'\n': # if newline is the last character, we want the text right before it file.seek(0, REL_FILE_END) # else, consider the text all the way at the end (after last newline) tot_sz = 0 blocks = [] # For storing succesive search blocks, so that we don't end up searching in the already searched j = file.tell() # j = end pos not_done = True block_sz = len_guess while not_done: if j < block_sz: # in case our block doubling takes us past the start of the file (here j also = length of file remainder) block_sz = j not_done = False tot_sz += block_sz file.seek(-tot_sz, REL_FILE_END) # Yes, seek() works with negative numbers for seeking backward from file end text = file.read(block_sz) i = text.rfind(b'\n') if i != -1: text = text[i+1:].join(reversed(blocks)) return str(text) else: blocks.append(text) block_sz <<= 1 # double block size (converge with open ended binary search-like strategy) j = j - block_sz # if this doesn't work, try using tmp j1 = file.tell() above return str(b''.join(reversed(blocks))) # if newline was never found, return everything read 

Lo ideal sería envolver esto en una clase LastTextFileLine y hacer un seguimiento de un promedio móvil de longitud de línea. Esto te daría una buena len_guess tal vez.

 lines = file.readlines() fileHandle.close() last_line = lines[-1] 
 #!/usr/bin/python count = 0 f = open('last_line1','r') for line in f.readlines(): line = line.strip() count = count + 1 print line print count f.close() count1 = 0 h = open('last_line1','r') for line in h.readlines(): line = line.strip() count1 = count1 + 1 if count1 == count: print line #-------------------- this is the last line h.close()