Авторский курс Елизаровой НаталииAuthor's course by Nataliia Elizarova
Модуль 5 · Урок 05-20 · Новый урокModule 5 · Lesson 05-20 · New lesson

Подключаем свои файлы: выгрузки, справочники, проверкиConnecting your files: exports, master data, checks

Красивый дашборд с неверными цифрами хуже, чем таблица в Excel: ему верят. В уроках 05-18 и 05-19 вы собрали дашборд на учебных данных. Теперь подключаем свои: выгрузки продаж из учётной системы, справочник кодов, города. Разберём, как Python читает файлы, где прячутся ловушки и какие проверки ставить после каждого шага, чтобы в дашборд попадали только проверенные цифры. В конце — как подключить файл прямо из браузера.

A good-looking dashboard with wrong numbers is worse than an Excel table: people trust it. In lessons 05-18 and 05-19 you built a dashboard on training data. Now we connect your own: sales exports from the accounting system, the code list, cities. We look at how Python reads files, where the traps hide and which checks to put after every step so that only checked numbers reach the dashboard. At the end — how to connect a file right from the browser.

🆕 Новый урок🆕 New lesson ⏱ 45 минут⏱ 45 minutes 🎮 Тренажёр «Конвейер подключения»🎮 Simulator: "Connection conveyor" 📄 Скрипт загрузки и книга Excel📄 Loader script and Excel workbook ✅ Тест из 8 вопросов✅ An 8-question quiz

После урока вы сможетеAfter this lesson you will be able to

  • разложить выгрузки по папке так, чтобы скрипт брал ровно один файл на закрытый месяц;
  • прочитать Excel и CSV в Python и найти колонки по названию, а не по номеру;
  • вычистить пять тихих ловушек: число текстом, неразрывный пробел, латинскую букву в русском слове, «nan», код без пары;
  • поставить шесть контрольных точек и не пускать данные дальше, пока стоит «СТОП»;
  • написать пути так, чтобы скрипт работал на любом ноутбуке;
  • подключить файл прямо в дашборде — выбором файла или через File System Access.
  • lay out exports in a folder so the script takes exactly one file per closed month;
  • read Excel and CSV in Python and find columns by name, not by number;
  • clean five silent traps: a number stored as text, a non-breaking space, a Latin letter in a Cyrillic word, "nan", a code with no match;
  • set six checkpoints and stop the data while there is a "STOP";
  • write paths so the script runs on any laptop;
  • connect a file right in the dashboard — by picking a file or via File System Access.
1

Пять слов урокаFive words of the lesson

ВыгрузкаExportфайл, который учётная или BI-система отдала по запросу: продажи, остатки, заказы за периодa file the accounting or BI system produced on request: sales, stock, orders for a period
СправочникMaster dataтаблица соответствий: код → канонический код, город → территория, упаковка → литры (урок 02-06)a mapping table: code → canonical code, city → territory, pack → litres (lesson 02-06)
СклейкаJoinсоединение строк выгрузки со справочником по ключу; строки без пары при склейке пропадают молчаmatching export rows to master data by a key; rows without a match disappear silently
Контрольная точкаCheckpointпроверка после шага; если она не пройдена — «СТОП», данные дальше не идутa check after a step; if it fails — "STOP", the data goes no further
data.jsonготовые проверенные цифры, из которых сборщик делает HTML-дашборд (урок 05-19)the ready, checked numbers the builder turns into the HTML dashboard (lesson 05-19)
2

На пальцах: приёмка на складеIn plain words: goods receiving at a warehouse

Кладовщик не ставит товар на полку, пока не сверил его с накладной: сколько коробок, те ли коды, не битая ли упаковка. Если не сходится — коробки стоят в зоне приёмки, а не на полке. Данные для дашборда принимают так же: выгрузку сначала сверяют, и только потом она «встаёт на полку» — в data.json.

A storekeeper does not put goods on the shelf until they are checked against the delivery note: how many cases, the right codes, no damaged packs. If it does not match — the cases stay in the receiving area, not on the shelf. Data for a dashboard is received the same way: an export is checked first and only then "goes on the shelf" — into data.json.

Формула урокаThe lesson formula В дашборд попадает только то, что прошло проверку. Проверка — после каждого шага, а не одна в конце. Ошибка, найденная на шаге «чтение», стоит минуту. Та же ошибка, найденная на встрече S&OP, стоит доверия к дашборду. Only what passed a check reaches the dashboard. A check after every step, not one at the end. An error found at the "read" step costs a minute. The same error found at the S&OP meeting costs trust in the dashboard.
3

Путь данных: от выгрузки до дашбордаThe data path: from export to dashboard

ШагStepЧто делаетWhat it doesКонтрольная точкаCheckpoint
1Найти папку и файлыFind the folder and filesпапка есть; один файл на месяц; месяц закрытfolder exists; one file per month; month closed
2Прочитать строкиRead the rowsстрок столько, сколько ждали; колонки найдены по названиюas many rows as expected; columns found by name
3Почистить текст и числаClean text and numbersчисел текстом и «nan» — ноль или каждое записано в отчётnumbers as text and "nan" — zero, or each one logged
4Склеить со справочникамиJoin with master dataкодов и городов без пары — нольcodes and cities without a match — zero
5Сложить и сверитьSum and reconcileсумма до склейки = сумме послеtotal before join = total after
6Записать data.jsonWrite data.jsonфайл правда записан; в нём список прочитанных файловthe file is really written; it lists the files that were read

Дальше data.json забирает сборщик из урока 05-19, а обновление и публикацию разбираем в уроке 05-21.

Then the builder from lesson 05-19 takes data.json; refreshing and publishing are in lesson 05-21.

4

Папка выгрузок: четыре правилаThe exports folder: four rules

Один месяц — один файлOne month — one fileимя с месяцем: Sales_2026-09.xlsx. Файл «Sales_2026-09 (1).xlsx» — это задвоение, его видно по имениa name with the month: Sales_2026-09.xlsx. A file "Sales_2026-09 (1).xlsx" is a duplicate, visible by its name
Только закрытые месяцыClosed months onlyоткрытый месяц лежит в папке, но в дашборд не идёт — иначе «провал» в последнем месяцеthe open month may sit in the folder but does not go into the dashboard — otherwise a "drop" in the last month
Исходники не трогаемDo not touch the source filesвыгрузки и файлы компании — только для чтения; чистка — в скрипте, результат — новым файломexports and company files are read-only; cleaning happens in the script, the result is a new file
Закрытое не перекачиваемDo not re-download what is closedBI-система медленная: закрытые месяцы лежат в кэше, обновляется только текущийthe BI system is slow: closed months stay in a cache, only the current one is refreshed
ПримерExample В папке пять файлов: июль, август, сентябрь, ещё раз сентябрь («(1)») и октябрь по 19-е число. Правильно брать три: июль, август и один сентябрь. Если взять всё — сентябрь посчитается дважды, а октябрь покажет «падение» на две трети. The folder has five files: July, August, September, September again ("(1)") and October up to the 19th. The right set is three: July, August and one September. Take everything — September is counted twice and October shows a two-thirds "drop".
5

Чтение файлов в PythonReading files in Python

ФайлFileЧем читатьWhat reads itНа что смотретьWhat to watch
.xlsxopenpyxl, pandasформулы без значений: книга, сохранённая не в Excel, может не хранить результаты формулformulas without values: a workbook not saved by Excel may not store formula results
.xlsbpandas + pyxlsbотдельная библиотека; даты приходят числамиa separate library; dates arrive as numbers
.csvcsv, pandasкодировка (utf-8-sig или cp1251) и разделитель «;»encoding (utf-8-sig or cp1251) and the ";" separator
# Колонки — по названию, а не по номеру / columns by name, not by number
ws = load_workbook(path, read_only=True, data_only=True).active
rows = list(ws.iter_rows(values_only=True))
head = [str(h).strip().lower() for h in rows[0]]
i_qty = head.index("количество")   # упадёт с понятной ошибкой, если колонку переименовали

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

Why by name: tomorrow a "Channel" column is added to the export and "Quantity" moves from third place to fourth. A script using the column number silently sums the wrong thing. A script using the name either finds the right column or fails with a clear error.

6

Пять тихих ловушек и как их чиститьFive silent traps and how to clean them

ЛовушкаTrapКак выглядитWhat it looks likeЧто происходитWhat happensЧисткаCleaning
Число текстомNumber as text«650», «1 200», «1200,5»СУММ его не видит: август 4 000 → 3 350SUM ignores it: August 4,000 → 3,350убрать пробелы, запятую → точку, перевести в числоremove spaces, comma → dot, convert to a number
Неразрывный пробелNon-breaking space«Озёрск »город не находит пару в справочникеthe city finds no match in master dataзаменить символ 160 обычным пробелом и обрезать краяreplace character 160 with a normal space and trim
Буква-двойникLook-alike letter«Горный» с латинской «o»a Cyrillic city name with a Latin "o"на экране одинаково, для компьютера — другой городidentical on screen, a different city to a computerв русских словах менять латинских двойников на кириллицуin Cyrillic words replace Latin look-alikes with Cyrillic
«nan»"nan"пустая ячейка в текстовой колонкеan empty cell in a text columnpandas читает её как текст «nan», и правило «если пусто» не срабатываетpandas reads it as the text "nan", and the "if empty" rule does not fireсчитать «nan», «None», «-» пустымиtreat "nan", "None", "-" as empty
Код без парыCode without a match100199новый код, которого нет в справочнике: 600 бутылок пропадают при склейкеa new code missing in master data: 600 bottles vanish in the joinне чистить, а остановиться и дописать справочникdo not clean — stop and add it to master data
# Excel (RU): ПОДСТАВИТЬ(СЖПРОБЕЛЫ(A2);СИМВОЛ(160);"") · ЗНАЧЕН(B2) # Excel (EN): SUBSTITUTE(TRIM(A2),CHAR(160),"") · VALUE(B2) # СЖПРОБЕЛЫ / TRIM символ 160 НЕ убирает — только обычные пробелы
s = str(v).replace("\u00a0", " ").strip()
Чистить или останавливатьсяClean or stop Формат чистим молча, но пишем в отчёт: пробелы, двойники, числа текстом. Смысл не угадываем никогда: новый код, неизвестный город, «не число» — это «СТОП», пока человек не решит, что с ними делать. Format is cleaned silently but logged: spaces, look-alikes, numbers as text. Meaning is never guessed: a new code, an unknown city, "not a number" — that is a "STOP" until a person decides what to do with them.
7

Справочники: коды, города, единицыMaster data: codes, cities, units

🏷️

Код → канонический кодCode → canonical code

Старый и новый код одного товара сводятся к одному: 100103 → 100105. Таблицу только дописывают: строки не удаляют, иначе история старого кода выпадет.

The old and new code of one product map to one: 100103 → 100105. The table is only appended to: rows are not deleted, otherwise the old code history drops out.

📍

Город → территорияCity → territory

В выгрузке бывает город склада, а планируем по территории. Справочник говорит, какая строка куда.

An export may have the warehouse city while we plan by territory. Master data says which row goes where.

⚖️

ЕдиницыUnits

Бутылки, коробки, литры. Перевод — по справочнику упаковок, у чая — килограммы, а не литры (урок 02-06).

Bottles, cases, litres. Conversion by the pack master data; tea is in kilograms, not litres (lesson 02-06).

# code_map.csv (UTF-8, разделитель «;»)
code;canon
100101;100101
100103;100105
100105;100105
8

Шесть контрольных точекSix checkpoints

#ПроверкаCheckНормаNormЕсли нетIf not
1Папка найдена, файлы прочитаныFolder found, files readпрочитано столько месяцев, сколько закрытоas many months read as are closedСТОПSTOP
2Файлов за месяцFiles per month1СТОП: оставить одинSTOP: keep one
3Месяц закрытMonth closedмесяц ≤ последнего закрытогоmonth ≤ last closedпропустить и записатьskip and log
4ЧислаNumbers«не число» — 0"not a number" — 0СТОП; числа текстом — перевести и записатьSTOP; numbers as text — convert and log
5Без парыWithout a matchкодов и городов — 0codes and cities — 0СТОП: список и бутылкиSTOP: list and bottles
6Сумма до = послеTotal before = afterразница 0difference 0СТОПSTOP
Пример на цифрахA worked example Правда за июль–сентябрь — 12 000 бутылок. Если собрать «как есть», выйдет 14 950: из августа выпадут 650 (число текстом) и 700 (пробел в городе), из сентября — 600 (новый код), зато сентябрь войдёт дважды (+3 400) и октябрь добавит 1 500. Ошибки в разные стороны не гасятся — они прячутся в итоге. The truth for July–September is 12,000 bottles. Built "as is", you get 14,950: August loses 650 (number as text) and 700 (space in a city), September loses 600 (new code), but September comes in twice (+3,400) and October adds 1,500. Errors in opposite directions do not cancel — they hide in the total.
9

Тренажёр: конвейер подключенияSimulator: the connection conveyor

Включайте поломки — такие, какие бывают в жизни, — и переключайте конвейер: «без проверок» или «с контрольными точками». Смотрите, что попадёт в дашборд и какая проверка что поймала.

Switch on breakdowns — the kind that happen in real life — and switch the conveyor: "no checks" or "with checkpoints". See what reaches the dashboard and which check caught what.

10

Пути, которые работают на любом ноутбукеPaths that work on any laptop

Скрипт с путём C:\Users\Имя\OneDrive - Компания\… работает только у автора. У коллеги другое имя пользователя, а библиотека OneDrive может называться иначе. Правила простые:

A script with the path C:\Users\Name\OneDrive - Company\… works only for its author. A colleague has another user name, and the OneDrive library may have a different name. The rules are simple:

  1. всё, что рядом со скриптом, — путём от папки скрипта: Path(__file__).parent / "exports";
  2. общую папку ищите по содержимому, «по уровням»: поднимайтесь вверх, пока не найдёте папку, в которой лежат нужные подпапки;
  3. ни имени ноутбука, ни имени пользователя в скрипте.
  1. everything next to the script — as a path from the script folder: Path(__file__).parent / "exports";
  2. find the shared folder by its content, "by levels": walk up until you find the folder that holds the needed subfolders;
  3. no laptop name and no user name in the script.
# Общая папка — по содержимому, а не по имени / the shared folder by content, not by name
def find_root(markers, start=HERE):
    for p in [start] + list(start.parents):
        if all((p / m).exists() for m in markers):
            return p
    return None
11

Отчёт о чтении и «готово» только после записиA read report and "done" only after writing

После каждого запуска скрипт пишет check_report.txt: какие файлы прочитаны, сколько строк, что почищено, что остановило. В дашборде рядом с датой обновления показывайте список реально прочитанных файлов — тогда видно, что сентябрь пришёл, а октябрь ещё нет.

After every run the script writes check_report.txt: which files were read, how many rows, what was cleaned, what stopped it. In the dashboard, next to the refresh date, show the list of files actually read — then it is visible that September arrived and October has not yet.

«Готово» без файла"Done" without a file Скрипт печатает «ГОТОВО» только после того, как проверил: data.json записан сейчас, а не остался от прошлого раза. Если файл был открыт в Excel и не записался, «готово» — неправда. The script prints "DONE" only after checking that data.json was written now, not left from last time. If the file was open in Excel and was not written, "done" is a lie.
12

Файл прямо в дашборде: выбор файлаA file right in the dashboard: picking a file

Иногда Python не нужен: коллега сама выбирает свой файл в дашборде, и страница читает его в браузере библиотекой SheetJS. Файл никуда не отправляется — он читается на её компьютере. Попробуйте: выберите любую небольшую книгу .xlsx или .csv, например книгу практики этого урока.

Sometimes Python is not needed: a colleague picks her own file in the dashboard and the page reads it in the browser with the SheetJS library. The file is not sent anywhere — it is read on her computer. Try it: pick any small .xlsx or .csv workbook, for example this lesson's practice workbook.

// Чтение выбранного файла в браузере / reading a picked file in the browser
input.onchange = async () => {
  const wb = XLSX.read(await input.files[0].arrayBuffer());
  const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], {header: 1});
};
13

Читать и записывать настоящий файл: File System AccessReading and writing a real file: File System Access

Выбор файла даёт копию: дашборд может прочитать файл, но не может записать обратно. Для комментариев и правок, которые должны лечь в общий .xlsx, есть File System Access API: пользователь один раз «подключает» файл, и страница может перечитывать его и записывать изменения.

Picking a file gives a copy: the dashboard can read it but cannot write back. For comments and edits that must land in a shared .xlsx there is the File System Access API: the user "connects" a file once, and the page can re-read it and write changes.

ЧтоWhatКак естьThe facts
БраузерыBrowsersChrome и Edge (с версии 86); в Firefox и Safari нетChrome and Edge (since version 86); not in Firefox or Safari
УсловияConditionsтолько по действию пользователя (кнопка) и в безопасном контексте; браузер спрашивает разрешение на записьonly on a user action (a button) and in a secure context; the browser asks permission to write
БудущееFutureу одного из производителей браузеров отрицательная позиция по стандарту — общим для всех браузеров API вряд ли станетone browser vendor has a negative standards position — the API is unlikely to become available in every browser
// Подключить файл и записать изменения / connect a file and write changes
if ("showOpenFilePicker" in window) {
  const [h] = await showOpenFilePicker({types: [{accept: {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"]}}]});
  const wb = XLSX.read(await (await h.getFile()).arrayBuffer());
  // ... добавить комментарий в лист / add a comment to a sheet ...
  const w = await h.createWritable();
  await w.write(XLSX.write(wb, {type: "array", bookType: "xlsx"})); await w.close();
} else {
  // запасной путь: выбор файла + скачать новую версию / fallback: pick a file + download a new version
}
Запасной путь обязателенThe fallback is a must Всегда проверяйте, есть ли функция в браузере, и давайте запасной путь: выбрать файл, внести правки, скачать новую версию и положить её в папку. И помните: пока файл открыт в Excel у коллеги, записать его не получится — дашборд должен честно сказать «не записано», а не «готово». Always check whether the browser has the function and offer a fallback: pick the file, make edits, download a new version and put it in the folder. And remember: while a colleague has the file open in Excel, it cannot be written — the dashboard must honestly say "not written", not "done".
14

Скачайте шаблон загрузчикаDownload the loader template

Положите файлы в папку проекта из урока 05-19, рядом создайте папку exports и сложите туда свои выгрузки. Запуск: python load_exports.py. Скрипт читает только закрытые месяцы, чистит формат, останавливается на смысловых ошибках и пишет data.json и check_report.txt. Нужна только библиотека openpyxl.

Put the files into the project folder from lesson 05-19, create an exports folder next to them and put your exports there. Run: python load_exports.py. The script reads only closed months, cleans the format, stops on meaning errors and writes data.json and check_report.txt. It needs only the openpyxl library.

load_exports.pyзагрузчик с шестью контрольными точкамиthe loader with six checkpoints
code_map.csvпример справочника кодовa sample code list
city_map.csvпример справочника городовa sample city list
Показать текст load_exports.pyShow the text of load_exports.py
# -*- coding: utf-8 -*-
"""Загрузка ежемесячных выгрузок продаж в data.json для дашборда.
Load monthly sales exports into data.json for the dashboard.
Папка рядом со скриптом / folders next to the script:
  exports/      Sales_2026-07.xlsx, Sales_2026-08.xlsx ...  (колонки: Город | Код | Количество)
  code_map.csv  код в выгрузке;канонический код   (export code;canonical code)
  city_map.csv  город в выгрузке;город справочника (export city;master city)
Запуск / run:  python load_exports.py            -> data.json + check_report.txt
               python load_exports.py --force    -> записать, даже если есть СТОП (не советую)
"""
import datetime
import json
import re
import sys
from pathlib import Path
from openpyxl import load_workbook
HERE = Path(__file__).resolve().parent          # пути только от папки скрипта / paths only from the script folder
EXPORTS = HERE / "exports"
CLOSED = 202609                                  # последний закрытый месяц ГГГГММ / last closed month YYYYMM
NBSP = " "
# латинские буквы, похожие на русские / Latin letters that look like Cyrillic
TWINS = str.maketrans("ACEHKMOPTXaceopxy", "АСЕНКМОРТХасеорху")
HEAD = {"city": ("город", "city"), "code": ("код", "code"), "qty": ("количество", "qty", "quantity")}
def find_root(markers, start=HERE):
    """Ищет общую папку по уровням: поднимается вверх, пока не найдёт папку со всеми подпапками markers.
    Finds the shared folder by levels: walks up until a folder contains all marker subfolders.
    Имя библиотеки OneDrive у коллег разное, поэтому ищем по содержимому, а не по имени."""
    for p in [start] + list(start.parents):
        if all((p / m).exists() for m in markers):
            return p
    return None
def clean_text(s):
    """Убирает неразрывные пробелы и двойные пробелы; в русских словах меняет латинских двойников на кириллицу."""
    s = str(s).replace(NBSP, " ").strip()
    s = re.sub(r"\s+", " ", s)
    if re.search("[А-Яа-яЁё]", s):
        s = s.translate(TWINS)
    return s
def to_number(v):
    """Число или None. Понимает «1 200», «1200,5», «1200.5». Пусто, «nan», «-» — не число."""
    if isinstance(v, (int, float)):
        return float(v)
    if v is None:
        return None
    s = str(v).replace(NBSP, "").replace(" ", "").replace(",", ".")
    if s.lower() in ("", "nan", "none", "-"):
        return None
    try:
        return float(s)
    except ValueError:
        return None
def read_map(path):
    """Справочник «было;стало» из CSV (UTF-8). Ключ и значение чистятся так же, как данные."""
    out = {}
    for line in path.read_text(encoding="utf-8-sig").splitlines()[1:]:
        if ";" in line:
            a, b = line.split(";", 1)
            out[clean_text(a)] = clean_text(b)
    return out
def month_of(name):
    m = re.search(r"(\d{4})-(\d{2})", name)
    return int(m.group(1) + m.group(2)) if m else None
def pick_files(log):
    """Один месяц — один файл: при повторе берём самый новый по времени изменения и пишем об этом в отчёт."""
    by_month = {}
    for f in sorted(EXPORTS.glob("*.xlsx")):
        if f.name.startswith("~$"):
            continue                              # временный файл открытого Excel / temp file of an open Excel
        mo = month_of(f.name)
        if mo is None:
            log.append(f"ПРОПУСК {f.name}: нет месяца в имени")
            continue
        by_month.setdefault(mo, []).append(f)
    chosen, stops = {}, []
    for mo, files in sorted(by_month.items()):
        if mo > CLOSED:
            log.append(f"ПРОПУСК {mo}: месяц не закрыт ({', '.join(x.name for x in files)})")
            continue
        files.sort(key=lambda x: x.stat().st_mtime)
        if len(files) > 1:
            stops.append(f"СТОП: за {mo} файлов {len(files)}: {', '.join(x.name for x in files)} - оставьте один")
        chosen[mo] = files[-1]
    return chosen, stops
def read_export(path):
    ws = load_workbook(path, read_only=True, data_only=True).active
    rows = list(ws.iter_rows(values_only=True))
    head = [clean_text(h).lower() for h in rows[0]]
    idx = {k: next(i for i, h in enumerate(head) if h in names) for k, names in HEAD.items()}
    return [{k: r[i] for k, i in idx.items()} for r in rows[1:] if any(c is not None for c in r)]
def main():
    start = datetime.datetime.now().timestamp()
    log, stops = [], []
    if not EXPORTS.exists():
        print(f"СТОП: нет папки {EXPORTS}")
        return 1
    codes, cities = read_map(HERE / "code_map.csv"), read_map(HERE / "city_map.csv")
    chosen, st = pick_files(log)
    stops += st
    total_in, data, bad_num, no_code, no_city = 0.0, {}, [], [], []
    for mo, f in chosen.items():
        rows = read_export(f)
        log.append(f"ПРОЧИТАН {f.name}: строк {len(rows)}")
        for r in rows:
            q = to_number(r["qty"])
            if q is None:
                bad_num.append(f"{f.name}: {r}")
                continue
            if not isinstance(r["qty"], (int, float)):
                log.append(f"ЧИСЛО ТЕКСТОМ {f.name}: {r['qty']!r} -> {q:g}")
            total_in += q
            code, city = clean_text(r["code"]), clean_text(r["city"])
            code = codes.get(code)
            city = cities.get(city)
            if code is None:
                no_code.append((f.name, r["code"], q))
                continue
            if city is None:
                no_city.append((f.name, r["city"], q))
                continue
            key = f"{mo}|{city}|{code}"
            data[key] = data.get(key, 0) + q
    total_out = sum(data.values())
    if bad_num:
        stops.append(f"СТОП: не число в количестве, строк {len(bad_num)}")
    if no_code:
        stops.append(f"СТОП: кодов без пары {len(no_code)}, бутылок {sum(x[2] for x in no_code):g}: "
                     + ", ".join(str(x[1]) for x in no_code))
    if no_city:
        stops.append(f"СТОП: городов без пары {len(no_city)}: " + ", ".join(repr(x[1]) for x in no_city))
    if abs(total_in - total_out) > 1e-6 and not (no_code or no_city):
        stops.append(f"СТОП: сумма до склейки {total_in:g} не равна сумме после {total_out:g}")
    report = [f"Прочитано месяцев: {len(chosen)}", f"Сумма до склейки: {total_in:g}", f"Сумма после склейки: {total_out:g}"] + log + stops
    (HERE / "check_report.txt").write_text("\n".join(report), encoding="utf-8")
    for line in report:
        print(line)
    if stops and "--force" not in sys.argv:
        print("Данные НЕ записаны. Исправьте и запустите снова. Отчёт: check_report.txt")
        return 1
    out = HERE / "data.json"
    rows = [{"month": int(k.split("|")[0]), "city": k.split("|")[1], "code": k.split("|")[2], "qty": v} for k, v in sorted(data.items())]
    out.write_text(json.dumps({"rows": rows, "files": [f.name for f in chosen.values()], "closed": CLOSED},
                              ensure_ascii=False, indent=1), encoding="utf-8")
    if out.exists() and out.stat().st_mtime >= start - 1:   # «готово» — только если файл правда записан
        print(f"ГОТОВО: {out.name}, строк {len(rows)}, сумма {total_out:g}")
        return 0
    print("ОШИБКА: data.json не записан")
    return 1
if __name__ == "__main__":
    sys.exit(main())

Скрипт проверен на учебной папке: с ловушками он останавливается (задвоенный сентябрь, код 100199), после исправления пишет data.json на 12 000 бутылок.

The script was tested on the training folder: with traps it stops (duplicated September, code 100199); after fixing it writes data.json with 12,000 bottles.

15

Руками ↔ как называетсяBy hand ↔ what it is called

Как вы делаете руками в ExcelWhat you do by hand in ExcelКак это называетсяWhat it is called
копируете выгрузки в одну таблицуcopy exports into one tableизвлечение (Extract)Extract
ищете ВПР-ом код в справочникеlook up a code with VLOOKUPсклейка (join, merge)join, merge
чистите пробелы и числа-текстclean spaces and numbers as textпреобразование (Transform)Transform
сохраняете итоговую таблицу для дашбордаsave the final table for the dashboardзагрузка (Load) — вместе ETLLoad — together ETL
сверяете итог с отчётомreconcile the total with a reportконтроль качества данных (data validation)data validation
16

Визуальная проверка обязательнаVisual check is a must

Визуальная проверка обязательна. Контрольные точки ловят то, что вы заранее придумали. Новую беду ловит глаз: откройте дашборд после загрузки и посмотрите на график по месяцам. Ступенька, провал в последнем месяце, город с нулём — повод открыть check_report.txt раньше, чем показывать дашборд людям.

Visual check is a must. Checkpoints catch what you thought of in advance. A new problem is caught by the eye: open the dashboard after loading and look at the monthly chart. A step, a drop in the last month, a city with zero — a reason to open check_report.txt before showing the dashboard to people.

17

Что нового в 2025–2026What is new in 2025–2026

🗂️

File System Access: только ChromiumFile System Access: Chromium only

Каталог возможностей веб-платформы помечает File System Access как не базовую возможность: Chrome и Edge поддерживают, Firefox и Safari — нет, у одного производителя отрицательная позиция. Значит, в корпоративном Edge — пользуемся, но запасной путь держим всегда.

The web platform features catalogue marks File System Access as not Baseline: Chrome and Edge support it, Firefox and Safari do not, one vendor has a negative position. So in corporate Edge — use it, but always keep a fallback.

🤖

Загрузчик пишет ассистентAn assistant writes the loader

Скрипт вроде load_exports.py ассистент пишет за минуты (урок 02-08). Но контрольные точки придумываете вы: только вы знаете, что код 100103 — это 100105, а октябрь ещё не закрыт.

An assistant writes a script like load_exports.py in minutes (lesson 02-08). But you design the checkpoints: only you know that code 100103 is 100105 and that October is not closed yet.

Правило для любой новинкиThe rule for any novelty Новый способ подключения берём, только если на ваших данных он даёт те же цифры, что проверенный старый, — сверка итога «как год назад» по каждому месяцу. Adopt a new way of connecting only if on your data it gives the same numbers as the checked old one — reconcile the total "as last year" for every month.

Источники: web.dev, «How to open one or multiple files»; Web Platform features explorer, «File system access»; MDN, showOpenFilePicker().

Sources: web.dev, "How to open one or multiple files"; Web Platform features explorer, "File system access"; MDN, showOpenFilePicker().

18

Как это бывает в жизниHow it happens in real life

Три случая из работы планера в компании-производителе напитков. Названия изменены.

Three cases from planning work at a beverage manufacturer. Names are changed.

1. Месяц, который скачали дважды1. The month downloaded twice

Что случилосьWhat happened
В дашборде один месяц оказался почти вдвое выше соседних. Начали искать акцию.In the dashboard one month came out almost twice as high as its neighbours. People started looking for a promo.
Что на самом делеWhat was really going on
Выгрузку месяца скачали дважды, второй файл лёг рядом с «(1)» в имени. Скрипт сложил все файлы папки.The month export was downloaded twice; the second file landed next to it with "(1)" in the name. The script summed every file in the folder.
ПравилоRule
Проверка «один месяц — один файл» до чтения строк.A "one month — one file" check before reading rows.

2. Пятая часть объёма, которая пропала молча2. A fifth of the volume that vanished silently

Что случилосьWhat happened
Прогноз склеили со справочником городов по коду. Ошибок не было, итог выглядел правдоподобно.The forecast was joined to the city list by code. No errors, the total looked plausible.
Что на самом делеWhat was really going on
Часть городов в справочнике записана другими кодами, и около пятой части объёма выпала при склейке.Some cities in the list used other codes, and about a fifth of the volume dropped out in the join.
ПравилоRule
Сумма до склейки = сумме после; список строк без пары — в отчёт.Total before the join = total after; the list of unmatched rows goes into the report.

3. Пустая ячейка, которая оказалась словом3. An empty cell that turned out to be a word

Что случилосьWhat happened
В листе настроек у одной страны не заполнили цель. Скрипт должен был взять цель по умолчанию — но не взял, и страна осталась без цели.On the settings sheet one country had no target filled in. The script should have taken the default target — it did not, and the country was left without a target.
Что на самом делеWhat was really going on
Пустая ячейка в текстовой колонке прочиталась как слово «nan». Для правила «если пусто» это не пусто.The empty cell in a text column was read as the word "nan". For an "if empty" rule that is not empty.
ПравилоRule
Считайте «nan», «None» и «-» пустыми — явно, в одной функции.Treat "nan", "None" and "-" as empty — explicitly, in one function.
19

Практика в ExcelExcel practice

В книге та же папка, что в тренажёре: лист «файлы», справочник, три выгрузки и лист «контроль» с проверками формулами. Статус «СТОП» значит: в дашборд не пускаем.

The workbook has the same folder as the simulator: the "files" sheet, master data, three exports and a "control" sheet with checks by formulas. Status "STOP" means: not into the dashboard.

ЗаданияTasks

  1. Сколько строк контроля в статусе «СТОП» до исправлений? Какие?
  2. Найдите число текстом в августе и исправьте его. Что изменилось в контроле?
  3. Найдите город с неразрывным пробелом и исправьте. Сколько теперь городов без пары?
  4. Код 100199 — новый. Что правильно: удалить строку или дописать справочник? Дополните справочник (канонический код 100199) и посмотрите на контроль.
  5. Какой файл сентября брать и что сделать со вторым? Что делать с октябрём?
  1. How many control rows are in "STOP" before fixing? Which ones?
  2. Find the number stored as text in August and fix it. What changed in the control sheet?
  3. Find the city with a non-breaking space and fix it. How many cities without a match now?
  4. Code 100199 is new. What is right: delete the row or add it to master data? Add it (canonical code 100199) and look at the control sheet.
  5. Which September file to take and what to do with the second? What to do with October?
ОтветыAnswers

1. Девять «СТОП» из одиннадцати строк: всё, кроме первой строки и строки «правда». СЧЁТ видит 7 чисел из 8 строк; число текстом 1; город без пары 1; код без пары 1 (600 бутылок); файлов за сентябрь 2; незакрытых файлов 1; сумма трёх выгрузок 11 350 вместо 12 000; «как есть» 14 950.
2. В августе в первой строке «650» записано текстом. Умножьте на 1 или перепишите числом: СЧЁТ станет 8, чисел текстом 0, сумма выгрузок 12 000.
3. Пятая строка августа — «Озёрск» с пробелом в конце. После замены городов без пары 0.
4. Дописать справочник: строка — настоящие продажи, удалять нельзя. Кодов без пары 0, бутылок без пары 0.
5. Брать один файл — самый свежий, второй убрать из папки (или в архив). Октябрь не брать, пока месяц не закрыт: лист «параметры», последний закрытый месяц 202609. Строки «файлов за сентябрь» и «незакрытых» исправляются в папке (в книге — удалите строки второго сентября и октября на листе «файлы»), а не формулами. Последняя строка «как есть» остаётся «СТОП» всегда — это память о том, что было бы без проверок.

1. Nine "STOP" out of eleven rows: all but the first row and the "truth" row. COUNT sees 7 numbers out of 8 rows; numbers as text 1; city without a match 1; code without a match 1 (600 bottles); files for September 2; open-month files 1; sum of the three exports 11,350 instead of 12,000; "as is" 14,950.
2. In August the first row has "650" stored as text. Multiply by 1 or retype as a number: COUNT becomes 8, numbers as text 0, sum of exports 12,000.
3. The fifth row of August is "Ozyorsk" with a trailing space. After replacing, cities without a match 0.
4. Add it to master data: the row is real sales, it must not be deleted. Codes without a match 0, bottles 0.
5. Take one file — the newest; remove the second from the folder (or archive it). Do not take October until the month is closed: sheet "parameters", last closed month 202609. The "files for September" and "open months" rows are fixed in the folder (in the workbook — delete the second September and the October rows on the "files" sheet), not with formulas. The last "as is" row always stays "STOP" — a reminder of what would happen without checks.

20

Частые вопросыFrequently asked questions

Кто что делает: я, Claude, Python?Who does what: me, Claude, Python?

Claude пишет скрипт по вашему заданию. Python читает файлы и считает. Вы проверяете: сверяете итог с отчётом, которому доверяете, и решаете, что делать с каждым «СТОП».

Claude writes the script from your brief. Python reads the files and calculates. You check: reconcile the total with a report you trust, and decide what to do with every "STOP".

Почему не Power Query?Why not Power Query?

Можно и Power Query — «из папки» он собирает все файлы (урок 05-13). Правила те же: один месяц — один файл, проверки после каждого шага. Python удобнее, когда шагов много и нужен отчёт о проверках.

Power Query works too — "from folder" combines all files (lesson 05-13). The rules are the same: one month — one file, checks after every step. Python is handier when there are many steps and a check report is needed.

Можно автоматически «угадывать» новый код по названию?Can a new code be "guessed" by name automatically?

Подсказать — можно, записать без человека — нет. Апсайз, новинка и смена кода выглядят похоже, а решения разные (урок 02-06).

Suggest — yes, write without a person — no. An upsize, a launch and a code change look alike, but the decisions differ (lesson 02-06).

Файл открыт у коллеги — что будет?A colleague has the file open — what happens?

Прочитать обычно можно, записать — нет. Скрипт должен сказать «не записано», а не «готово».

Reading usually works, writing does not. The script must say "not written", not "done".

Где хранить справочники?Where to keep the master data?

Рядом со скриптами в общей папке — тогда дашборд собирается с любого ноутбука, и у всех один справочник.

Next to the scripts in the shared folder — then the dashboard builds from any laptop and everybody has one master data.

21

Упражнение: какая проверка поймает?Exercise: which check catches it?

22

Три мифаThree myths

Миф:Myth: скрипт не упал — значит, данные верные.the script did not crash — so the data is right.
На самом деле:In fact: самые дорогие ошибки молчат: строки без пары, задвоенный месяц, число текстом.the most expensive errors are silent: unmatched rows, a duplicated month, a number as text.
Миф:Myth: одной проверки в конце хватит.one check at the end is enough.
На самом деле:In fact: ошибки в разные стороны гасятся в итоге: 12 000 могут сойтись случайно. Проверка — после каждого шага.errors in opposite directions cancel in the total: 12,000 may match by chance. A check after every step.
Миф:Myth: путь к папке можно вписать один раз.the folder path can be written in once.
На самом деле:In fact: у коллеги другой пользователь и другое имя библиотеки. Пути — от папки скрипта или поиском по содержимому.a colleague has another user and another library name. Paths — from the script folder or found by content.
23

Внедрение и что запомнить: отметьте галочкойRollout and what to remember: tick it off

24

Проверьте себяCheck yourself

25

Шпаргалка терминовGlossary

ETLизвлечь, преобразовать, загрузить: путь данных от выгрузки до дашбордаextract, transform, load: the data path from export to dashboard
Canonical codeканонический код: один код на товар, к которому сводятся старыеone code per product that old codes map to
NBSP (CHAR 160)неразрывный пробел: на экране как пробел, СЖПРОБЕЛЫ его не убираетnon-breaking space: looks like a space, TRIM does not remove it
Homoglyphбуква-двойник: латинская «o» в русском словеa look-alike letter: a Latin "o" in a Cyrillic word
Checkpointконтрольная точка: проверка после шага, «СТОП» при ошибкеa check after a step, "STOP" on error
SheetJSбиблиотека, которая читает и пишет Excel прямо в браузереa library that reads and writes Excel right in the browser
File System Access APIдоступ страницы к настоящему файлу на диске с разрешения пользователя (Chrome, Edge)a page's access to a real file on disk with the user's permission (Chrome, Edge)
data.jsonпроверенные цифры для сборки дашбордаchecked numbers for building the dashboard
Итог урокаLesson summary Свои данные подключаются как приёмка на складе: сначала сверка, потом полка. Один месяц — один файл, только закрытые месяцы, исходники не трогаем. Колонки — по названию. Формат чистим и записываем в отчёт, смысл не угадываем: новый код и неизвестный город — «СТОП». Шесть контрольных точек после каждого шага, «готово» — только после записи, пути — от папки скрипта. В браузере файл можно выбрать (SheetJS) или подключить для записи (File System Access, Chrome и Edge) — с запасным путём. Дальше — обновление, правки и публикация, урок 05-21. Your data is connected like goods receiving: check first, then the shelf. One month — one file, closed months only, source files untouched. Columns by name. Format is cleaned and logged, meaning is never guessed: a new code and an unknown city are a "STOP". Six checkpoints after every step, "done" only after writing, paths from the script folder. In the browser a file can be picked (SheetJS) or connected for writing (File System Access, Chrome and Edge) — with a fallback. Next — refreshing, edits and publishing, lesson 05-21.
Made on
Tilda