Удалить файлы, которых нет в списке шаблонов

Я управляю веб-сайтом по продаже автомобилей для клиента. Они постоянно добавляют и убирают машины. Когда приходит новый, они добавляют пакет изображений, и веб-сайт генерирует миниатюру для каждого. На сайте хранится базовое имя файла (через которое я могу получить доступ как к миниатюре, так и к оригиналу). Вот пример:

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

Проблема возникает после удаления продукта. В моем существующем рабочем процессе нет простого способа удалить старые изображения. В течение нескольких месяцев мы получаем 10000 изображений, из которых только 10% живут.

Я могу искать в базе данных и генерировать список живых заглушек изображений:

5e1adcf7c9c1bcf8842c24f3bacbf169
5e2497180424aa0d5a61c42162b03fef

Я хочу удалить изображения, которые не начинаются с этих заглушек.

Обратите внимание, что время / пространство производительность также является проблемой здесь. Есть ~500+ заглушки в любой момент времени. Я пробовал grep ls как:

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

Это работает, но это очень медленно (и вы не должны разбиратьls). Запрос быстрый, так что это grep немного, что утомляет все это. Я хотел бы лучшие решения. Bash не нужен, но это то, чем я занимаюсь в большинстве своих сценариев обслуживания.

7 ответов

При написании вопроса я начал играть с grep, Частично проблема производительности заключается в том, что grep выполняет тонну регулярных выражений для каждого файла. Это дорого.

Мы можем просто выполнить поиск по всей строке без регулярного выражения, используя -F аргумент.

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

Вывод такой же, и работает в 0,045 с.
Старый взял 14.211 с.


Одна из проблем с разбором ls это проблемные имена файлов. Комментарий Муру ниже подчеркивает довольно приличный способ использования нулевых символов по всему конвейеру.

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

Причина, по которой я не переключаю свой основной ответ на это, состоит в том, что я знаю, что мои файлы всегда будут чистыми, и что я запускаю это в wc -l чтобы убедиться, что я вижу правильное количество файлов для удаления.

Я думаю, это будет проще и быстрее, просто использовать GLOBIGNORE (в любом случае, если ваша оболочка - bash):

   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.

Таким образом, вы можете просто прочитать шаблоны, которые вы хотите из вашего файла, добавить * сделать их глобусами и преобразовать их в список, разделенный двоеточиями:

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

Тогда вы можете просто rm все, и сбросьте GLOBIGNORE (или просто закройте текущий терминал):

rm * && GLOBIGNORE=""

Так как GLOBIGNORE теперь будет выглядеть так:

$ echo $GLOBIGNORE 
5e1adcf7c9c1bcf8842c24f3bacbf169*:5e2497180424aa0d5a61c42162b03fef*

Любые файлы, соответствующие этим глобусам, не будут включены в расширение *, Это дает дополнительное преимущество работы с любым типом имени файла, в том числе с пробелами, переводами строки или другими странными символами.

Долгосрочное решение, к которому я ошибаюсь, находится в конце моего скрипта обновления (Python/Django). У меня есть список объектов Car, так что больше нет запросов к базе данных, что делает это еще быстрее. Это также происходит в то время, когда старые изображения перестают быть полезными.

Я использую питон set потому что это, вероятно, самый быстрый способ проверки. Для этого я добавляю все заглушки изображений, которые хочу сохранить, затем перебираю миниатюры (их легче выделить) и удаляю файлы, которых нет в наборе.

# 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)

Есть несколько хитростей с map а также itertools чтобы сэкономить немного времени, но в основном это говорит само за себя.

Вы можете просто удалить изображения при выполнении скрипта удаления продукта. Таким образом, нагрузка будет распределяться по каждому продукту с течением времени. Кроме того, вам не придется беспокоиться о запуске сценария для его очистки, и все приложение будет самодостаточным. Не говоря уже о том, что это решит проблему космоса с этой целью.

Я понятия не имею о том, какую СУБД вы используете, ни о каком языке сценариев, который вы используете для управления ею, или о том, как выглядит структура вашей базы данных (также нет понятия о пути к изображениям), но, например, предполагая, MySQL как СУБД, PHP в качестве языка сценариев и Products таблица в отношениях 1-ко-многим с Images таблица с путем изображения, указывающим на img папка, расположенная под корневым каталогом, будет выглядеть примерно так:

<?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');
    }
    // ...
?>

Если вы обеспокоены unlink() Вы всегда можете использовать выступления:

<?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']}*");
    }
    // ...
?>

Обеспокоенность по поводу этого решения может быть связана с дополнительным запросом, который вам придется выполнять каждый раз, если вы не извлекаете Images уже раньше в сценарии, и если это вообще проблема.

Если вы используете bash как ваша оболочка, то shopt -s extglob может включить некоторые дополнительные функции в шаблонах глобуса. Например

!(5e1adcf7c9c1bcf8842c24f3bacbf169*|5e2497180424aa0d5a61c42162b03fef*)

будет соответствовать всем именам, не начинающимся с одной из двух строк.

Когда чистый bash не обрезает его (или становится неоправданно неловким), пришло время переключиться на надлежащий язык сценариев. Мой инструмент выбора обычно Perl, но вы можете использовать Python или Ruby или, черт возьми, даже PHP для этого, если хотите.

Тем не менее, вот простой Perl-скрипт, который читает список префиксов из stdin (поскольку вы не указали, как именно вы получаете этот список), по одному на строку, и удаляет все файлы в текущем каталоге с .jpg суффикс, у которого нет одного из этих префиксов:

#!/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";
}

Если вы предпочитаете, вы можете сжать это до одной строки, например:

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

Ps. В вашем случае, поскольку достаточно легко извлечь префикс из имен файлов, вы также можете использовать хеш вместо регулярного выражения для оптимизации поиска, например так:

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";
}

Однако, несмотря на то, что этот метод масштабируется лучше асимптотически (по крайней мере, на практике; теоретически механизм регулярных выражений может оптимизировать сопоставление для масштабирования, а также метод хеширования), для всего 500 префиксов нет никакой заметной разницы.

Однако, по крайней мере, в текущих версиях Perl решение для регулярных выражений становится намного медленнее, когда число альтернатив превышает определенный предел. Для 32-байтовых префиксов мое тестирование показало значительный скачок времени выполнения, когда число альтернатив достигло 6553, но точный порог, по-видимому, также зависит от длины префиксов и от того, что еще, если вообще что-либо, содержится в регулярном выражении. Это, очевидно, причуды механизма регулярных выражений Perl и его оптимизатора, поэтому другие реализации регулярных выражений (даже PCRE) могут демонстрировать другое поведение.

Запрос быстрый, так что это grep немного, что утомляет все это.

Другое решение состоит в том, чтобы просто инвертировать запрос, чтобы вы могли передать результаты в rm непосредственно.

Это не должно вносить никакой разницы во времени вообще.

Другие вопросы по тегам