Сразу скажу, что использование Python для написания простых скриптов - оверкилл. Потому что, в отличие от скриптовых языков (Bash/SH), это полноценный язык программирования. Тем не менее в этом есть свои плюсы. Я так и не осилил скриптовые языки, и каждая попытка воспользоваться оными оборачивалась болью.
Если Вы хорошо знаете скриптовые языки и у Вас есть огромный опыт - то этот пост не для Вас. А всем остальным - добро пожаловать.
Для понимания статьи у Вас уже должны быть базовые знания питона.
Много кода было взято отсюда, не забудьте посетить ссылку:
https://github.com/ninjaaron/replacing-b...ith-python
Причины
Начну с того, что этот пост не является агитацией за тот или иной язык, а является, скажем так, наглядным сравнением. Выбор остаётся за читателем.
Заголовок немного лукавит. Питон не является равноценной заменой скриптовым языкам, т.к. они состоят в разных весовых категориях. Однако питон имеет замену большинству возможностей Bash/SH. Разумеется, ни о какой замене всего Bash/SH на Python (в т.ч. для установки по умолчанию в терминале) речи не идёт.
Для себя я выбрал питон по следующим причинам:
- Работа со строками. Т.к. это полноценный язык программирования, то в нём присутствует полноценная работа со строками. Парсинг, форматирование, разделение и т.д. Никакого жонглирования с grep и awk.
- Различные типы переменных. Строки являются строками, числа - числами. Это позволяет нормально сравнивать переменные и проводить с ними операции.
- Возможность писать код полностью без функций. Т.е. прямо как Bash/SH.
- Полноценные циклы и итераторы
- Обработка данных средствами языка, без вызова внешних программ. Bash/SH являются всего лишь клеем для внешних приложений.
- Наличие API у различных штук, что позволяет не парсить выхлоп программы, а взаимодействовать с ней по-человечески
- Да и в целом приятнее писать, благодаря синтаксическому сахару
А ещё питон позволяет избежать подобных нюансов:
Код:
foo = 'test test1 test2'
echo $foo
# test test1 test2
echo "$foo"
# test test1 test2
Можно вообще на любом языке писать, на самом деле, хоть на сишке. Но питон - это современный BASIC, не требующий предварительной компиляции, и входящий в поставку во многих дистрибутивах Linux, так что почему бы и не воспользоваться им.
Примеры
Некоторые примеры будут притянуты за уши, т.к... эээ... когда я садился за написание статьи, я не задумывался о том, что у меня уже давно не возникало каких-то задач по теме. Так что задачи будут абстрактными, но показанный код может быть применим в реальных задачах.
Начнём с простого. Есть модуль shutil, который реализовывает многие низкоуровневые вещи. Ещё есть модуль os.
Код:
shutil.move(src, dest) //mv
shutil.copy2(src, dest) //cp
shutil.copytree(src, dest) //cp -r
shutil.rmtree(dir) //rm -r
shutil.chown(file, user, group) //chown
shutil.which(file) //which
os.remove(file) //rm
os.rename(old_name, new_name) //mv
os.mkdir(dir) //mkdir
os.makedirs(dir) //mkdir -p
Например, вам надо произвести некую каталогизацию. В данном случае это будут директории с годом в названии, а в них - директории с номером месяца.
Код:
import os
base_path = "."
for year in range(2000, 2011):
for month in range(1, 13):
os.makedirs(f"{base_path}/{year}/{month}", exist_ok=True)
Я не знаю, почему верхняя граница диапазона требует указать на единицу больше. Тем не менее всё работает. Аргумент exist_ok=True указывает не выдавать ошибку, если директория уже существует.
Скрипт можно сделать чуть гибче:
Код:
import os
import sys
if len(sys.argv) < 2:
print("Укажите базовый путь")
sys.exit(-1)
base_path = sys.argv[1]
for year in range(2000, 2011):
for month in range(1, 13):
os.makedirs(f"{base_path}/{year}/{month}")
Теперь можно указать директорию, в которой будут создаваться другие директории, аргументом для скрипта. Что-то вроде python yourscript.py /home/user/your_folder/.
На самом деле у питона в стандартной библиотеке есть мощный
модуль для работы с аргументами командной строки, но в этот раз обойдёмся без него.
В Python есть средства для получения информации. Давайте напишем небольшой скрипт, отображающий информацию о системе (что-то вроде screenfetch):
Код:
import os
import platform
import psutil
def get_uptime():
with open("/proc/uptime", "r") as f:
uptime = f.read().split(" ")[0].strip()
uptime = int(float(uptime))
return uptime
# Переменные
fetch_user = os.environ["USER"]
fetch_host = os.environ["HOSTNAME"]
fetch_de = os.environ["DESKTOP_SESSION"]
fetch_shell = os.environ["SHELL"]
fetch_home = os.path.expanduser("~")
fetch_os = platform.system();
fetch_kernel = platform.uname()
fetch_cpu = platform.processor()
fetch_dist = platform.freedesktop_os_release()
fetch_ram = psutil.virtual_memory()
fetch_swap = psutil.swap_memory()
print(f"{fetch_user}@{fetch_host}")
print(f"Shell: {fetch_shell}")
print(f"Home: {fetch_home}")
print(f"OS: {fetch_os}")
print(f"Distro: {fetch_dist['PRETTY_NAME']}")
print(f"Kernel: {fetch_kernel.release}")
print(f"Desktop: {fetch_de}")
print(f"CPU: {fetch_cpu}")
print(f"Memory (GB): {fetch_ram[0] // 1000000000} (total), {fetch_ram[3] // 1000000000} (used)")
print(f"Swap (GB): {fetch_swap[0] // 1000000000} (total), {fetch_swap[3] // 1000000000} (used)")
print(f"Uptime: {get_uptime() // 3600} hours, {(get_uptime() % 3600) // 60} minutes")
Пример выхлопа команды:
Код:
user@desktop
Shell: /usr/bin/zsh
Home: /home/user
OS: Linux
Distro: Fedora Linux 36 (KDE Plasma)
Kernel: 6.0.8-xm1.0.fc36.x86_64
Desktop: plasma
CPU: x86_64
Memory (GB): 16 (total), 9 (used)
Swap (GB): 17 (total), 0.0 (used)
Uptime: 9 hours, 51 minutes
Все модули отработали, кроме отображения модели процессора. Не знаю, только ли у меня такой косяк. Но раз модуль не отработал, то можно пропарсить /proc/cpuinfo, благо что в питоне это делается очень легко:
Код:
def get_cpu():
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "model name" in line:
return(line.split(": ")[1].strip())
Здесь мы проверили строки на предмет содержания "model name" и разделили найденную строку по двоеточию с пробелом. В данном случае процессор у меня один, и я просто сделал возврат первого совпадения. Но что, если установлены два разных процессора?
Мы не будем учитывать количество ядер (т.к. я не знаю, как), только разные модели процессоров.
Поменяем функцию:
Код:
def get_cpu():
cpu_list = []
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "model name" in line:
cpu_model = line.split(": ")[1].strip()
if cpu_model not in cpu_list:
cpu_list.append(cpu_model)
return cpu_list
И поменяем код вывода модели процессора:
Код:
if len(fetch_cpu) == 1:
print(f"CPU: {fetch_cpu[0]}")
else:
for cpu in fetch_cpu:
print(f"CPU {fetch_cpu.index(cpu) + 1}: {cpu}")
Мы просто проверяем длину списка с процессорами, и в зависимости от неё выводим разный текст. Прибавление единицы можно и убрать, просто мне так больше нравится.
А вот пример реальной задачи, которая у меня возникла пару месяцев назад. Мне нужно было сжать образы игр .ISO для PSP. И вроде бы ничего особенного, всего лишь запустить бинарь с нужными аргументами. Но игр было довольно много.
Я уже писал выше, что Bash/SH - это клей для внешних приложений. И питон тоже может быть им!
Код:
import os
import subprocess
import sys
ISO_LIST = os.listdir(sys.argv[1])
FOLDER_NAME = sys.argv[2]
os.makedirs(FOLDER_NAME, exist_ok=True)
for iso in ISO_LIST:
if iso.endswith(".iso"):
print(f"Now compressing: {iso}")
out_file = os.path.splitext(iso)[0] + '.cso'
sp = subprocess.run(["./ciso", "9", iso, f"{FOLDER_NAME}/{out_file}"])
В данном скрипте в цикле запускается внешний
бинарь, а у выходных файлов указывается расширение .CSO. За запуск внешних процессов отвечает модуль
subprocess.
Код:
user@desktop ~/G/ciso> python ciso_all_isos.py . cso_files master?
Now compressing: From Russia with Love - 007.iso
Compressed ISO9660 converter Ver.1.02 by BOOSTER
Compress 'From Russia with Love - 007.iso' to 'cso_files/From Russia with Love - 007.cso'
Total File Size 732495872 bytes
block size 2048 bytes
index align 1
compress level 9
ciso compress completed , total size = 565886491 bytes , rate 77%
Now compressing: Family Guy.iso
Compressed ISO9660 converter Ver.1.02 by BOOSTER
Compress 'Family Guy.iso' to 'cso_files/Family Guy.cso'
Total File Size 1186463744 bytes
block size 2048 bytes
index align 1
compress level 9
ciso compress completed , total size = 1101765517 bytes , rate 92%
Вообще, пакетная обработка в питоне сделана довольно приятно. Вот ещё один пример. В нём я хочу залить несколько картинок на сервер
https://0x0.st.
Код:
import os
import requests
FILE_LIST = os.listdir()
def upload_pic(filename):
with open(filename, "rb") as fname:
r = requests.post("https://0x0.st", files={"file": fname})
return r.text.strip()
for f in FILE_LIST:
if f.endswith(".jpg"):
print(upload_pic(f))
Запускаем:
Код:
# python upload_pics.py
https://0x0.st/ohHP.jpg
https://0x0.st/ohHZ.jpg
https://0x0.st/ohHN.jpg
https://0x0.st/ohHq.jpg
https://0x0.st/ohHb.jpg
requests - неофициальный, но очень популярный модуль реализующий HTTP клиент.
Или наоборот, сделаем качалку:
Код:
import sys
import requests
with open(sys.argv[1], "r") as fname:
FILE_LIST = fname.readlines()
for line in FILE_LIST:
line = line.strip()
r = requests.get(line)
outfile = line.split("/")[-1]
with open(outfile, "wb") as ofname:
ofname.write(r.content)
Закидываем ссылки в текстовик и запускаем:
Код:
python download_files.py files.txt
Пример парсинга файла. Подсчитаем количество уникальных IP адресов в логе веб-сервера Nginx.
Код:
import sys
LOG_FILE = sys.argv[1]
UNIQUE_IPS = []
ALL_IPS = 0
logfile = open(LOG_FILE, "r")
for line in logfile:
ip = line.split(" ")[0]
ALL_IPS += 1
if ip not in UNIQUE_IPS:
UNIQUE_IPS.append(ip)
logfile.close()
print(f"Unique IPs: {len(UNIQUE_IPS)}, all IPs: {ALL_IPS}")
Пример работы:
Код:
# python unique_ips.py access.log
Unique IPs: 215, all IPs: 4373
Можно было бы воспользоваться регуляркой, или добавить дополнительные проверки... но сам формат лог-файла простой. Хотя... А вдруг нам подсунули не лог-файл?
Код:
import sys
import re
LOG_FILE = sys.argv[1]
UNIQUE_IPS = []
ALL_IPS = 0
IP_REGEX = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}"
logfile = open(LOG_FILE, "r")
for line in logfile:
ip = re.findall(IP_REGEX, line)[0]
ALL_IPS += 1
if ip not in UNIQUE_IPS:
UNIQUE_IPS.append(ip)
logfile.close()
print(f"Unique IPs: {len(UNIQUE_IPS)}, all IPs: {ALL_IPS}")
Заключение
Python вполне может быть клеем для внешних программ. Благодаря средствам языка и своей довольно богатой библиотеке, можно писать скрипты, зачастую не прибегая к вызову внешних программ, полагаясь на обработку данных средствами самого языка.