Сказ о том, как техпис без опыта программирования свой первый скрипт писал

Публикации в СМИ
10.10.2025
Опубликовано на
Хабре
a46e48f68ee7a638fe1f8e5308f4a371.png

Гой еси, PlatformV!

Звать меня Артем Клещев, я технический писатель в СберТехе. Работа моя — складывать сказания да инструкции для достославного продукта Platform V DropApp.

А продукт наш, если на современный лад перевести — царство-государство под названием Kubernetes да с верной свитой операторов. Управляют они приложениями в контейнерах, как древние витязи — дружиной своей.

Хоть и славно наше царство, а и в нем есть работа рутинная, небогатырская. Вот и решился я выковать себе меч-кладенец, приложение на языке Python, что само будет рутину рубить.

Для сведущего в разработке богатыря — ерундовое дело. А для меня настоящее сражение с чудом-юдом невиданным. И не выйти мне победителем, кабы не ИИ, который на каждом шаге делал за меня самую важную работу.

Но и с ним всё было не так гладко. Расскажу, с какими сложностями столкнулся я на своём пути и какие шишки набил.

Надеюсь, опыт мой будет полезен как минимум моим коллегам-техническим писателям. У нас часто встречаются рутинные задачи, на которые жалко тратить рабочее время — и хочется сковать себе меч-кладенец, да непонятно, с чего начать.

О техстеке-супостате

Продукт наш — могучее царство Kubernetes с операторами для управления контейнерными приложениями. Царство это не простое, а семикомпонентное. И в каждом компоненте, помимо своей дружины разработческой, туча союзников опенсорсных.

Каждый релиз нужно нам готовить технические описания этих союзников и отдавать юристам для проверки лицензий. Вот как это выглядит (зелёным выделен инструмент, версия которого не менялась).

e412b461f920b35439837b8014e438a0.png

Проблем в подготовке такого техстека, казалось бы, нет. Сиди, сравнивай цифры, значения версий да крась ячейки в Excel. Но не скоро сказка сказывается:

  • Ручной процесс сравнения занимает около 8-9 часов тщательной проверки и переписывания для каждого релиза. С развитием продукта и ростом количества инструментов этот процесс будет только удлиняться.
  • Окончательные версии инструментов каждого релиза собраны в релизном конфиге в формате.yaml — это сотни строк кода, которые нужно внимательно перечитать.

Решил я тогда, что вместо палицы ручной работы нужен нам меч‑кладенец автоматизации. Заручился помощью ИИ и отправился на ратный подвиг — ковать приложение, которое будет само собирать данные о техстеке и отправлять в Excel-таблицы.

Для начала нарисовал себе карту путевую, по которой буду двигаться к приложению, и наметил такие шаги:

  • подключиться к BitBucket;
  • преобразовать .yaml в .xlsx;
  • настроить Excel;
  • разработать графический интерфейс.

Призвал на помощь помощника искусственного и бросился ковать код. Хотел похвастать удалью молодецкой, да не учёл, что без опыта работы с ИИ не получится ни буквы кода выковать.

Так и вышло — первые же попытки сломались о суровый камень ошибок синтаксиса.

Первые попытки кода и нерабочие промпты

167de04a44b740e0ad79513e6ec5641d.png
21a7ea41a5e7ef56227e626b99d62c65.png
0e292693836f1831009d47d6baa88c19.png

Понял я, что нужна техника. Не случайно мудрые люди советуют оттачивать навык общаться с ИИ-помощником с помощью точных и метких промптов.

Так как я работал с GigaChat, разобраться с промптингом мне помог открытый курс в СберУниверситете. Вот ссылки, которые пригодились — возможно, найдёте их полезными:

И вот, с отточенным резцом промптинга в руках, я был готов выковать свой первый настоящий клинок. Первым делом предстояло проложить путь к самой сокровищнице — к Bitbucket.

Прокладываю путь к Bitbucket

Но и тут меня поджидала не одна засада. То не хватало какой-то библиотеки, то мой промпт оказывался недостаточно острым, и ИИ понимал меня криво. А то и я его ответа понять не мог.

Я наносил удары наугад: «как подключиться к Bitbucket», «как получить YAML-файл». Запускал сырой, неработающий код, получал в ответ ошибку, а затем просил ИИ исправить оплошность. То просил заменить авторизацию по паролю на HTTP-токен, то молил вывести на экран хоть что-то, кроме гневных ругательств компилятора.

Но с каждой ошибкой я учился формулировать запросы точнее: писал для себя понятные комментарии в коде и просил их реализовать: «Сделай вот так, как я написал».

Наконец — первый чистый удар! Выковал я такой код, он первый сработал и добыл информацию из .yaml-файла в Bitbucket и вывел её на экран.

python
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:).

Долго бился я впустую, пока меня не осенило: я атаковал список так, будто это был словарь. Я не понимал, что сначала этот хаотичный список нужно переплавить в упорядоченный словарь, и только потом выковывать из него нужные пары «ключ-значение».

Наконец вывел меня ИИ на дорожку прямоезжую! На выходе я получил не просто файл, а тот самый идеально выкованный клинок данных: Excel-таблицу, где в одной колонке красовались имена инструментов, а в другой — их версии.

4f5d932cda506e6c299eb75bb7234ca6.png

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

fdb3747f0ea663f9bca4442de3a1adae.png

Добавляю три точеных лезвия для подготовки данных

Мой скрипт уже добывал данные, но этого было мало. Чтобы по-настоящему автоматизировать процесс, ему предстояло научиться сравнивать, оформлять и готовить данные к бою — то есть, к отправке юристам.

Я решил выковать для своего цифрового орудия три новых лезвия.

1. Лезвие первое — для сравнения версий

Продукт наш, как я уже говорил, состоит из семи компонентов. И для каждого релиза нужно было готовить свой отчёт. Рубя вручную, я рисковал пропустить важное изменение.

Мой промпт‑приказ был таков — добавить в выходной Excel‑файл третий столбец, в который будет добавляться значение ключа upstream_tag: из соседнего файла с другой версией.

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

a46e48f68ee7a638fe1f8e5308f4a371.png

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

e6832c3a65e02c310377a2ca18b31e9d.png
614c6de330237eef36215bdea91cba91.png

2. Лезвие второе — для остроты взгляда

Юристам важно видеть, что изменилось. Выискивать каждое изменение вручную — всё равно что искать иголку в стоге сена при тусклом свете.

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

b7e83f63ba3ad2cc8de70e2cec7a39a5.png

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

13815d4a36969c49c1a784ba3d321372.png

3. Лезвие третье — для финальной закалки

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

Финальный точный удар: «Создай в таблице новый столбец. В него нужно объединить данные из первого и второго столбца, поставив между ними пробел».

c767c72d0764a12a1f9cabcd6357bf39.png

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

76bbd8993f6a2c39862731e608e857bd.png

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

4f816e2dc90294eccbfb459b8258556a.png
7e2e340ba86f4efc38da2e0d937aa8b7.png

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

  1. Реши базовую задачу (добыча данных).
  2. Добавь один новый кейс (работа с двумя версиями).
  3. Наложи на результат слой анализа (сравнение и цветовое выделение).
  4. Доведи вывод до нужного формата (финальное форматирование).

Мой цифровой меч превратился в настоящее богатырское оружие — он сам:

  1. Добывал сырьё (данные из Bitbucket).
  2. Переплавлял его (конвертировал YAML в структуры Python).
  3. Ковал и сравнивал (создавал и анализировал таблицы).
  4. Приводил в боевой вид (форматировал и готовил к использованию).

Однако не к лицу витязю довольствоваться малым. Решил я выковать своему помощнику рукоять удобную да баскую — графический интерфейс, другими словами.

Выковываю богатырский GUI

Требования к рукояти сначала были простыми и функциональными:

  • поля ввода для актуальной и предыдущей версий;
  • возможность менять надписи перед полями;
  • кнопка запуска.

Можно было остановиться и на этом, но не радовалось сердце простому серому окошку. Пожелал я, чтобы интерфейс стал ещё и удобным, понятным и чуть более богатырским.

Обратился снова к ИИ: попросил сделать так, чтобы менялся цвет у поля для ввода имени выходного файла при расхождении с объясняющей фразой. Да добавить красоты и яркости в виде фона былинного.

a16ef5fe779b8e71bc459d63285aa563.png
b97e4a0e4c75015b7c60e0b8062a760f.png

Конец — делу венец

Слово числам — вот трофей долгожданный, добытый трудом праведным: подготовка техстека сократилась с 8-9 часов скрупулёзного труда до 30 минут под руководством автоматизированного помощника. Вся работа по его созданию заняла у меня около 9-10 часов. Это чуть больше, чем один ручной цикл подготовки отчёта. Но это были инвестиции, которые окупились многократно уже на втором релизе.

Главное — приобрёл я базовые знания, с которыми уже не страшно подойти к новой задаче автоматизации.

А тем, кто только задумывается о своём первом скрипте, я оставлю три самых ценных совета, выкованных в этой битве:

  1. Написать рабочий код вполне реально, даже если вы не знаете ни аза на Python.
  2. Поизучайте базовые принципы промптинга, а потом, в процессе, много спрашивайте ИИ.
  3. Если что-то не работает — просите ИИ показать ошибку, делайте шаг назад и исправляйте.  

код скрипта:

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