Otimização de script para encontrar o nome do arquivo duplicado em um arquivo CSV grande

4

Eu tenho vários arquivos CSV de 1MB a 6GB gerados pelo script inotify com uma lista de eventos formatados como: timestamp;fullpath;event;size .

Esses arquivos são formatados assim:

timestamp;fullpath;event;size
1521540649.02;/home/workdir/ScienceXMLIn/config.cfg;IN_OPEN;2324
1521540649.02;/home/workdir/ScienceXMLIn/config.cfg;IN_ACCESS;2324
1521540649.02;/home/workdir/ScienceXMLIn/config.cfg;IN_CLOSE_NOWRITE;2324
1521540649.02;/home/workdir/quad_list_14.json;IN_OPEN;2160
1521540649.03;/home/workdir/quad_list_14.json;IN_ACCESS;2160
1521540649.03;/home/workdir/quad_list_14.json;IN_CLOSE_NOWRITE;2160
1521540649.03;/home/workdir/ScienceXMLIn/masterbias_list.asc;IN_OPEN;70
1521540649.03;/home/workdir/ScienceXMLIn/masterbias_list.asc.1;IN_OPEN;80
1521540649.03;/home/workdir/ScienceXMLIn/masterbias_list.asc.2;IN_OPEN;70
1521540649.03;/home/workdir/otherfolder/quad_list_14.json;IN_OPEN;2160
1521540649.03;/home/workdir/otherfolder/quad_list_14.json;IN_CLOSE_NOWRITE;2160

Meu objetivo é identificar o arquivo com o mesmo nome que aparece em pastas diferentes.
Neste exemplo, o arquivo quad_list_14.json aparece em /home/workdir/otherfolder e /home/workdir/ .

Minha saída desejada é simples, apenas a lista de arquivos que aparece em mais de uma pasta, nesse caso, seria a seguinte:

quad_list_14.json

Para fazer isso, eu escrevi esse pequeno código:

#this line cut the file to only get unique filepath
PATHLIST=$(cut -d';' -f 2 ${1} | sort -u)
FILENAMELIST=""

#this loop build a list of basename from the list of filepath
for path in ${PATHLIST}
do
    FILENAMELIST="$(basename "${path}")
${FILENAMELIST}"
done

#once the list is build, I simply find the duplicates with uniq -d as the list is already sorted
echo "${FILENAMELIST}" | sort | uniq -d

Não use este código em casa é terrível, eu deveria ter substituído este script com um onliner como este:

#this get all file path, sort them and only keep unique entry then
#remove the path to get the basename of the file 
#and finally sort and output duplicates entry.
cut -d';' -f 2 ${1} | sort -u |  grep -o '[^/]*$' | sort | uniq -d

Meu problema permanece e muitos arquivos e o menor leva 0,5 segundos, mas o mais longo leva 45 segundos em um SSD (e meu disco de produção não será tão rápido) para encontrar o nome do arquivo duplicado na pasta diferente.

Eu preciso melhorar este código para torná-lo mais eficiente e não sou especialista em scripts, estou aberto a muitas soluções.
Minha única limitação é que não posso carregar totalmente os arquivos na RAM.

    
por Kiwy 11.04.2018 / 17:28

3 respostas

3

O seguinte script AWK deve funcionar, sem usar muita memória:

#!/usr/bin/awk -f

BEGIN {
    FS = ";"
}

{
    idx = match($2, "/[^/]+$")
    if (idx > 0) {
        path = substr($2, 1, idx)
        name = substr($2, idx + 1)
        if (paths[name] && paths[name] != path && !output[name]) {
            print name
            output[name] = 1
        }
        paths[name] = path
    }
}

Ele extrai o caminho e o nome de cada arquivo e armazena o último caminho visto para cada nome. Se já havia visto outro caminho, ele exibe o nome, a menos que já tenha sido enviado.

    
por 11.04.2018 / 17:39
4

O principal problema com o seu código é que você está coletando todos os nomes de caminho em uma variável e, em seguida, faz um loop sobre ele para chamar basename . Isso torna isso lento.

O loop também é executado sobre a expansão de variável sem nome ${PATHLIST} , o que seria insensato se os nomes de caminho contivessem espaços ou caracteres de globalização de shell. Em bash (ou outros shells que o suportam), seria usado um array.

Sugestão:

$ sed -e '1d' -e 's/^[^;]*;//' -e 's/;.*//' file.csv | sort -u | sed 's#.*/##' | sort | uniq -d
quad_list_14.json

O primeiro sed seleciona os nomes de caminho (e descarta a linha de cabeçalho). Isso também pode ser escrito como awk -F';' 'NR > 1 { print $2 }' file.csv ou como tail -n +2 file.csv | cut -d ';' -f 2 .

O sort -u nos dá nomes de caminho exclusivos e o seguinte sed nos fornece os nomes de base. O% final sort com uniq -d no final nos informa quais nomes básicos são duplicados.

O último sed 's#.*/##' que lhe dá os nomes básicos é uma reminiscência da expansão do parâmetro ${pathname##*/} , que é equivalente a $( basename "$pathname" ) . Ele apenas exclui tudo até e incluindo o último / na string.

A principal diferença do seu código é que, em vez do loop que chama basename várias vezes, um único sed é usado para produzir os nomes básicos de uma lista de nomes de caminhos.

Alternativa para observar apenas IN_OPEN entradas:

sed -e '/;IN_OPEN;/!d' -e 's/^[^;]*;//' -e 's/;.*//' file.csv | sort -u | sed 's#.*/##' | sort | uniq -d
    
por 11.04.2018 / 17:51
1

Obrigado a vocês dois por suas respostas e obrigado Isaac pelos comentários.
Eu peguei todo o seu código e coloquei em um script stephen.awk kusa.sh e isaac.sh depois disso eu executei um pequeno benchmark como este:

for i in $(ls *.csv)
do
    script.sh $1
done

Com o comando time eu os comparo e aqui estão os resultados:

stephen.awk

real    2m35,049s
user    2m26,278s
sys     0m8,495s

stephen.awk : atualizado com / IN_OPEN / antes do segundo bloco

real    0m35,749s
user    0m15,711s
sys     0m4,915s

kusa.sh

real    8m55,754s
user    8m48,924s
sys     0m21,307s

Atualizar com filtro em IN_OPEN :

real    0m37,463s
user    0m9,340s
sys     0m4,778s

Nota:
Embora correto, eu tinha um monte de linhas em branco produzidas com sed , seu script era o único assim.

isaac.sh

grep -oP '^[^;]*;\K[^;]*' file.csv | sort -u | grep -oP '.*/\K.*' | sort | uniq -d
real    7m2,715s
user    6m56,009s
sys     0m18,385s

com filtro em IN_OPEN :

real    0m32,785s
user    0m8,775s
sys     0m4,202s

meu script

real    6m27,645s
user    6m13,742s
sys     0m20,570s

@ Stephen você claramente ganha este, com um impressionante decréscimo de tempo por um fator 2.5.

Embora depois de pensar um pouco mais eu tenha vindo com outra ideia, e se eu olhar apenas para o evento OPEN file isso reduziria a complexidade e você não deveria acessar um arquivo ou escrevê-lo sem primeiro abri-lo, então eu fiz isso:

#see I add grep "IN_OPEN" to reduce complexity
PATHLIST=$(grep "IN_OPEN" "${1}" | cut -d';' -f 2 | sort -u)
FILENAMELIST=""
for path in ${PATHLIST}
do
    FILENAMELIST="$(basename "${path}")
${FILENAMELIST}"
done
echo "${FILENAMELIST}" | sort | uniq -d

Com essa única modificação que me deu o mesmo resultado, acabei com esse valor time :

real    0m56,412s
user    0m27,439s
sys     0m9,928s

E tenho certeza que há muitas outras coisas que eu poderia fazer

    
por 12.04.2018 / 10:42