#!/usr/bin/env python3 """Быстрый осмотр ОСВ-файлов из 1С: шапка, схема группировок, колонки сумм, уровни строк. Назначение — исследовательский инструмент для Среза 2 (парсер ОСВ), а не парсер. Запуск: python scripts/osv_inspect.py tests/fixtures/osv --rows 15 Зависимости: openpyxl, xlrd (pip install openpyxl xlrd). """ from __future__ import annotations import argparse import re import sys from pathlib import Path import openpyxl import xlrd TITLE_RE = re.compile(r"по счету\s+([\d.]+)\s*(?:за\s+(.+?))?\s*$") def load_rows(path: Path): """Возвращает список (level, [cells...]) и имя листа. xlsx — openpyxl (не read_only!), xls — xlrd.""" if path.suffix.lower() == ".xlsx": wb = openpyxl.load_workbook(path, data_only=True) ws = wb.worksheets[0] out = [] for r in range(1, ws.max_row + 1): lvl = ws.row_dimensions[r].outline_level or 0 out.append((lvl, [ws.cell(r, c).value for c in range(1, ws.max_column + 1)])) return ws.title, out wb = xlrd.open_workbook(path, formatting_info=True) ws = wb.sheets()[0] out = [] for r in range(ws.nrows): lvl = ws.rowinfo_map[r].outline_level if r in ws.rowinfo_map else 0 out.append((lvl, [c.value if c.value != "" else None for c in ws.row(r)])) return ws.name, out def col_letter(i: int) -> str: return openpyxl.utils.get_column_letter(i + 1) def inspect(path: Path, max_rows: int) -> None: sheet, rows = load_rows(path) print("=" * 110) print(path.name, f"[{sheet}]", f"rows={len(rows)}") first = [str(r[1][0]).strip() for r in rows[:3] if r[1] and r[1][0]] org = first[0] if first else "?" title = next((str(c) for _, cells in rows[:5] for c in cells if c and "по счету" in str(c)), "") m = TITLE_RE.search(title) acc, per = (m.group(1), m.group(2)) if m else ("?", "?") data_line = next((str(c) for _, cells in rows[:8] for c in cells if c and "Выводимые данные" in str(c)), "") print(f" org={org!r} account={acc} period={per!r} {data_line}") # заголовок таблицы: строка с «Счет» в первой колонке hdr_idx = next((i for i, (_, cells) in enumerate(rows) if cells and str(cells[0]).strip() in ("Счет", "Субконто")), None) if hdr_idx is None: print(" !! заголовок таблицы не найден") return groups = [] inn_col = None sums = {} i = hdr_idx while i < len(rows): _, cells = rows[i] c0 = str(cells[0]).strip() if cells and cells[0] else "" if i > hdr_idx and (not c0 or re.match(r"^\d", c0)): break if c0: groups.append(c0) for j, v in enumerate(cells): if v is None: continue s = str(v).strip() if s == "ИНН": inn_col = col_letter(j) if s in ("Дебет", "Кредит"): sums.setdefault(s, []).append(col_letter(j)) i += 1 data_start = i print(f" group rows (шапка): {groups} inn_col={inn_col} Дебет={sums.get('Дебет')} Кредит={sums.get('Кредит')}") print(f" table starts at row {data_start + 1}") for lvl, cells in rows[data_start : data_start + max_rows]: vals = [f"{col_letter(j)}={str(v)[:38]!r}" for j, v in enumerate(cells) if v is not None] print(f" L{lvl} | " + " ".join(vals)[:170]) last = next(((lvl, cells) for lvl, cells in reversed(rows) if cells and cells[0]), None) if last: print(f" last: L{last[0]} {str(last[1][0])!r} {[v for v in last[1][1:] if v is not None][:6]}") def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("paths", nargs="+") ap.add_argument("--rows", type=int, default=12) a = ap.parse_args() files: list[Path] = [] for p in a.paths: p = Path(p) files += sorted(p.rglob("*.xls*")) if p.is_dir() else [p] for f in files: try: inspect(f, a.rows) except Exception as e: # noqa: BLE001 — исследовательский скрипт print(f"!! {f}: {e}") return 0 if __name__ == "__main__": sys.exit(main())