Eu tenho um arquivo de texto enorme (70 GB), uma linha , e quero substituir uma string (token) nele. Quero substituir o token <unk>
por outro fictício ( problema de luva ).
Eu tentei sed
:
sed 's/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new
mas o arquivo de saída corpus.txt.new
possui zero bytes!
Eu também tentei usar perl:
perl -pe 's/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new
mas recebi um erro de falta de memória.
Para arquivos menores, os dois comandos acima funcionam.
Como posso substituir uma string é um arquivo? Esta é uma pergunta relacionada, mas nenhuma das respostas funcionou para mim.
Editar : Que tal dividir o arquivo em pedaços de 10 GB (ou o que for) cada um e aplicar sed
em cada um deles e depois mesclá-los cat
? Isso faz sentido? Existe uma solução mais elegante?
text-processing
sed
large-files
Christos Baziotis
fonte
fonte
split
com a-b
opção de definir tamanhos de arquivo de bloco em bytes. Processe cada um por sua vez, usandosed
e remontando. Existe o risco é que<unk>
pode ser dividido em dois arquivos e não será encontrado ...Respostas:
As ferramentas usuais de processamento de texto não foram projetadas para lidar com linhas que não cabem na RAM. Eles tendem a trabalhar lendo um registro (uma linha), manipulando-o e produzindo o resultado, depois prosseguindo para o próximo registro (linha).
Se houver um caractere ASCII que apareça com frequência no arquivo e não apareça em
<unk>
ou<raw_unk>
, você poderá usá-lo como separador de registros. Como a maioria das ferramentas não permite separadores de registros personalizados, troque entre esse caractere e as novas linhas.tr
processa bytes, não linhas, por isso não se importa com nenhum tamanho de registro. Supondo que;
funcione:Você também pode ancorar no primeiro caractere do texto que está procurando, supondo que ele não seja repetido no texto de pesquisa e apareça com frequência suficiente. Se o arquivo começar
unk>
, altere o comando sedsed '2,$ s/…
para evitar uma correspondência falsa.Como alternativa, use o último caractere.
Observe que essa técnica pressupõe que sed opera perfeitamente em um arquivo que não termina com uma nova linha, ou seja, que processa a última linha parcial sem truncá-la e sem anexar uma nova linha final. Funciona com o GNU sed. Se você puder escolher o último caractere do arquivo como separador de registros, evitará qualquer problema de portabilidade.
fonte
awk -v RS=, -v ORS=, '{gsub(/<unk>/, "<raw_unk>"); print}'
Não?-0
eo valor octal de um char, ou dentro do script que pode ser definido com a variável especial$/
awk
evitar passar o fluxo duas vezes paratr
. Então, seria ainda mais lento?tr
é muito rápido e o tubo pode até ser paralelo.Para um arquivo tão grande, uma possibilidade é o Flex. Let
unk.l
be:Em seguida, compile e execute:
fonte
make
possui regras padrão para isso, em vez do flex / cc, você pode adicionar um%option main
como a primeira linha de unk.l e depois apenasmake unk
. Eu uso mais ou menos reflexivamente%option main 8bit fast
e tenhoexport CFLAGS='-march=native -pipe -Os'
no meu.bashrc
.%option main
+make
+ opcionalmenteCFLAGS
são um truque muito bom !! O-march=native
comportamento padrão é?Portanto, você não tem memória física (RAM) suficiente para armazenar o arquivo inteiro de uma só vez, mas em um sistema de 64 bits, você tem espaço de endereço virtual suficiente para mapear o arquivo inteiro. Os mapeamentos virtuais podem ser úteis como um simples hack em casos como este.
As operações necessárias estão todas incluídas no Python. Existem várias sutilezas irritantes, mas evita a necessidade de escrever código C. Em particular, é necessário cuidado para evitar a cópia do arquivo na memória, o que anularia totalmente o argumento. No lado positivo, você obtém relatórios de erros gratuitamente ("exceções" em python)) :).
fonte
search
pode conter um caractere NUL. E notei que a outra versão C aqui não suporta caracteres NULreplace
.). Você pode obter a versão C para fins de comparação. No entanto, lembre-se de que minha versão inclui um relatório básico de erros para as operações que realiza. A versão C seria pelo menos mais chata de ler IMO, quando o relatório de erros estiver incluído.Há um
replace
utilitário no pacote mariadb-server / mysql-server. Ele substitui cadeias simples (não expressões regulares) e, diferentemente do grep / sed / awk,replace
não se importa com\n
e\0
. O consumo de memória é constante em qualquer arquivo de entrada (cerca de 400kb na minha máquina).Claro que você não precisa rodar um servidor mysql para usá-
replace
lo, ele é empacotado dessa maneira no Fedora. Outras distribuições / sistemas operacionais podem ser embalados separadamente.fonte
Eu acho que a versão C pode ter um desempenho muito melhor:
EDIT: Modificado de acordo com as sugestões dos comentários. Também foi corrigido o erro com o padrão
<<unk>
.fonte
memcpy
velocidade (ou seja, o gargalo de memória) é algo como 12 GB / segundo em uma CPU x86 recente (por exemplo, Skylake). Mesmo com a sobrecarga de chamada do sistema stdio +, para um arquivo de 30 MB quente no cache do disco, eu esperaria talvez 1 GB / segundo para uma implementação eficiente. Você compilou com a otimização desativada ou a E / S de um caracter por vez é realmente lenta?getchar_unlocked
/putchar_unlocked
Pode ajudar, mas definitivamente melhor para leitura / gravação em blocos de talvez 128kiB (metade do tamanho do cache L2 na maioria das CPUs x86, para que principalmente atingido na L2 ao loop depois de ler)fix
programa para"<<unk>"
ainda não funcionará se opattern
início for uma sequência repetida de caracteres (ou seja, não funcionaria se você estivesse tentando substituir o aardvark por zebra e tivesse entrada de aaardvak ou se estivesse tentando substituir ababc e teve entrada de abababc). Em geral, você não pode avançar pelo número de caracteres que leu, a menos que saiba que não há possibilidade de uma correspondência começar nos caracteres que você leu.O GNU
grep
pode mostrar o deslocamento de correspondências em arquivos "binários", sem a necessidade de ler linhas inteiras na memória. Você pode usardd
para ler esse deslocamento, pular a partida e continuar copiando do arquivo.Para
dd
aumentar a velocidade, dividi a leitura em um tamanho grande de bloco 1048576 e uma leitura menor de 1 byte por vez, mas essa operação ainda será um pouco lenta em um arquivo tão grande. Agrep
saída é, por exemplo,,13977:<unk>
e isso é dividido em dois pontos pela leitura em variáveisoffset
epattern
. Temos que acompanharpos
quantos bytes já foram copiados do arquivo.fonte
Aqui está outra linha de comando UNIX que pode ter um desempenho melhor do que outras opções, porque você pode "procurar" um "tamanho de bloco" com bom desempenho. Para que isso seja robusto, você precisa saber que possui pelo menos um espaço em cada caractere X, onde X é o seu "tamanho de bloco" arbitrário. No exemplo abaixo, escolhi um "tamanho do bloco" de 1024 caracteres.
Aqui, o fold pega até 1024 bytes, mas o -s garante que ele se quebre em um espaço se houver pelo menos um desde a última interrupção.
O comando sed é seu e faz o que você espera.
Em seguida, o comando tr "desdobra" o arquivo, convertendo as novas linhas que foram inseridas novamente em nada.
Você deve tentar tamanhos de bloco maiores para ver se o desempenho é mais rápido. Em vez de 1024, você pode tentar 10240 e 102400 e 1048576 para a opção -w de fold.
Aqui está um exemplo dividido por cada etapa que converte todos os Ns em minúsculas:
Você precisará adicionar uma nova linha no final do arquivo, se houver uma, porque o comando tr a removerá.
fonte
Usando
perl
Gerenciando seus próprios buffers
Você pode usar
IO::Handle
'ssetvbuf
para gerenciar os buffers padrão ou gerenciar seus próprios buffers comsysread
esyswrite
. Verifiqueperldoc -f sysread
e,perldoc -f syswrite
para obter mais informações, essencialmente eles ignoram o buffer io.Aqui rolamos nossa própria E / S de buffer, mas fazemos isso manualmente e arbitrariamente em 1024 bytes. Também abrimos o arquivo para o RW, então fazemos tudo no mesmo FH de uma só vez.
Se você estiver indo por esta rota
<unk>
e<raw_unk>
são do mesmo tamanho byte.CHUNKSIZE
limites, se você estiver substituindo mais de 1 byte.fonte
<unk>
cair em um limite entre pedaços?Você pode tentar o bbe ( editor de bloco binário ), um "
sed
para arquivos binários".Tive um bom sucesso usando-o em um arquivo de texto de 7 GB sem
EOL
caracteres, substituindo várias ocorrências de uma string por uma de comprimento diferente. Sem tentar qualquer otimização, obteve uma taxa de transferência média de processamento de> 50MB / s.fonte
Com
perl
, você pode trabalhar com registros de comprimento fixo, como:E espero que não haja
<unk>
s abrangendo dois desses registros de 100 MB.fonte
while read -N 1000 chunk;
(o1000
escolhido como exemplo). A solução para<unk>
, dividida entre os pedaços, é duas passagens pelo arquivo: a primeira com os pedaços de 100 MB e a segunda com os pedaços de '100 MB + 5 bytes'. Mas não é a solução ideal no caso do arquivo de 70GB.<unk>
.<unk>
ocorrências sejam muito diferentes, se não, use$/ = ">"
es/<unk>\z/<raw_unk>/g
) de estar correta.Aqui está um pequeno programa Go que executa a tarefa (
unk.go
):Apenas construa
go build unk.go
e execute como./unk <input >output
.EDITAR:
Desculpe, eu não li que tudo está em uma linha, então tentei ler o arquivo caractere por caractere agora.
EDIÇÃO II:
Aplicou a mesma correção do programa C.
fonte
scanner.Split(bufio.ScanRunes)
faz a mágica.go doc bufio.MaxScanTokenSize
o tamanho padrão do buffer.C
programa, isso não funciona para substituir o aardvark por zebra por uma entrada de aaardvark.Pode ser um exagero para um arquivo de 70 GB e pesquisa e substituição simples, mas a estrutura do Hadoop MapReduce resolveria seu problema agora sem nenhum custo (escolha a opção 'Único nó' ao configurá-lo para executá-lo localmente) - e poderá ser dimensionado para capacidade infinita no futuro sem a necessidade de modificar seu código.
O tutorial oficial em https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html usa Java (extremamente simples), mas você pode encontrar bibliotecas clientes para Perl ou qualquer idioma que você queira usar.
Portanto, se, posteriormente, você descobrir que está executando operações mais complexas em arquivos de texto de 7000 GB - e precisando fazer isso 100 vezes por dia -, poderá distribuir a carga de trabalho entre vários nós que você provisiona ou que são provisionados automaticamente por uma nuvem - cluster Hadoop baseado em
fonte
Todas as sugestões anteriores exigem a leitura do arquivo inteiro e a gravação do arquivo inteiro. Isso não leva muito tempo, mas também requer 70 GB de espaço livre.
1) Se eu entendi corretamente o seu caso específico, seria aceitável substituir por alguma outra string do mesmo comprimento?
2a) Existem múltiplas ocorrências? 2b) Se sim, você sabe quantos?
Tenho certeza de que você já resolveu esse problema de mais de um ano e gostaria de saber qual solução você usou.
Eu proporia uma solução (provavelmente em C) que leria os BLOCOS do arquivo pesquisando cada uma pela string, levando em consideração o possível cruzamento de blocos. Uma vez encontrada, substitua a string pelo mesmo comprimento alternativo e escreva apenas esse BLOCK. Continuando pelo número conhecido de ocorrências ou até o final do arquivo. Isso exigiria apenas o número de gravações de ocorrências e no máximo duas vezes isso (se todas as ocorrências fossem divididas em 2 blocos). Isso não exigiria espaço adicional!
fonte
Se tivermos um valor mínimo de
<unk>
(como esperado pela lei de Zipf),fonte
sed
Lê uma linha de cada vez na memória, independentemente. Não poderá caber nesta linha.sed
não fará buffer de entrada / saída ao usar esse sinalizador. Não vejo que ele leia linhas parciais.