Como melhorar a velocidade deste loop de readline em python?
Pergunta
eu estou importando várias partes de um Databasedump em formato de texto em MySQL, o problema é que antes do dado interessante há muito material em frente não interessante. Eu escrevi este loop para obter os dados necessários:
def readloop(DBFILE):
txtdb=open(DBFILE, 'r')
sline = ""
# loop till 1st "customernum:" is found
while sline.startswith("customernum: ") is False:
sline = txtdb.readline()
while sline.startswith("customernum: "):
data = []
data.append(sline)
sline = txtdb.readline()
while sline.startswith("customernum: ") is False:
data.append(sline)
sline = txtdb.readline()
if len(sline) == 0:
break
customernum = getitem(data, "customernum: ")
street = getitem(data, "street: ")
country = getitem(data, "country: ")
zip = getitem(data, "zip: ")
O Textfile é muito grande, por isso só looping até a entrada primeiro queria leva muito tempo. Qualquer um tem uma idéia se isso poderia ser feito mais rapidamente (ou se toda a maneira que eu fixo isso não é a melhor idéia)?
Muito obrigado antecipadamente!
Solução
A idéia geral para a otimização é para continuar "por grandes blocos" (principalmente-ignorando estrutura de linha) para localizar a primeira linha de interesse, em seguida, passar para o processamento por linha para o resto). É um pouco mimado e propenso a erros (off-by-one e similares) para que ele realmente precisa de testes, mas a idéia geral é o seguinte ...:
import itertools
def readloop(DBFILE):
txtdb=open(DBFILE, 'r')
tag = "customernum: "
BIGBLOCK = 1024 * 1024
# locate first occurrence of tag at line-start
# (assumes the VERY FIRST line doesn't start that way,
# else you need a special-case and slight refactoring)
blob = ''
while True:
blob = blob + txtdb.read(BIGBLOCK)
if not blob:
# tag not present at all -- warn about that, then
return
where = blob.find('\n' + tag)
if where != -1: # found it!
blob = blob[where+1:] + txtdb.readline()
break
blob = blob[-len(tag):]
# now make a by-line iterator over the part of interest
thelines = itertools.chain(blob.splitlines(1), txtdb)
sline = next(thelines, '')
while sline.startswith(tag):
data = []
data.append(sline)
sline = next(thelines, '')
while not sline.startswith(tag):
data.append(sline)
sline = next(thelines, '')
if not sline:
break
customernum = getitem(data, "customernum: ")
street = getitem(data, "street: ")
country = getitem(data, "country: ")
zip = getitem(data, "zip: ")
Aqui, eu tentei manter o máximo de sua estrutura intacta quanto possível, fazendo melhorias apenas pequenas além da "grande ideia" desta refatoração.
Outras dicas
Por favor, não escrever este código:
while condition is False:
condições booleanas são boolean para chorar em voz alta, para que eles possam ser testados (ou negado e testado) diretamente:
while not condition:
O segundo loop while não está escrito como "enquanto condição for verdadeira:"., Estou curioso por que você sentiu a necessidade de teste "é falso" na primeira
Retirando o dis módulo, eu pensei em dissecar isso um pouco mais. Na minha experiência pyparsing, chamadas de função são totais assassinos de desempenho, por isso seria bom para evitar chamadas de função, se possível. Aqui é o seu teste original:
>>> test = lambda t : t.startswith('customernum') is False
>>> dis.dis(test)
1 0 LOAD_FAST 0 (t)
3 LOAD_ATTR 0 (startswith)
6 LOAD_CONST 0 ('customernum')
9 CALL_FUNCTION 1
12 LOAD_GLOBAL 1 (False)
15 COMPARE_OP 8 (is)
18 RETURN_VALUE
Duas coisas caras acontecer aqui, CALL_FUNCTION
e LOAD_GLOBAL
. Você poderia cortar em LOAD_GLOBAL
pela definição de um nome local para Falso:
>>> test = lambda t,False=False : t.startswith('customernum') is False
>>> dis.dis(test)
1 0 LOAD_FAST 0 (t)
3 LOAD_ATTR 0 (startswith)
6 LOAD_CONST 0 ('customernum')
9 CALL_FUNCTION 1
12 LOAD_FAST 1 (False)
15 COMPARE_OP 8 (is)
18 RETURN_VALUE
Mas o que se basta soltar o teste 'é' completamente:?
>>> test = lambda t : not t.startswith('customernum')
>>> dis.dis(test)
1 0 LOAD_FAST 0 (t)
3 LOAD_ATTR 0 (startswith)
6 LOAD_CONST 0 ('customernum')
9 CALL_FUNCTION 1
12 UNARY_NOT
13 RETURN_VALUE
Nós já entrou em colapso um LOAD_xxx
e COMPARE_OP
com um UNARY_NOT
simples. "É falsa" certamente não está ajudando o desempenho causa qualquer.
Agora, o que se pode fazer alguma eliminação bruta de uma linha sem fazer quaisquer chamadas de função em tudo. Se o primeiro caractere da linha não é um 'c', não há como ele vai StartsWith ( 'customernum'). Vamos tentar isso:
>>> test = lambda t : t[0] != 'c' and not t.startswith('customernum')
>>> dis.dis(test)
1 0 LOAD_FAST 0 (t)
3 LOAD_CONST 0 (0)
6 BINARY_SUBSCR
7 LOAD_CONST 1 ('c')
10 COMPARE_OP 3 (!=)
13 JUMP_IF_FALSE 14 (to 30)
16 POP_TOP
17 LOAD_FAST 0 (t)
20 LOAD_ATTR 0 (startswith)
23 LOAD_CONST 2 ('customernum')
26 CALL_FUNCTION 1
29 UNARY_NOT
>> 30 RETURN_VALUE
(Note que usando [0] para obter o primeiro caractere de uma string não criar uma fatia -. Este é de fato muito rápido)
Agora, assumindo que não há um grande número de linhas que começam com 'c', o filtro bruto de corte pode eliminar uma linha usando todas as instruções bastante rápido. Na verdade, por meio de testes "t [0]! = 'C'" em vez de "não t [0] == 'c'" nós salvar uma instrução UNARY_NOT
estranho.
Então, usando esse aprendizado sobre otimização de atalho e eu sugiro mudar este código:
while sline.startswith("customernum: ") is False:
sline = txtdb.readline()
while sline.startswith("customernum: "):
... do the rest of the customer data stuff...
Para isto:
for sline in txtdb:
if sline[0] == 'c' and \
sline.startswith("customernum: "):
... do the rest of the customer data stuff...
Note que eu também tenha removido a chamada de função .readline (), e apenas iterar sobre o arquivo usando "para sline em txtdb".
Eu percebo Alex tem proporcionado um corpo diferente do código inteiramente para encontrar aquela primeira linha 'customernum', mas gostaria de tentar otimizar dentro dos limites gerais de seu algoritmo, antes de retirar a leitura do bloco armas grandes, mas obscuros.
Eu acho que você está escrevendo o script de importação e fica chato que esperar durante os testes, então as estadias dados da mesma o tempo todo.
Você pode executar o script uma vez para detectar as posições reais no arquivo que você deseja saltar para, com print txtdb.tell()
. Escrever para baixo aqueles e substituir o código de busca com txtdb.seek( pos )
. Basicamente desse builing um índice para o arquivo; -)
Outra maneira mais convetional seria ler dados em blocos maiores, alguns MB de cada vez, e não apenas os poucos bytes em uma linha.
Conte-nos mais sobre o arquivo.
Você pode usar file.seek fazer uma busca binária? Vai para a marca de metade, ler algumas linhas, determinar se você é antes ou depois da parte que você precisa, recurse. Isso vai transformar o seu O (n) procurar em O (log n).