Удалить файлы, которых нет в списке шаблонов
Я управляю веб-сайтом по продаже автомобилей для клиента. Они постоянно добавляют и убирают машины. Когда приходит новый, они добавляют пакет изображений, и веб-сайт генерирует миниатюру для каждого. На сайте хранится базовое имя файла (через которое я могу получить доступ как к миниатюре, так и к оригиналу). Вот пример:
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
непосредственно.
Это не должно вносить никакой разницы во времени вообще.