Como pular para uma linha específica em um arquivo de texto enorme?

107

Existem alternativas para o código abaixo:

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 estou processando um arquivo de texto enorme (~15MB)com linhas de comprimento desconhecido, mas diferente, e preciso pular para uma linha específica, qual número conheço com antecedência? Eu me sinto mal por processá-los um por um, quando sei que poderia ignorar pelo menos a primeira metade do arquivo. Procurando uma solução mais elegante, se houver alguma.

user63503
fonte
Como você sabe que a primeira metade do arquivo não é um monte de "\ n" s enquanto a segunda metade é uma única linha? Por que você se sente mal com isso?
Andrew Dalke,
7
Eu acho que o título é enganoso - tbh 15MB não é realmente um "arquivo de texto enorme", para dizer o mínimo ...
pms

Respostas:

30

linecache :

O linecachemódulo permite obter qualquer linha de um arquivo-fonte Python, enquanto tenta otimizar internamente, usando um cache, o caso comum em que muitas linhas são lidas de um único arquivo. Isso é usado pelo tracebackmódulo para recuperar linhas de origem para inclusão no traceback formatado ...

John Ellinwood
fonte
164
Acabei de verificar o código-fonte deste módulo: todo o arquivo é lido na memória! Portanto, eu definitivamente descartaria essa resposta com a finalidade de acessar rapidamente uma determinada linha em um arquivo.
MiniQuark
MiniQuark, eu tentei, ele realmente funciona, e muito rápido. Precisarei ver o que acontece se trabalhar em uma dúzia de arquivos ao mesmo tempo dessa maneira, descobrir em que ponto meu sistema morre.
user63503
5
O gerenciador de memória virtual do seu sistema operacional ajuda um pouco, portanto, a leitura de grandes arquivos na memória pode não ser lenta se você não estiver gerando muitas falhas de páginas :) Pelo contrário, fazendo isso da "maneira estúpida" e alocando muitos e muitos de memória pode ser extremamente rápido. Gostei do artigo do desenvolvedor dinamarquês do FreeBSD Poul-Henning Kamp sobre isso: queue.acm.org/detail.cfm?id=1814327
Morten Jensen
13
tente arquivo 100G, é uma merda. eu tenho que usar f.tell (), f.seek (), f.readline ()
whi
114

Você não pode pular sem ler o arquivo pelo menos uma vez, pois você não sabe onde estão as quebras de linha. Você poderia fazer 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
fonte
2
+1, mas cuidado, pois isso só é útil se ele for pular para várias linhas aleatórias! mas se ele está pulando apenas para uma linha, isso é um desperdício
hasen
3
+1: Além disso, se o arquivo não mudar, o índice do número da linha pode ser conservado e reutilizado, amortizando ainda mais o custo inicial de digitalização do arquivo.
S.Lott
OK, depois de pular para lá, como eu processaria linha por linha a partir dessa posição?
user63503
8
Uma coisa a ser observada (principalmente no Windows): tenha cuidado ao abrir o arquivo no modo binário ou, alternativamente, use offset = file.tell (). No modo de texto no Windows, a linha será um byte menor do que seu comprimento bruto no disco (\ r \ n substituído por \ n)
Brian,
2
@photographer: Use read () ou readline (), eles começam na posição atual conforme definido por busca.
S.Lott
22

Você realmente não tem muitas opções se as linhas forem de comprimentos diferentes ... infelizmente, você precisa processar os caracteres de final de linha para saber quando avançou para a próxima linha.

Você pode, no entanto, acelerar drasticamente E reduzir o uso de memória alterando o último parâmetro para "abrir" para algo diferente de 0.

0 significa que a operação de leitura do arquivo não tem buffer, o que é muito lento e exige muito do disco. 1 significa que o arquivo está em buffer de linha, o que seria uma melhoria. Qualquer coisa acima de 1 (digamos 8k .. ex .: 8096 ou superior) lê pedaços do arquivo na memória. Você ainda pode acessá-lo por meio for line in open(etc):, mas o python só vai um pouco de cada vez, descartando cada bloco armazenado em buffer após seu processamento.

Jarret Hardie
fonte
6
8K é 8192, talvez seja melhor escrever 8 << 10 para estar no lado seguro. :)
descontrair
Você por acaso sabe que o tamanho do buffer é especificado em bytes? Quais são os formatos apropriados? Posso escrever '8k'? Ou deveria ser '8096'?
user63503
1
HAHAHA ... deve ser sexta-feira ... eu claramente não posso fazer matemática. O tamanho do buffer é de fato um número inteiro expressando bytes, então escreva 8192 (não 8096 :-)), em vez de 8
Jarret Hardie
O prazer é meu - espero que dê certo. Em um sistema moderno, você provavelmente pode aumentar um pouco o tamanho do buffer. 8k é apenas um resquício na minha memória por algum motivo que não consigo identificar.
Jarret Hardie
Fiz alguns testes aqui e configurei-o para -1 (o padrão do sistema operacional, muitas vezes 8k, mas muitas vezes difícil de dizer), parece ser o mais rápido possível. Dito isso, parte disso pode ser que estou testando em um servidor virtual.
Oscar Smith
12

Eu provavelmente estou estragado por carneiro abundante, mas 15 M não é enorme. Ler para a memória com readlines() é o que costumo fazer com arquivos desse tamanho. Acessar uma linha depois disso é trivial.

SilentGhost
fonte
Por que eu estava um pouco hesitante em ler o arquivo inteiro - posso ter vários desses processos em execução, e se uma dúzia deles ler 12 arquivos de 15 MB cada, poderia não ser bom. Mas preciso testá-lo para descobrir se funciona. Obrigado.
user63503
4
Hrm, e se for um arquivo de 1 GB?
Noah
@photographer: mesmo "vários" processos lendo em arquivos de 15 MB não deveriam importar em uma máquina moderna típica (dependendo, é claro, de exatamente o que você está fazendo com eles).
Jacob Gabrielson
Jacob, sim, eu deveria apenas tentar. O (s) processo (s) está / estão em execução em uma máquina virtual por semanas se a VM não travar. Infelizmente da última vez, ele travou após 6 dias. Eu preciso continuar de onde parou de repente. Ainda preciso descobrir como descobrir onde foi deixado.
user63503
@Noah: mas não é! Por que você não vai mais longe? E se o arquivo de 128 TB? Do que muitos sistemas operacionais não seriam capazes de suportá-lo. Por que não resolver o problema da forma como eles aparecem?
SilentGhost
7

Estou surpreso que ninguém mencionou islice

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

ou se você quiser todo o resto do arquivo

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

ou se você quiser todas as outras linhas do arquivo

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

Como não há como determinar o comprimento de todas as linhas sem lê-las, você não tem escolha a não ser iterar todas as linhas antes de sua linha de partida. Tudo que você pode fazer é torná-lo bonito. Se o arquivo for realmente grande, você pode querer usar uma abordagem baseada em gerador:

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: o índice é zero com base nesta abordagem.


fonte
4

Se você não quiser ler o arquivo inteiro na memória, pode ser necessário criar algum formato diferente de texto simples.

é claro que tudo depende do que você está tentando fazer e da frequência com que irá pular pelo arquivo.

Por exemplo, se você for pular para as linhas várias vezes no mesmo arquivo e sabe que o arquivo não muda enquanto trabalha com ele, você pode fazer o seguinte:
Primeiro, passe por todo o arquivo e grave o " procurar-localização "de alguns números de linha-chave (como, sempre 1000 linhas),
então se você quiser a linha 12005, pule para a posição de 12000 (que você gravou) e leia 5 linhas e você saberá está na linha 12005 e assim por diante

Hassen
fonte
3

Se você souber com antecedência a posição no arquivo (ao invés do número da linha), você pode usar file.seek () para ir para essa posição.

Edit : você pode usar a função linecache.getline (filename, lineno) , que retornará o conteúdo da linha lineno, mas somente após ler o arquivo inteiro na memória. Bom se você estiver acessando aleatoriamente linhas de dentro do arquivo (como o próprio python pode querer fazer para imprimir um traceback), mas não é bom para um arquivo de 15 MB.

Noé
fonte
Eu definitivamente não usaria o linecache para esse propósito, porque ele lê todo o arquivo na memória antes de retornar a linha solicitada.
MiniQuark
Sim, parecia bom demais para ser verdade. Eu ainda gostaria que houvesse um módulo para fazer isso com eficiência, mas tendem a usar o método file.seek () em vez disso.
Noah
3

O que gera o arquivo que você deseja processar? Se for algo sob seu controle, você pode gerar um índice (qual linha está em qual posição.) No momento em que o arquivo é anexado. O arquivo de índice pode ter um tamanho de linha fixo (espaço preenchido ou 0 números preenchidos) e definitivamente será menor. E assim pode ser lido e processado rapidamente.

  • Qual linha você quer?
  • Calcule o deslocamento de byte do número da linha correspondente no arquivo de índice (possível porque o tamanho da linha do arquivo de índice é constante).
  • Use buscar ou qualquer outra coisa para pular diretamente para obter a linha do arquivo de índice.
  • Analise para obter o deslocamento de byte para a linha correspondente do arquivo real.
Kamthln
fonte
3

Eu tive o mesmo problema (precisa recuperar de uma linha específica de arquivo enorme).

Certamente, posso sempre percorrer todos os registros no arquivo e interrompê-lo quando o contador for igual à linha de destino, mas não funciona de forma eficaz em um caso em que você deseja obter o número plural de linhas específicas. Isso fez com que o problema principal fosse resolvido - como lidar diretamente com o local de arquivo necessário.

Descobri a próxima decisão: primeiro concluí o dicionário com a posição inicial de cada linha (a chave é o número da linha e o valor - comprimento acumulado das linhas 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

em última análise, função 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 executa a remoção do arquivo até o início da linha. Então, se você confirmar readline - você obterá sua linha de destino.

Usando essa abordagem, economizei uma parte significativa do tempo.

user3810114
fonte
3

Você pode usar o mmap para encontrar o deslocamento das linhas. MMap parece ser a maneira mais rápida de processar um arquivo

exemplo:

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

então use f.seek (offsets) para mover para a linha que você precisa

George
fonte
2

As próprias linhas contêm alguma informação de índice? Se o conteúdo de cada linha fosse algo como " <line index>:Data", então a seek()abordagem poderia ser usada para fazer uma pesquisa binária pelo arquivo, mesmo se a quantidade de Datafor variável. Você iria procurar o ponto médio do arquivo, ler uma linha, verificar se seu índice é maior ou menor do que o desejado, etc.

Caso contrário, o melhor que você pode fazer é apenas readlines(). Se você não quiser ler todos os 15 MB, pode usar o sizehintargumento para pelo menos substituir muitos readline()s por um número menor de chamadas para readlines().

DNS
fonte
2

Se você está lidando com um arquivo de texto e baseado no sistema Linux , você pode usar os comandos do Linux.
Para mim, funcionou bem!

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
fonte
claro que não é compatível com o Windows ou algum tipo de shell do Linux que não suporta head / tail.
Wizmann
Isso é mais rápido do que em Python?
Shamoon
Isso pode ter várias linhas?
Shamoon
1

Aqui está um exemplo usando 'readlines (sizehint)' para ler um pedaço de linhas por vez. DNS apontou essa solução. Escrevi este exemplo porque os outros exemplos aqui são orientados por linha única.

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

Nenhuma das respostas é particularmente satisfatória, então aqui está um pequeno trecho para ajudar.

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

Exemplo de uso:

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!

Isso envolve fazer muitas buscas de arquivos, mas é útil para os casos em que você não pode colocar o arquivo inteiro na memória. Ele faz uma leitura inicial para obter as localizações das linhas (portanto, ele lê todo o arquivo, mas não o mantém na memória) e, em seguida, cada acesso faz uma busca pelo fato.

Eu ofereço o trecho acima sob a licença MIT ou Apache a critério do usuário.

Joseph Catrambone
fonte
-1

Pode usar esta função para retornar a linha n:

def skipton(infile, n):
    with open(infile,'r') as fi:
        for i in range(n-1):
            fi.next()
        return fi.next()
ksed
fonte
Esta lógica não funciona se houver linhas vazias contínuas, fi.next () pula todas as linhas vazias de uma vez, caso contrário, é bom :)
Anvesh Yalamarthy
O OP não menciona que as linhas têm linhas com quebras de linha não padrão. Nesse caso, você teria que analisar cada linha com pelo menos uma instrução if para as quebras de linha parciais.
ksed