
Гой еси, Хабр!
Звать меня Артем Клещев, я технический писатель в СберТехе. Работа моя — складывать сказания да инструкции для достославного продукта Platform V DropApp.
А продукт наш, если на современный лад перевести — царство-государство под названием Kubernetes да с верной свитой операторов. Управляют они приложениями в контейнерах, как древние витязи — дружиной своей.
Хоть и славно наше царство, а и в нем есть работа рутинная, небогатырская. Вот и решился я выковать себе меч-кладенец, приложение на языке Python, что само будет рутину рубить.
Для сведущего в разработке богатыря — ерундовое дело. А для меня настоящее сражение с чудом-юдом невиданным. И не выйти мне победителем, кабы не ИИ, который на каждом шаге делал за меня самую важную работу.
Но и с ним всё было не так гладко. Расскажу, с какими сложностями столкнулся я на своём пути и какие шишки набил.
Надеюсь, опыт мой будет полезен как минимум моим коллегам-техническим писателям. У нас часто встречаются рутинные задачи, на которые жалко тратить рабочее время — и хочется сковать себе меч-кладенец, да непонятно, с чего начать.
О техстеке-супостате
Продукт наш — могучее царство Kubernetes с операторами для управления контейнерными приложениями. Царство это не простое, а семикомпонентное. И в каждом компоненте, помимо своей дружины разработческой, туча союзников опенсорсных.
Каждый релиз нужно нам готовить технические описания этих союзников и отдавать юристам для проверки лицензий. Вот как это выглядит (зелёным выделен инструмент, версия которого не менялась).

Проблем в подготовке такого техстека, казалось бы, нет. Сиди, сравнивай цифры, значения версий да крась ячейки в Excel. Но не скоро сказка сказывается:
Ручной процесс сравнения занимает около 8-9 часов тщательной проверки и переписывания для каждого релиза. С развитием продукта и ростом количества инструментов этот процесс будет только удлиняться.
Окончательные версии инструментов каждого релиза собраны в релизном конфиге в формате.yaml — это сотни строк кода, которые нужно внимательно перечитать.
Решил я тогда, что вместо палицы ручной работы нужен нам меч‑кладенец автоматизации. Заручился помощью ИИ и отправился на ратный подвиг — ковать приложение, которое будет само собирать данные о техстеке и отправлять в Excel-таблицы.
Для начала нарисовал себе карту путевую, по которой буду двигаться к приложению, и наметил такие шаги:
подключиться к BitBucket;
преобразовать .yaml в .xlsx;
настроить Excel;
разработать графический интерфейс.
Призвал на помощь помощника искусственного и бросился ковать код. Хотел похвастать удалью молодецкой, да не учёл, что без опыта работы с ИИ не получится ни буквы кода выковать.
Так и вышло — первые же попытки сломались о суровый камень ошибок синтаксиса.
Первые попытки кода и нерабочие промпты



Понял я, что нужна техника. Не случайно мудрые люди советуют оттачивать навык общаться с ИИ-помощником с помощью точных и метких промптов.
Так как я работал с GigaChat, разобраться с промптингом мне помог открытый курс в СберУниверситете. Вот ссылки, которые пригодились — возможно, найдёте их полезными:
И вот, с отточенным резцом промптинга в руках, я был готов выковать свой первый настоящий клинок. Первым делом предстояло проложить путь к самой сокровищнице — к Bitbucket.
Прокладываю путь к Bitbucket
Но и тут меня поджидала не одна засада. То не хватало какой-то библиотеки, то мой промпт оказывался недостаточно острым, и ИИ понимал меня криво. А то и я его ответа понять не мог.
Я наносил удары наугад: «как подключиться к Bitbucket», «как получить YAML-файл». Запускал сырой, неработающий код, получал в ответ ошибку, а затем просил ИИ исправить оплошность. То просил заменить авторизацию по паролю на HTTP-токен, то молил вывести на экран хоть что-то, кроме гневных ругательств компилятора.
Но с каждой ошибкой я учился формулировать запросы точнее: писал для себя понятные комментарии в коде и просил их реализовать: «Сделай вот так, как я написал».
Наконец — первый чистый удар! Выковал я такой код, он первый сработал и добыл информацию из .yaml-файла в Bitbucket и вывел её на экран.
import requests
import yaml
import json
import pandas as pd
from atlassian import Bitbucket
from requests.packages.urllib3.exceptions import InsecureRequestWarning
# Конфигурация
BITBUCKET_URL = 'https://sberworks.ru/bitbucket-ci/projects/{workspace}/repos/{repo_slug}/raw/{file_path}'
WORKSPACE = 'your_workspace' # Замените на ваше имя рабочего пространства
REPO_SLUG = 'your_repo_slug' # Замените на имя вашего репозитория
BRANCH = 'main' # Замените на нужную ветку
FILE_PATH = 'resources/config/components/k8so/release_k8so_3.2.0.yaml' # Замените на путь к вашему YAML-файлу
HTTP_TOKEN = 'your_http_token' # Замените на ваш HTTP-токен
# Получение YAML-файла из Bitbucket
headers = {
'Authorization': f'Bearer {HTTP_TOKEN}'
}
response = requests.get(BITBUCKET_URL.format(workspace=WORKSPACE, repo_slug=REPO_SLUG, branch=BRANCH, file_path=FILE_PATH), headers=headers, verify=False)
# Проверка статуса ответа
yaml_data = None
if response.status_code == 200:
try:
yaml_data = yaml.safe_load(response.text)
except yaml.YAMLError as e:
print("Ошибка при разборе YAML:", e)
else:
print(f"Ошибка при получении файла: {response.status_code}")
print(yaml_data)
На какие грабли наступил и какие выводы сделал:
Нужно просить ИИ показывать и объяснять ошибки. Если код не срабатывает, закидывайте описание ошибки в промпт и спрашивайте, что не так, а потом требуйте исправить проблему в коде.
Обязательно писать комментарии в коде, это поможет увидеть, какие именно ошибки допущены.
Не бояться количества итераций. Автоматизация без опыта — наверное, самая яркая иллюстрация цитаты Эдисона про 10 тыс. неработающих способов.
Перековываю .yaml в .xlsx
Успех окрылил, но ненадолго. Нужно было не просто добыть данные, а перековать их в аккуратную таблицу Excel. И здесь мой меч вновь затупился о непонятную структуру.
Данные в YAML были спрятаны не в простом словаре, а в лабиринте, где ключ repositories:
к целому арсеналу разных значений, сваленных в одну кучу — в список. Мне же нужно было выудить из этой кучи две конкретные вещи: название инструмента и его версию (upstream_tag:
).
Долго бился я впустую, пока меня не осенило: я атаковал список так, будто это был словарь. Я не понимал, что сначала этот хаотичный список нужно переплавить в упорядоченный словарь, и только потом выковывать из него нужные пары «ключ-значение».
import requests
import yaml
import json
import pandas as pd
from atlassian import Bitbucket
from requests.packages.urllib3.exceptions import InsecureRequestWarning
# Отключение предупреждений о небезопасных запросах (не рекомендуется для продакшена)
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
# Конфигурация
BITBUCKET_URL = 'https://sberworks.ru/bitbucket-ci/projects/{workspace}/repos/{repo_slug}/raw/{file_path}'
WORKSPACE = 'your_workspace' # Замените на ваше имя рабочего пространства
REPO_SLUG = 'your_repo_slug' # Замените на имя вашего репозитория
BRANCH = 'main' # Замените на нужную ветку
FILE_PATH = 'resources/config/components/k8so/release_k8so_3.2.0.yaml' # Замените на путь к вашему YAML-файлу
HTTP_TOKEN = 'your_http_token' # Замените на ваш HTTP-токен
# Получение YAML-файла из Bitbucket
headers = {
'Authorization': f'Bearer {HTTP_TOKEN}'
}
response = requests.get(BITBUCKET_URL.format(workspace=WORKSPACE, repo_slug=REPO_SLUG, branch=BRANCH, file_path=FILE_PATH), headers=headers, verify=False)
# Проверка статуса ответа
yaml_data = None
if response.status_code == 200:
try:
yaml_data = yaml.safe_load(response.text)
except yaml.YAMLError as e:
print("Ошибка при разборе YAML:", e)
else:
print(f"Ошибка при получении файла: {response.status_code}")
# Отладочная информация
print("Полученные данные YAML:", yaml_data)
# Подготовка данных для DataFrame
data = []
if isinstance(yaml_data, dict): # Проверяем, что yaml_data - это словарь
repositories = yaml_data.get('repositories', {})
for repo_name, details in repositories.items():
if isinstance(details, dict): # Проверяем, что details - это словарь
upstream_tag = details.get('upstream_tag', 'Не указано') # Значение по умолчанию
data.append({
'Repository': repo_name,
'Upstream Tag': upstream_tag
})
# Создание DataFrame
if data:
df = pd.DataFrame(data)
# Сохранение в Excel
try:
df.to_excel('output.xlsx', index=False)
print("Excel файл успешно создан: output.xlsx")
except PermissionError:
print("Ошибка: файл 'output.xlsx' уже открыт или нет прав на запись. Попробуйте закрыть файл или изменить имя.")
else:
print("Нет данных для сохранения в Excel.")
Наконец вывел меня ИИ на дорожку прямоезжую! На выходе я получил не просто файл, а тот самый идеально выкованный клинок данных: Excel-таблицу, где в одной колонке красовались имена инструментов, а в другой — их версии.


Заодно научил свой меч предупреждать об опасности: попросил добавить обработку ошибки, если файл уже открыт.

Добавляю три точеных лезвия для подготовки данных
Мой скрипт уже добывал данные, но этого было мало. Чтобы по-настоящему автоматизировать процесс, ему предстояло научиться сравнивать, оформлять и готовить данные к бою — то есть, к отправке юристам.
Я решил выковать для своего цифрового орудия три новых лезвия.
1. Лезвие первое — для сравнения версий
Продукт наш, как я уже говорил, состоит из семи компонентов. И для каждого релиза нужно было готовить свой отчёт. Рубя вручную, я рисковал пропустить важное изменение.
Мой промпт‑приказ был таков — добавить в выходной Excel‑файл третий столбец, в который будет добавляться значение ключа upstream_tag:
из соседнего файла с другой версией.
Попросил добавить код, чтобы я указывал версии продукта, а скрипт попеременно забирал информацию из нескольких файлов с нужными версиями.

Добавил код для переименования столбцов и листов:


2. Лезвие второе — для остроты взгляда
Юристам важно видеть, что изменилось. Выискивать каждое изменение вручную — всё равно что искать иголку в стоге сена при тусклом свете.
Я приказал закалить клинок новой способностью: «Добавь в код сравнение столбцов с версиями. Если версия инструмента не изменилась между релизами — закрась ячейку в зелёный цвет».

Так мой скрипт приобрёл зрение. Теперь он не просто бездумно копил данные, а анализировал их, выделяя самое важное.

3. Лезвие третье — для финальной закалки
Финальный техстек требовал данных в специфичном формате: «Название_инструмента версия». Мой скрипт же выводил их в двух разных ячейках. Копировать и склеивать их вручную — значит снова скатываться в рутину.
Финальный точный удар: «Создай в таблице новый столбец. В него нужно объединить данные из первого и второго столбца, поставив между ними пробел».

Вот что получилось:

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


На какие грабли наступил и какие выводы сделал: сначала я попробовал добавлять функции вразнобой, без особого порядка, но быстро понял, что каждая новая просьба усложняет код, и я начинаю путаться. Чтобы опять не наломать дров, я вывел для себя такой алгоритм донастройки инструмента:
Реши базовую задачу (добыча данных).
Добавь один новый кейс (работа с двумя версиями).
Наложи на результат слой анализа (сравнение и цветовое выделение).
Доведи вывод до нужного формата (финальное форматирование).
Мой цифровой меч превратился в настоящее богатырское оружие — он сам:
Добывал сырьё (данные из Bitbucket).
Переплавлял его (конвертировал YAML в структуры Python).
Ковал и сравнивал (создавал и анализировал таблицы).
Приводил в боевой вид (форматировал и готовил к использованию).
Однако не к лицу витязю довольствоваться малым. Решил я выковать своему помощнику рукоять удобную да баскую — графический интерфейс, другими словами.
Выковываю богатырский GUI
Требования к рукояти сначала были простыми и функциональными:
поля ввода для актуальной и предыдущей версий;
возможность менять надписи перед полями;
кнопка запуска.
Можно было остановиться и на этом, но не радовалось сердце простому серому окошку. Пожелал я, чтобы интерфейс стал ещё и удобным, понятным и чуть более богатырским.
Обратился снова к ИИ: попросил сделать так, чтобы менялся цвет у поля для ввода имени выходного файла при расхождении с объясняющей фразой. Да добавить красоты и яркости в виде фона былинного.


Конец — делу венец
Слово числам — вот трофей долгожданный, добытый трудом праведным: подготовка техстека сократилась с 8-9 часов скрупулёзного труда до 30 минут под руководством автоматизированного помощника. Вся работа по его созданию заняла у меня около 9-10 часов. Это чуть больше, чем один ручной цикл подготовки отчёта. Но это были инвестиции, которые окупились многократно уже на втором релизе.
Главное — приобрёл я базовые знания, с которыми уже не страшно подойти к новой задаче автоматизации.
А тем, кто только задумывается о своём первом скрипте, я оставлю три самых ценных совета, выкованных в этой битве:
Написать рабочий код вполне реально, даже если вы не знаете ни аза на Python.
Поизучайте базовые принципы промптинга, а потом, в процессе, много спрашивайте ИИ.
Если что-то не работает — просите ИИ показать ошибку, делайте шаг назад и исправляйте.
Буду рад вашим комментариям и вопросам. И да пребудет с вами сила автоматизации!
Код скрипта
import requests
import yaml
import pandas as pd
import re
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import os
import tkinter as tk
from tkinter import messagebox
import numpy as np
from PIL import Image, ImageTk # Импортируем необходимые классы из Pillow
# Нужно установить `pip install xlsxwriter`
# Нужно установить `pip install pyinstaller`
# Нужно установить `pip install Pillow`
# Отключение предупреждений о небезопасных запросах (не рекомендуется для продакшена)
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
# Функция для выполнения основного кода
def run_script(version_1, version_2, output_file):
# Конфигурация
BITBUCKET_URL = 'https://sberworks.ru/bitbucket-ci/projects/{workspace}/repos/{repo_slug}/raw/resources/config/components/{file_path}'
WORKSPACE = 'DAPP' # Замените на ваше имя рабочего пространства
REPO_SLUG = 'dapp-jenkins-lib' # Замените на имя вашего репозитория
BRANCH = 'master' # Замените на нужную ветку
FILE_PATHS = [ # Замените на пути к вашим YAML-файлам
f'k8sc/release_k8sc_{version_1}.yaml',
f'k8sc/release_k8sc_{version_2}.yaml',
f'k8su/release_k8su_{version_1}.yaml',
f'k8su/release_k8su_{version_2}.yaml',
f'k8so/release_k8so_{version_1}.yaml',
f'k8so/release_k8so_{version_2}.yaml',
f'k8si/release_k8si_{version_1}.yaml',
f'k8si/release_k8si_{version_2}.yaml'
]
HTTP_TOKEN = os.getenv("HTTP_TOKEN_BITBUCKET")
if HTTP_TOKEN is None:
raise ValueError("Переменная окружения 'HTTP_TOKEN_BITBUCKET' не установлена.")
# Получение данных из нескольких YAML-файлов
headers = {
'Authorization': f'Bearer {HTTP_TOKEN}'
}
# Подготовка данных для DataFrame
all_data = {}
for file_path in FILE_PATHS:
response = requests.get(BITBUCKET_URL.format(workspace=WORKSPACE, repo_slug=REPO_SLUG, branch=BRANCH, file_path=file_path), headers=headers, verify=False)
# Проверка статуса ответа
if response.status_code == 200:
try:
yaml_data = yaml.safe_load(response.text)
if isinstance(yaml_data, dict): # Проверяем, что yaml_data - это словарь
repositories = yaml_data.get('repositories', {})
# Извлекаем версию из имени файла
version = re.search(r'_(\d+\.\d+\.\d+)', file_path.split('/')[-1])
version_number = version.group(1) if version else 'Не указано' # Получаем номер версии
for repo_name, details in repositories.items():
if isinstance(details, dict): # Проверяем, что details - это словарь
upstream_tag = details.get('upstream_tag', 'Не указано') # Значение по умолчанию
# Группируем данные по папкам
folder_name = '/'.join(file_path.split('/')[:-1]) # Получаем имя папки
if folder_name not in all_data:
all_data[folder_name] = {}
if repo_name not in all_data[folder_name]:
all_data[folder_name][repo_name] = {}
all_data[folder_name][repo_name][version_number] = upstream_tag # Убираем "Upstream Tag"
except yaml.YAMLError as e:
print(f"Ошибка при разборе YAML из файла {file_path}:", e)
else:
print(f"Ошибка при получении файла {file_path}: {response.status_code}")
# Создание DataFrame и сохранение в Excel
if all_data:
try:
with pd.ExcelWriter(output_file, engine='xlsxwriter') as writer: # Используем имя файла из интерфейса
for folder_name, repos in all_data.items():
df = pd.DataFrame.from_dict(repos, orient='index').reset_index()
df.columns = ['Repository'] + list(df.columns[1:]) # Переименовываем первый столбец
# Добавление нового столбца с объединенными значениями
df['Combined'] = df['Repository'] + ' ' + df.iloc[:, 1].astype(str) # Объединяем первый и второй столбцы с пробелом
sheet_name = folder_name.split('/')[-1] # Имя листа - имя папки
df.to_excel(writer, sheet_name=sheet_name, index=False) # Сохраняем в отдельный лист
# Получаем доступ к объекту workbook и worksheet
workbook = writer.book
worksheet = writer.sheets[sheet_name]
# Условное форматирование для выделения ячеек
green_format = workbook.add_format({'bg_color': '#00FF00'}) # Зеленый цвет
# Проверяем совпадения в столбцах с именами версий B и C
for row in range(1, len(df) + 1): # Начинаем с 1, чтобы пропустить заголовок
# Получаем значения в строке для столбцов B и C
value_b = df.iloc[row - 1, 1] # Значение в столбце B
value_c = df.iloc[row - 1, 2] # Значение в столбце C
# Проверяем, является ли значение NaN
if pd.isna(value_b) or pd.isna(value_c):
# Записываем пустые значения для NaN
worksheet.write(row, 1, '') # Записываем пустую строку в ячейку B
worksheet.write(row, 2, '') # Записываем пустую строку в ячейку C
elif value_b == value_c: # Если значения совпадают
# Выделяем ячейки в столбцах B и C
worksheet.write(row, 1, value_b, green_format) # Выделяем ячейку B
worksheet.write(row, 2, value_c, green_format) # Выделяем ячейку C
else:
# Записываем значения без форматирования
worksheet.write(row, 1, value_b) # Записываем значение в ячейку B
worksheet.write(row, 2, value_c) # Записываем значение в ячейку C
# Записываем остальные столбцы без изменений
for col in range(3, len(df.columns)): # Начинаем с 3, чтобы пропустить столбцы B и C
for row in range(1, len(df) + 1):
cell_value = df.iloc[row - 1, col]
if pd.isna(cell_value): # Проверяем на NaN
worksheet.write(row, col, '') # Записываем пустую строку для NaN
else:
worksheet.write(row, col, cell_value) # Записываем значение
# Автоматическая настройка ширины столбцов
for i, col in enumerate(df.columns):
max_length = max(df[col].astype(str).map(len).max(), len(col)) # Находим максимальную длину текста
worksheet.set_column(i, i, max_length + 2) # Устанавливаем ширину столбца с небольшим запасом
print(f"Господин! Excel файл успешно создан: {output_file}")
except PermissionError:
print("Ошибка: файл уже открыт или нет прав на запись. Попробуйте закрыть файл или изменить имя.")
else:
print("Нет данных для сохранения в Excel.")
# Создание графического интерфейса
def start_gui():
# Создание основного окна
root = tk.Tk()
root.title("YAML to Excel Converter")
# Загрузка изображения с помощью Pillow
image_path = "C:\\Users\\kleshchev.a.y\\Documents\\Work\\Scripts\\yaml-to-excel\\Hohloma.jpg"
background_image = Image.open(image_path)
background_image = background_image.resize((root.winfo_screenwidth(), root.winfo_screenheight()), Image.LANCZOS) # Изменяем размер изображения
background_image_tk = ImageTk.PhotoImage(background_image)
# Создание метки с изображением для фона
background_label = tk.Label(root, image=background_image_tk)
background_label.place(relwidth=1, relheight=1) # Занять всю область окна
# Создание фрейма для центрирования элементов
frame = tk.Frame(root, bg='white')
frame.place(relx=0.5, rely=0.5, anchor='center') # Центрируем фрейм
# Метки и поля ввода для версий
tk.Label(frame, text="Богатырь, введи старшую версию для сравнения:", font=("Arial", 16), bg='white', fg='black').grid(row=0, column=0, pady=5)
version_1_entry = tk.Entry(frame, width=50, font=("Arial", 16), justify=tk.CENTER, highlightthickness=2)
version_1_entry.grid(row=0, column=1, pady=5)
tk.Label(frame, text="Благодетель, извольте ввести вторую версию для сравнения:", font=("Arial", 16), bg='white', fg='black').grid(row=1, column=0, pady=5)
version_2_entry = tk.Entry(frame, width=50, font=("Arial", 16), justify=tk.CENTER, highlightthickness=2)
version_2_entry.grid(row=1, column=1, pady=5)
# Метка и поле ввода для имени выходного файла
tk.Label(frame, text="Имя выходного файла (необязательно):", font=("Arial", 16), bg='white', fg='black').grid(row=2, column=0, pady=5)
output_file_entry = tk.Entry(frame, width=50, font=("Arial", 16), justify=tk.CENTER, highlightthickness=2)
output_file_entry.grid(row=2, column=1, pady=5)
# Добавление текста-подсказки
placeholder_text = "Без заполнения будет создан файл output.xlsx"
output_file_entry.insert(0, placeholder_text)
output_file_entry.config(fg='gray', bg='gray86') # Установка серого цвета текста и белого фона
# Функция для обработки нажатия кнопки "Старт"
def on_start():
version_1 = version_1_entry.get()
version_2 = version_2_entry.get()
output_file = output_file_entry.get() or 'output.xlsx' # Используем стандартное имя, если поле пустое
# Добавляем расширение .xlsx, если оно не указано
if not output_file.endswith('.xlsx'):
output_file += '.xlsx'
if version_1 and version_2:
try:
run_script(version_1, version_2, output_file) # Передаем имя файла в функцию
messagebox.showinfo("Success", "Господин! Процесс завершен успешно!")
except Exception as e:
messagebox.showerror("Error", str(e))
else:
messagebox.showwarning("Input Error", "Пожалуйста, введите обе версии.")
# Обработчик для удаления текста-подсказки
def on_entry_click(event):
if output_file_entry.get() == placeholder_text:
output_file_entry.delete(0, tk.END) # Удаляем текст-подсказку
output_file_entry.config(fg='black') # Меняем цвет текста на черный
output_file_entry.config(bg='white') # Меняем фон на белый
# Обработчик для восстановления текста-подсказки
def on_focusout(event):
if output_file_entry.get() == '':
output_file_entry.insert(0, placeholder_text) # Восстанавливаем текст-подсказку
output_file_entry.config(fg='gray66') # Меняем цвет текста на серый
output_file_entry.config(bg='gray86') # Меняем фон на серый
# Привязка обработчиков событий
output_file_entry.bind('<FocusIn>', on_entry_click)
output_file_entry.bind('<FocusOut>', on_focusout)
# Кнопка "Старт"
start_button = tk.Button(frame, text="Мудрец! Запустить сравнение!", command=on_start,
bg="white", fg="black", font=("Arial", 18), width=25)
start_button.grid(row=3, columnspan=2, pady=10) # Центрируем кнопку под полями
# Запуск главного цикла
root.mainloop()
# Запуск графического интерфейса
if __name__ == "__main__":
start_gui()
GeorgyGordienko
Красота-то какая... лепота!