Estou usando o Python 3.5.2
Eu tenho duas listas
- uma lista de cerca de 750.000 "frases" (seqüências longas)
- uma lista de cerca de 20.000 "palavras" que gostaria de excluir de minhas 750.000 frases
Então, eu tenho que percorrer 750.000 frases e executar cerca de 20.000 substituições, mas SOMENTE se minhas palavras forem realmente "palavras" e não fizerem parte de uma cadeia maior de caracteres.
Estou fazendo isso pré-compilando minhas palavras para que elas sejam flanqueadas pelo \b
metacaractere
compiled_words = [re.compile(r'\b' + word + r'\b') for word in my20000words]
Então eu percorro minhas "frases"
import re
for sentence in sentences:
for word in compiled_words:
sentence = re.sub(word, "", sentence)
# put sentence into a growing list
Esse loop aninhado está processando cerca de 50 frases por segundo , o que é bom, mas ainda leva várias horas para processar todas as minhas frases.
Existe uma maneira de usar o
str.replace
método (que acredito ser mais rápido), mas ainda exigindo que as substituições ocorram apenas nos limites das palavras ?Como alternativa, existe uma maneira de acelerar o
re.sub
método? Eu já melhorei a velocidade marginalmente saltando sobrere.sub
se o comprimento da minha palavra é> maior que o comprimento da minha frase, mas não é uma grande melhoria.
Obrigado por todas as sugestões.
multiprocessing
(ou seja, vários processos Python).Respostas:
Uma coisa que você pode tentar é compilar um único padrão
"\b(word1|word2|word3)\b"
.Como
re
depende do código C para fazer a correspondência real, a economia pode ser dramática.Como o @pvg apontou nos comentários, ele também se beneficia da correspondência de passe único.
Se suas palavras não são regulares, a resposta de Eric é mais rápida.
fonte
s/They actually use/They actually could in theory sometimes use/
. Você tem algum motivo para acreditar que a implementação do Python está fazendo algo além de um loop aqui?TLDR
Use este método (com pesquisa de conjunto) se desejar a solução mais rápida. Para um conjunto de dados semelhante ao OP, é aproximadamente 2000 vezes mais rápido que a resposta aceita.
Se você insistir em usar um regex para pesquisa, use esta versão baseada em trie , que ainda é 1000 vezes mais rápida que uma união de regex.
Teoria
Se suas sentenças não forem seqüências de caracteres enormes, provavelmente é possível processar muito mais que 50 por segundo.
Se você salvar todas as palavras banidas em um conjunto, será muito rápido verificar se outra palavra está incluída nesse conjunto.
Empacote a lógica em uma função, dê essa função como argumento
re.sub
e pronto!Código
As frases convertidas são:
Observe que:
lower()
)""
pode deixar dois espaços (como no seu código)\w+
também combina caracteres acentuados (por exemplo"ångström"
).atuação
Há um milhão de frases,
banned_words
tem quase 100000 palavras e o script é executado em menos de 7s.Em comparação, a resposta de Liteye precisava de 160s para 10 mil frases.
Com
n
sendo o amound total de palavras em
a quantidade de palavras proibidas, código de Liteye do OP e sãoO(n*m)
.Em comparação, meu código deve ser executado
O(n+m)
. Considerando que existem muito mais frases do que palavras proibidas, o algoritmo se tornaO(n)
.Teste de união Regex
Qual é a complexidade de uma pesquisa regex com um
'\b(word1|word2|...|wordN)\b'
padrão? ÉO(N)
ouO(1)
?É muito difícil entender o funcionamento do mecanismo regex, então vamos escrever um teste simples.
Esse código extrai
10**i
palavras aleatórias em inglês em uma lista. Ele cria a união de expressão regular correspondente e a testa com palavras diferentes:#
)Emite:
Portanto, parece que a busca por uma única palavra com um
'\b(word1|word2|...|wordN)\b'
padrão tem:O(1)
melhor casoO(n/2)
caso médio, que ainda éO(n)
O(n)
pior casoEsses resultados são consistentes com uma pesquisa simples de loop.
Uma alternativa muito mais rápida a uma união de expressões regulares é criar o padrão de expressões regulares a partir de uma tentativa .
fonte
O(1)
alegação enganosa , sua resposta definitivamente merece uma votação positiva.TLDR
Use esse método se desejar a solução mais rápida baseada em regex. Para um conjunto de dados semelhante aos do OP, é aproximadamente 1000 vezes mais rápido que a resposta aceita.
Se você não se importa com regex, use esta versão baseada em conjunto , que é 2000 vezes mais rápida que uma união de regex.
Regex otimizado com Trie
Uma abordagem simples de união do Regex fica lenta com muitas palavras proibidas, porque o mecanismo do regex não faz um bom trabalho de otimização do padrão.
É possível criar um Trie com todas as palavras banidas e escrever a regex correspondente. O trie ou regex resultante não é realmente legível por humanos, mas permite uma pesquisa e uma correspondência muito rápidas.
Exemplo
A lista é convertida em um trie:
E então para este padrão regex:
A grande vantagem é que, para testar se há
zoo
correspondência, o mecanismo regex precisa apenas comparar o primeiro caractere (não corresponde), em vez de tentar as 5 palavras . É um exagero de pré-processo para 5 palavras, mas mostra resultados promissores para muitos milhares de palavras.Observe que
(?:)
os grupos que não capturam são usados porque:foobar|baz
corresponderiafoobar
oubaz
, mas nãofoobaz
foo(bar|baz)
salvaria informações desnecessárias em um grupo de captura .Código
Aqui está uma essência levemente modificada , que podemos usar como uma
trie.py
biblioteca:Teste
Aqui está um pequeno teste (o mesmo que este ):
Emite:
Para informações, o regex começa assim:
É realmente ilegível, mas para uma lista de 100.000 palavras proibidas, esse regie Trie é 1000 vezes mais rápido que uma simples união regex!
Aqui está um diagrama da trie completa, exportada com trie-python-graphviz e graphviz
twopi
:fonte
|
mas os grupos de captura não são necessários para o nosso propósito. Eles apenas retardavam o processo e usavam mais memória sem benefício.\b
( limite da palavra ). Se a lista for['apple', 'banana']
, ela substituirá as palavras que são exatamenteapple
oubanana
, mas não sãonana
,bana
oupineapple
.Uma coisa que você pode querer tentar é pré-processar as frases para codificar os limites das palavras. Transforme basicamente cada frase em uma lista de palavras dividindo-as nos limites das palavras.
Isso deve ser mais rápido, porque para processar uma frase, basta percorrer cada uma das palavras e verificar se é uma correspondência.
Atualmente, a pesquisa regex precisa passar por toda a sequência novamente, procurando os limites das palavras e, em seguida, "descartando" o resultado desse trabalho antes da próxima passagem.
fonte
Bem, aqui está uma solução rápida e fácil, com conjunto de testes.
Estratégia vencedora:
re.sub ("\ w +", repl, sentença) procura por palavras.
"repl" pode ser exigível. Eu usei uma função que executa uma pesquisa de ditado, e o ditado contém as palavras para pesquisar e substituir.
Esta é a solução mais simples e rápida (consulte a função replace4 no código de exemplo abaixo).
Segundo melhor
A idéia é dividir as frases em palavras, usando re.split, enquanto conserva os separadores para reconstruí-las posteriormente. Em seguida, as substituições são feitas com uma simples pesquisa de ditado.
(consulte a função replace3 no código de exemplo abaixo).
Tempos para funções de exemplo:
... e código:
Editar: você também pode ignorar letras minúsculas ao verificar se passa uma lista minúscula de frases e edita
fonte
replace4
e meu código tem desempenhos semelhantes.repl(m):
está fazendo e como você está atribuindom
na replace4 funçãoerror: unbalanced parenthesis
para a linhapatterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
Talvez o Python não seja a ferramenta certa aqui. Aqui está um com a cadeia de ferramentas Unix
supondo que seu arquivo da lista negra seja pré-processado com os limites da palavra adicionados. As etapas são: converter o arquivo em espaço duplo, dividir cada frase em uma palavra por linha, excluir em massa as palavras da lista negra do arquivo e mesclar novamente as linhas.
Isso deve executar pelo menos uma ordem de magnitude mais rápido.
Para pré-processar o arquivo da lista negra de palavras (uma palavra por linha)
fonte
Que tal agora:
Essas soluções dividem os limites das palavras e pesquisam cada palavra em um conjunto. Eles devem ser mais rápidos que o re.sub de palavras alternativas (solução da Liteyes), pois essas soluções são
O(n)
onde n é o tamanho da entrada devido àamortized O(1)
pesquisa definida, enquanto o uso de expressões regulares regex faria com que o mecanismo regex tivesse que procurar correspondências de palavras em todos os caracteres e não apenas nos limites das palavras. Minha solução tem um cuidado extra para preservar os espaços em branco usados no texto original (ou seja, não compacta os espaços em branco e preserva guias, novas linhas e outros caracteres de espaço em branco), mas se você decidir que não se importa com isso, deve ser bastante simples para removê-los da saída.Testei no corpus.txt, que é uma concatenação de vários eBooks baixados do Projeto Gutenberg, e banned_words.txt contém 20.000 palavras escolhidas aleatoriamente na lista de palavras do Ubuntu (/ usr / share / dict / american-english). Demora cerca de 30 segundos para processar 862462 sentenças (e metade disso no PyPy). Eu defini frases como qualquer coisa separada por ".".
O PyPy se beneficia particularmente mais da segunda abordagem, enquanto o CPython se sai melhor na primeira abordagem. O código acima deve funcionar no Python 2 e 3.
fonte
\W+
é basicamente comosub
em\w+
, certo?Abordagem prática
Uma solução descrita abaixo usa muita memória para armazenar todo o texto na mesma string e reduzir o nível de complexidade. Se a RAM é um problema, pense duas vezes antes de usá-la.
Com
join
/split
truques você pode evitar loops, o que deve acelerar o algoritmo.|
instrução "ou" regex:atuação
"".join
a complexidade é O (n). Isso é bastante intuitivo, mas de qualquer maneira há uma citação abreviada de uma fonte:Portanto,
join/split
você tem O (palavras) + 2 * O (sentenças), que ainda é uma complexidade linear vs 2 * O (N 2 ) com a abordagem inicial.btw não use multithreading. O GIL bloqueará cada operação porque sua tarefa é estritamente vinculada à CPU, portanto, o GIL não tem chance de ser liberado, mas cada thread envia ticks simultaneamente, o que causa um esforço extra e até leva a operação ao infinito.
fonte
Concatene todas as suas frases em um único documento. Use qualquer implementação do algoritmo Aho-Corasick ( aqui está um ) para localizar todas as suas palavras "ruins". Percorra o arquivo, substituindo cada palavra incorreta, atualizando os deslocamentos das palavras encontradas que se seguem, etc.
fonte