grid.py: Cell, MergedRange, Sheet (1-based, только непустые ячейки, ленивые индексы строк и merged; диапазоны больше 10 000 ячеек проверяются перебором), column_letter для диагностики. errors.py: ParserError и коды контракта. readers: read_book определяет формат по сигнатуре (zip → XLSX/XLSB по xl/workbook.bin, OLE2 → XLS), предупреждает о несовпадении с file_type; ошибки библиотек → UnreadableError одной строкой. Числа — Decimal(str(value)), bool отдельно, даты как datetime; xlsb без outline/merged с предупреждением. Тесты: синтетическая книга openpyxl, подмена file_type, мусор/обрезанные файлы, xlsb через подменённый pyxlsb; 66 реальных ОСВ читаются < 5 с с outline и merged; xls «62 за 3 квартал 2024»: строка 15 — outline 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
45 lines
1.5 KiB
Python
45 lines
1.5 KiB
Python
"""Чтение XLSX через openpyxl (полное чтение: нужны merged, outline level и шрифт)."""
|
|
|
|
import io
|
|
from typing import Any
|
|
|
|
import openpyxl
|
|
|
|
from src.parser.grid import Cell, MergedRange, Sheet
|
|
from src.parser.readers._common import make_cell
|
|
|
|
__all__ = ["read_xlsx"]
|
|
|
|
|
|
def read_xlsx(data: bytes) -> tuple[Sheet, ...]:
|
|
book = openpyxl.load_workbook(io.BytesIO(data), data_only=True, read_only=False)
|
|
try:
|
|
return tuple(_sheet(worksheet) for worksheet in book.worksheets)
|
|
finally:
|
|
book.close()
|
|
|
|
|
|
def _sheet(ws: Any) -> Sheet:
|
|
cells: dict[tuple[int, int], Cell] = {}
|
|
# `_cells` — разреженное хранилище openpyxl (версия закреплена): iter_rows создал бы пустые ячейки на весь
|
|
# прямоугольник листа
|
|
for source in ws._cells.values():
|
|
cell = make_cell(source.value, bold=bool(source.font.bold), indent=int(source.alignment.indent or 0))
|
|
if cell is not None:
|
|
cells[(source.row, source.column)] = cell
|
|
merged = tuple(
|
|
MergedRange(top=span.min_row, left=span.min_col, bottom=span.max_row, right=span.max_col)
|
|
for span in ws.merged_cells.ranges
|
|
)
|
|
row_outline = {
|
|
row: int(dimension.outline_level) for row, dimension in ws.row_dimensions.items() if dimension.outline_level
|
|
}
|
|
return Sheet(
|
|
name=str(ws.title),
|
|
cells=cells,
|
|
merged=merged,
|
|
row_outline=row_outline,
|
|
n_rows=int(ws.max_row),
|
|
n_cols=int(ws.max_column),
|
|
)
|