Exclui arquivos que não estão em uma lista de padrões

4

Eu gerencio um site de vendas de carros para um cliente. Eles estão constantemente adicionando e removendo carros. Quando um novo vem, eles adicionam um lote de imagens e o site gera uma miniatura para cada um. O site armazena o nome do arquivo base (através do qual eu posso acessar tanto a miniatura quanto o original). Aqui está um exemplo:

5e1adcf7c9c1bcf8842c24f3bacbf169.jpg
5e1adcf7c9c1bcf8842c24f3bacbf169_tn.jpg
5e1de0c86e45f84b6d01af9066581e84.jpg
5e1de0c86e45f84b6d01af9066581e84_tn.jpg
5e2497180424aa0d5a61c42162b03fef.jpg
5e2497180424aa0d5a61c42162b03fef_tn.jpg
5e2728ac5eff260f20d4890fcafb1373.jpg
5e2728ac5eff260f20d4890fcafb1373_tn.jpg

O problema surge depois que um produto é removido. No meu fluxo de trabalho existente, não há uma maneira simples de remover imagens antigas. Em um período de alguns meses, acabamos com 10 mil imagens, onde apenas 10% estão ao vivo.

Eu posso pesquisar o banco de dados e gerar uma lista de stubs de imagem ao vivo :

5e1adcf7c9c1bcf8842c24f3bacbf169
5e2497180424aa0d5a61c42162b03fef

Eu quero excluir as imagens que não começam com esses stubs.

Observe que o desempenho de tempo / espaço também é um problema. Existem ~ 500 + stubs ao mesmo tempo. Eu tentei grepping ls como:

ls | grep -vf <(
    sqlite3 database.sqlite3 'select replace(images, CHAR(124), CHAR(10)) from cars_car'
)

Isso funciona, mas é extremamente lento (e você não deve analisar ls ). A consulta é rápida, então é o grep que atrasa tudo. Eu gostaria de melhores soluções. O Bash não é necessário, mas é o que eu faço na maioria dos meus scripts de manutenção.

    
por Oli 22.03.2015 / 12:58

7 respostas

3

Eu acho que será mais simples e rápido usar apenas GLOBIGNORE (supondo que seu shell seja bash mesmo assim):

   GLOBIGNORE
          A colon-separated list of patterns defining the set of filenames
          to be ignored by pathname expansion.  If a filename matched by a
          pathname expansion pattern also matches one of the  patterns  in
          GLOBIGNORE, it is removed from the list of matches.

Assim, você pode ler os padrões desejados em seu arquivo, adicionar um * para torná-los globs e convertê-los em uma lista separada por dois pontos:

GLOBIGNORE=$(sqlite3 database.sqlite3 'select images from cars_car;' |
             sed 's/|/*:/g; s/$/*/')

Então, você pode apenas rm all e resetar GLOBIGNORE (ou apenas fechar o terminal atual):

rm * && GLOBIGNORE=""

Porque GLOBIGNORE será agora assim:

$ echo $GLOBIGNORE 
5e1adcf7c9c1bcf8842c24f3bacbf169*:5e2497180424aa0d5a61c42162b03fef*

Todos os arquivos correspondentes a esses globs não serão incluídos na expansão de * . Isso tem o benefício adicional de trabalhar com qualquer tipo de nome de arquivo, incluindo aqueles com espaços, novas linhas ou outros caracteres estranhos.

    
por terdon 22.03.2015 / 13:15
3

Enquanto escrevia a pergunta, comecei a brincar com grep . Parte do problema de desempenho é que o grep está executando uma grande quantidade de buscas por regex para cada arquivo. Estes são caros .

Podemos apenas fazer buscas completas sem o regex, usando o argumento -F .

find | grep -vFf <(
    sqlite3 database.sqlite3 'select replace(images, CHAR(124), CHAR(10)) from cars_car'
) ### | xargs rm

A saída é a mesma e é executada em 0,045s.
O antigo levou 14.211s.

Um dos problemas com a análise de ls são os nomes de arquivos problemáticos. O comentário de Muru abaixo destaca uma maneira bastante decente de usar caracteres nulos em todo o pipeline.

find -print0 | grep -vzFf <(
    sqlite3 database.sqlite3 'select replace(images, CHAR(124), CHAR(10)) from cars_car'
) ### | xargs -0 rm

A razão pela qual não estou mudando a minha resposta principal para isso é que sei que meus arquivos estarão sempre limpos e que estou executando isso em wc -l para ter certeza de que estou vendo o número correto de arquivos para exclusão.

    
por Oli 22.03.2015 / 12:58
1

Se você estiver usando bash como seu shell, o shopt -s extglob poderá ativar mais alguns recursos nos padrões glob. Por exemplo

!(5e1adcf7c9c1bcf8842c24f3bacbf169*|5e2497180424aa0d5a61c42162b03fef*)

corresponderá a todos os nomes que não estiverem começando com uma das duas strings.

    
por kasperd 22.03.2015 / 16:50
1

Você pode simplesmente remover as imagens na execução do script de remoção do produto. Dessa forma, a carga será equilibrada na remoção de cada produto ao longo do tempo. Além disso, você não precisa se preocupar com a execução de um script para limpá-los, e todo o aplicativo será auto-suficiente. Sem mencionar que isso resolveria a questão do espaço para esse fim.

Eu não tenho idéia sobre qual DBMS você está usando, nem sobre qual linguagem de script você está usando para manipulá-lo ou sobre como sua estrutura de banco de dados se parece (nenhuma idéia sobre o caminho das imagens também), mas por exemplo , assumindo MySQL como o DBMS, PHP como a linguagem de script e uma tabela Products em um relacionamento 1-para-muitos com uma tabela Images , com o caminho das imagens apontando para uma pasta img sob o diretório raiz, seria algo assim:

<?php
    // ...
    $imgPath = $SERVER['DOCUMENT_ROOT'].'/img/';
    $result = mysqli_query($link, "SELECT Images.basename FROM Products, Images WHERE Products.productId = Images.productId AND Products.productId = $productId)
    while($row = mysqli_fetch_assoc($result)) {
        unlink($imgPath.$row['Images.basename'].'.jpg');
        unlink($imgPath.$row['Images.basename'].'_tn.jpg');
    }
    // ...
?>

Se estiver preocupado com as apresentações de unlink() , você sempre poderá usar:

<?php
    // ...
    $imgPath = $SERVER['DOCUMENT_ROOT'].'/img/';
    $result = mysqli_query($link, "SELECT Images.basename FROM Products, Images WHERE Products.productId = Images.productId AND Products.productId = $productId)
    while($row = mysqli_fetch_assoc($result)) {
        shell_exec("rm {$imgPath}{$row['Images.basename']}*");
    }
    // ...
?>

As preocupações sobre essa solução podem ser sobre a consulta adicional que você terá que executar a cada vez, a menos que você esteja obtendo Images antes no script e se isso for uma preocupação. .

    
por kos 22.03.2015 / 20:10
1

A solução de longo prazo para a qual estou errando é algo no final do meu script de atualização (Python / Django). Eu tenho uma lista de objetos Car - portanto, nenhuma consulta ao banco de dados - o que torna isso ainda mais rápido. Também acontece no momento exato em que as imagens antigas deixam de ser úteis.

Estou usando um Python set porque é provavelmente a maneira mais rápida de verificar. Para isso eu estou adicionando todos os stubs das imagens que eu quero manter, então eu estou interagindo com as miniaturas (mais fácil de glob), e excluindo os arquivos que não estão no conjunto.

# Generate a python "set" of image stubs
import itertools
imagehashes = set(itertools.chain(*map(lambda c: c.images.split('|'), cars)))

# Check which files aren't in the set and delete
import glob, os
for imhash in map(lambda i: i[25:-7], glob.glob('/path/to/images/*_tn.jpg')):
    if imhash in imagehashes:
        continue

    os.remove('/path/to/images/%s_tn.jpg' % imhash)
    os.remove('/path/to/images/%s.jpg' % imhash)

Existem alguns truques com map e itertools para economizar um pouco de tempo, mas isso é basicamente auto-explicativo.

    
por Oli 23.03.2015 / 12:49
1

Quando o bash puro não o corta (ou fica desnecessariamente desajeitado), é hora de mudar para uma linguagem de script apropriada. Minha ferramenta de escolha é geralmente Perl, mas você pode usar Python ou Ruby ou, até mesmo PHP, se você preferir.

Dito isso, aqui está um script Perl simples que lê uma lista de prefixos de stdin (desde que você não especificou exatamente como você está obtendo essa lista), um por linha, e exclui todos os arquivos no diretório atual com um Sufixo .jpg que não possui um desses prefixos:

#!/usr/bin/perl
use strict;
use warnings;

my @prefixes = <>;
chomp @prefixes;
# if you need to do any further input mangling, do it here

my $regex = join "|", map quotemeta, @prefixes;
$regex = qr/^($regex)/;   # anchor the regex and precompile it

foreach my $filename (<*.jpg>) {
    next if $filename =~ $regex;
    unlink $filename or warn "Error deleting $filename: $!\n";
}

Se preferir, você pode compactar isso em uma linha única, por exemplo:

perl -e '$re = "^(" . join("|", map { chomp; "\Q$_" } <>) . ")"; unlink grep !/$re/, <*.jpg>'

Ps. No seu caso, já que é fácil extrair o prefixo dos nomes dos arquivos, você também pode usar um hash em vez de um regex para otimizar a pesquisa, assim:

my %hash;
undef @hash{@prefixes};   # fastest way to add keys to a hash

foreach my $filename (<*.jpg>) {
    my ($prefix) = ($filename =~ /^([0-9a-f]+)/);
    next if exists $hash{$prefix};
    unlink $filename or warn "Error deleting $filename: $!\n";
}

No entanto, embora este método seja melhor assintoticamente (pelo menos na prática, em teoria, o mecanismo regex poderia otimizar a correspondência para escala, bem como o método hash), por apenas 500 prefixos não há diferença notável.

Pelo menos nas versões atuais do Perl, no entanto, por Ilmari Karonen 22.03.2015 / 19:27

0
  

A consulta é rápida, então é o grep que atrapalha tudo.

Outra solução seria simplesmente inverter a consulta, para que você possa canalizar os resultados para rm diretamente.

Isso não deve introduzir diferenças no tempo.

    
por kos 23.03.2015 / 11:44