Недавно у меня возникла необходимость просмотреть журналы моего сервера Minecraft. Мне нужно было распаковать несколько файлов журналов, заархивированных в формате gzip, и свернуть их в один огромный журнал. А затем я использовал grep для поиска полезной информации (однако я решил это сделать).
Поэтому я решил написать bash-скрипт:
dir = "/home/minecraft/spigot-1.20.2/logs"
logs=$(ls $dir)
start=$SECONDS
sum = ""
for log in $logs
do
s=$SECONDS
log_date=$(echo $log | cut -b 1-10)
unzipped_log = "$(zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g")"$'\n'
while read -r line
do
echo "$line"
done <<< "$unzipped_log"
echo The file $log $(du -h $dir/$log) processed in $(($SECONDS - $s))
done
s=$SECONDS
last = "$(cat $dir/latest.log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //")"$'\n'
date_today=$(date +%Y-%m-%d)
while read -r line
do
echo "$date_today $line"
done <<< "$last"
echo The file latest.log processed in $(($SECONDS - $s))
echo Script worked in $(($SECONDS-$start)) seconds
После запуска source sum_logs.sh | tac > all_logs.txt тайминг составил 60 секунд.
Затем я переписал код на Python:
import os, gzip, time, re
from datetime import datetime
dir = "/home/minecraft/spigot-1.20.2/logs"
start = time.time()
time.sleep(1)
logs = os.listdir(dir)
for log in logs:
log_path = os.path.join(dir, log)
log_date = log[:10]
try:
with gzip.open(log_path, 'rt') as f:
for line in f.readlines():
line = re.sub(r'\[Async Chat Thread - #\d+/INFO]: ', '', line)
print(log_date, line.replace('\n', ''))
except:
with open(log_path, 'r') as f:
for line in f.readlines():
line = re.sub(r'\[Async Chat Thread - #\d+/INFO]: ', '', line)
print(log_date, line.replace('\n', ''))
elapsed = round(time.time() - start)
print(f'Script worked in {elapsed} seconds')
Время 9 секунд.
Я понимаю, что bash медленнее Python. Но я понятия не имел, что разница в 6 раз. Действительно ли bash такой медленный или есть способ ускорить мой код?
Редактировать.
dir = "/home/minecraft/spigot-1.20.2/logs"
start=$SECONDS
sum = ""
for f in $dir/*
do
s=$SECONDS
log=$(echo $f | cut -b 36-)
log_date=$(echo $log | cut -b 1-10)
unzipped_log = "$(zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g")"$'\n'
echo "$unzipped_log"
echo The file $log $(du -h $dir/$log) processed in $(($SECONDS - $s))
done
s=$SECONDS
date_today=$(date +%Y-%m-%d)
last = "$(cat $dir/latest.log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$date_today /g")"$'\n'
echo $last
echo The file latest.log processed in $(($SECONDS - $s))
echo Script worked in $(($SECONDS-$start)) seconds
Приведенный выше код выполняется за 23 секунды!
Я думаю, основная проблема заключалась во вложенных циклах.
Я понятия не имел, что мне пригодится s/^/$date/g.
Редактировать 2.
dir = "/home/minecraft/spigot-1.20.2/logs"
start=$SECONDS
sum = ""
for f in $dir/*
do
s=$SECONDS
log=$(echo $f | cut -b 36-)
log_date=$(echo $log | cut -b 1-10)
zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g"
echo The file $log $(du -h $dir/$log) processed in $(($SECONDS - $s))
done
s=$SECONDS
date_today=$(date +%Y-%m-%d)
cat $dir/latest.log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$date_today /g"
echo The file latest.log processed in $(($SECONDS - $s))
echo Script worked in $(($SECONDS-$start)) seconds
Хорошо, теперь это 5 секунд :0. Удалил ненужные переменные, как предлагалось. Я в большом удивлении...
Медлительность, похоже, является вариантом Подсчета строк или нумерации номеров строк, чтобы я мог перебирать их — почему это антишаблон?
@tripleee Понятно, спасибо за совет. Каждый файл здесь имеет либо 2024-05-09-1.log.gz, либо latest.log. Так что в моем случае, я думаю, с ls проблем нет. Но приятно знать.
unzipped_log = "$(zcat ... | sed ...)"; while/read ... echo ... done <<< "$unzipped_log" можно заменить на zcat ... | sed -e ... -e "s/^/$log_date /"; аналогично для 2-го цикла
во втором цикле у вас есть echo "$(date +%Y-%m-%d) $line", который вызывает (дорогой) подад для получения текущей даты; здесь большой удар по производительности, поскольку эта подоболочка вызывается при каждом проходе цикла; еще хуже, каждая подоболочка возвращает одно и то же; вам лучше вызвать эту подоболочку один раз перед циклом (сохранив результат в переменной), а затем сослаться на переменную в echo (еще лучше добавить дополнительное предложение sed -e, чтобы отдать предпочтение всем строкам с $last_date - см. предыдущий комментарий о переписывании 1-го петля)
@markp-fuso О, здорово! Мне нравится ярлык. Это похоже на замену начала строки ^ на date? Я думал, что это будет типа s/^/&$log_date/. Но ваш вариант работает)
поскольку мы говорим о начале строки, то & лишний (в данном случае); несмотря ни на что... when in doubt, try it out
@markp-fuso Что касается второго цикла, я вижу ошибку. Однако цикл работает только с небольшим файлом текущего журнала. Так что в худшем случае это может занять около 5-10 дополнительных секунд. А остальные 50...
есть ли у вас сроки обработки каждого файла? если нет, то добавьте некоторые тайминги (например, date перед обработкой каждого файла; разница между двумя выборками $SECONDS и т. д.); эти вызовы подоболочки (для получения того же date результата) требуют больших затрат времени; в зависимости от количества строк и вашей ОС... эти вызовы $(date ...) могут привести к тому, что последняя обработка файла журнала будет занимать слишком большую часть общего времени
@markp-fuso вау, я не знал о $SECONDS. Переделал все с ним. Однако производительность та же: 65 секунд. Я добавил тайминги в файлы. И большинство из них обрабатываются немедленно. Хотя есть такие и с 27/25/7 секундами. Это большие файлы размером более 400 КБ (остальные ~4 КБ). Обновил рассматриваемый код.
В чем смысл цикла while read? Вы можете просто echo "$unzipped_log" с гораздо меньшими накладными расходами или позволить вашему zcat | sed | ... конвейеру записывать данные напрямую в стандартный вывод и вообще никогда не сохранять содержимое в переменной unzipped_log.
Как человек, который много пишет как на bash, так и на Python (помимо других языков), я бы посчитал код bash, производительность которого на 1/6 ниже производительности эквивалентного Python, вполне конкурентоспособным. Люди, которые совершают обычные ошибки (например, создают ненужные подоболочки или внешние исполняемые файлы в тесном внутреннем цикле), часто делают сценарии оболочки более чем в 100 раз медленнее, чем эквивалентные Python; на самом деле вы делаете некоторые умеренно неэффективные вещи, но (по большей части, исключая ненужный цикл while read line; do echo "$line"; done) делаете их только один раз для каждого файла, а не один раз для каждой строки.
Тем не менее, я хотел бы призвать вас не использовать кроличью нору «все имена моих файлов соответствуют этому шаблону, поэтому код, который их неправильно обрабатывает, в порядке». Худшее событие потери данных, с которым я когда-либо лично присутствовал, было вызвано тем, что кто-то сделал такое предположение - затем была добавлена новая программа, создающая файлы в том же каталоге, и библиотека C, используемая этой программой Python («безопасная для памяти» , ха!) имело переполнение буфера, которое сбрасывало случайный мусор в строку, используемую для имен файлов.
Конечным результатом было то, что мы получили имя файла с * в нем, окруженным пробелами, и когда небрежно написанный сценарий оболочки попытался удалить этот файл, он удалил все наши резервные копии, используемые для поддержки выставления счетов. Это не то место, где вы хотите быть; Просто скажите «нет» расширениям без кавычек, чтобы попытаться обрабатывать строки в переменных оболочки, как если бы они были массивами.
(также избегайте cat foo | ... в пользу <foo ... или ... <foo - не большая разница, когда ... равен sed, но когда это что-то вроде tail или sort или wc -c, которое можно оптимизировать при наличии доступного для поиска файлового дескриптора, вы можете упустить очень реальный прирост производительности ).
(и заведите привычку запускать свой код через shellcheck.net - он не найдет ничего, что явно ориентировано на производительность, но доступно множество небольших оптимизаций корректности).
ls $dir собирается забрать latest.log, так что вам придется обрабатывать latest.log дважды, да? Это то что ты хочешь?
Спасибо всем за поддержку! Я думаю, что код можно оптимизировать дальше, но я рад, что разница уже х2,5, а не х6.
@markp-fuso Этого не происходит, latels.log только один раз
из последнего обновления - unzipped_log = "$(zcat ... | sed ...)" ; echo "$unzipped_log" - зачем нести накладные расходы на а) вызов подоболочки zcat | sed и б) сохранение всех результатов в памяти только для echo ввода следующей строки кода; как уже указывалось в комментариях и принятом ответе... устраните эти накладные расходы, заменив эти 2 строки кода одной строкой zcat | sed; то же самое касается last = "$(zcat | sed)" ; echo "$last"
@markp-fuso Для меня не было очевидным, что переменные будут настолько убийственны для производительности: 0. Спасибо!
Подумайте об операциях с точки зрения того, как они реализованы. Подключить программу к FIFO, прочитать ее выходные данные из этого FIFO, а затем снова записать их - вместо того, чтобы просто записывать выходные данные непосредственно в то место, где вы хотите, чтобы они были в первую очередь, - это будет накладными расходами. на любом языке.
Имейте в виду, что еще хуже то, что echo не гарантирует, что ваше содержимое будет побайтно идентично исходным значениям, но это совершенно другая тема. (Если вас волнует надежность, вместо этого используйте printf — см. unix.stackexchange.com/a/65819).
это не большая проблема с производительностью, но cat здесь не нужен - cat $dir/latest.log | sed -e ... -e ... - поскольку sed способен читать непосредственно из файла; вместо этого рассмотрите sed -e ... -e ... "$dir/latest.log" (также возьмите за привычку заключать ссылки на переменные в двойные кавычки (запустите тест, чтобы увидеть, что произойдет, если dir содержит пробелы и вы не используете двойные кавычки); во втором скрипте у вас есть echo $last, но если $last встроено переводы строк (\n), затем посмотрите на разницу вывода между echo $last и echo "$last"
у вас должна быть возможность сократить еще немного свободного времени, заменив неэффективные конструкции $(echo ... | cut ...); та же проблема, которая уже была избита до смерти... исключите ненужные подоболочки, особенно внутри цикла; Я добавил раздел к (принятому) ответу с более подробной информацией. устранение этих дополнительных 4 подоболочек (для каждого прохода цикла) должно немного сократить общее время; Кстати, вы можете получить немного более детальный тайминг с помощью time for f in "$dir"/*; do ... done






С последним обновлением вопроса кажется, что комментарии не имеют достаточного смысла, повторяя предложения...
unzipped_log = "$(zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g")"
while read -r line
do
echo "$line"
done <<< "$unzipped_log"
Устраните сохранение всех результатов zcat ... | sed ... в переменной и вместо этого передавайте их непосредственно в цикл while/read:
while read -r line
do
echo "$line"
done < <(zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g")"
Но поскольку единственное, что делает цикл, это «прочитать строку, повторить строку, прочитать строку, повторить строку», мы можем просто исключить цикл:
zcat -f $dir/$log | sed -e "s/\[Async Chat Thread - #[0-9]\+/INFO]: //" -e "s/^/$log_date /g"
Тот же самый дизайн кода можно использовать для замены второго цикла while/read.
ПРИМЕЧАНИЕ. переместите $date_today в сценарий sed так же, как это было сделано для $log_date.
Конструкция var1=$(echo $var2 | cut -b start-length) слишком сложна и требует двух дополнительных (дорогих) подоболочек. Тот факт, что эта конструкция используется дважды внутри цикла while, усиливает снижение производительности.
С помощью bash 4.2+ мы можем использовать встроенную возможность подстроки:
${var:start:len}
Имейте в виду, что нумерация позиций bash начинается с 0, поэтому, если вы начинаете с x=123456 и хотите, чтобы первые 3 символа: ${x:1:3} => 234 while ${x:0:3} => 123
Используя эту информацию, мы можем изменить текущий код OP следующим образом:
### replace this:
log=$(echo $f | cut -b 36-)
log_date=$(echo $log | cut -b 1-10)
### with this:
log = "${f:35}"
log_date = "${log:0:10}"
ПРИМЕЧАНИЕ. Я предполагаю, что OP имеет дело с однобайтовыми символами, поэтому cut -b и cut -c будут эквивалентны; если OP имеет дело с многобайтовыми символами, то код замены может быть немного более сложным
Конструкцию logs=$(ls $dir) / for log in $logs можно улучшить, но было бы полезно иметь образец реальных имен файлов в каталоге $dir.
Отдельно от разглагольствований в комментариях — имейте в виду, что когда вы делаете sed ..., например, в Bash, вы запускаете целый подпроцесс — подпроцесс, поддерживаемый ОС, с большими накладными расходами — в то время как в Python вызов функции просто запустит пару коды операций. То же самое касается ls и почти всех «команд», за исключением нескольких встроенных. Есть исключения, которые со временем стали встроенными, например echo.
Люди, владеющие Bash, обычно знают, как избежать этих ловушек, и просто не вызывают подпроцесс в циклах. Поэтому люди в комментариях советуют вам «никогда не использовать ls в скрипте».
С другой стороны, такой язык, как Python, имеет стандартный синтаксис, все будет выполняться в одном процессе, и если нужно запустить что-то во внешнем процессе, это совершенно очевидно: вызовы функций будут занимать в 1000 раз меньше ресурсов ОС, чем внешний процесс. процесс запущен.
echo является встроенным, поэтому, если кто-то не заставит /usr/bin/echo, он не попадает в ту же категорию, что ls или sed. (Тем не менее, встроенное echo в bash менее эффективно, чем print() или sys.stdout.write() в Python, поскольку оно должно сбрасываться после каждой операции, тогда как запись или печать в Python могут откладывать сброс, когда стандартный вывод полностью буферизован).
Спасибо за информацию - я обновляю ее.
То, что это подпроцесс, не является причиной отказа от ls в сценариях.
Вы делаете что-то не так, но это не источник вашей проблемы: не используйте ls в скриптах