Скрытое в таблице не парсим

Правило: если строка или столбец в таблице скрыт, его данные не попадают в
расписание — раньше скрытые столбцы пропускались только при поиске групп.

- xlsx: у листа появился набор скрытых строк (Worksheet.hidden_rows);
- расписание, недели: скрытые строки пропускаются;
- аудитории и справочник ФИО: скрытые столбцы не читаются вовсе (убрана
  двухпроходная логика «сначала видимые, потом скрытые»);
- тесты: билдер умеет скрытые строки, добавлены проверки «скрытое не парсится».

Проверено на таблице: группы и всё расписание совпали байт в байт (скрытых
строк нет), из аудиторий ушли только те, что лежат в скрытых столбцах
(402, 405, 415), справочник ФИО стал короче на 2 скрытые записи, у 223
осталось описание из видимого столбца (20 мест, интерактивная панель).
Тесты: 171.
This commit is contained in:
2026-10-12 12:07:39 +03:00
parent 172636f227
commit d131adbcb8
4 changed files with 44 additions and 17 deletions

View File

@@ -83,7 +83,9 @@ def extract_week_ranges(sheets: dict[str, Worksheet]) -> list[dict]:
return [] return []
ranges: list[dict] = [] ranges: list[dict] = []
seen: set[tuple[str, date]] = set() seen: set[tuple[str, date]] = set()
for row in sheet.rows[1:]: for row_idx, row in enumerate(sheet.rows[1:], start=1):
if row_idx in sheet.hidden_rows:
continue # скрытые строки не парсим
if not any(row[i] for i in range(4)): if not any(row[i] for i in range(4)):
continue continue
for parity, start_col, end_col in (("ч", 0, 1), ("з", 2, 3)): for parity, start_col, end_col in (("ч", 0, 1), ("з", 2, 3)):
@@ -148,7 +150,9 @@ def parse_schedule(sheets: dict[str, Worksheet]) -> dict:
parity: str | None = None parity: str | None = None
rows_in_day = 0 rows_in_day = 0
for row in rows[1:]: for row_idx, row in enumerate(rows[1:], start=1):
if row_idx in sheet.hidden_rows:
continue # скрытые строки не парсим
if not any(row[i] for i in range(4)): if not any(row[i] for i in range(4)):
continue continue
day = _normalize_header(row[_COL_DAY]).lower() day = _normalize_header(row[_COL_DAY]).lower()
@@ -485,7 +489,7 @@ def parse_rooms_info(sheets: dict[str, Worksheet]) -> dict:
return return
label = f"ауд. {m.group(1)}" label = f"ауд. {m.group(1)}"
if label in info: if label in info:
return # видимый столбец важнее скрытого дубля с тем же номером return # дубль с тем же номером: первый столбец важнее
text = _strip_marks( text = _strip_marks(
" ".join(ln.strip() for ln in str(desc[col]).split("\n") if ln.strip()) " ".join(ln.strip() for ln in str(desc[col]).split("\n") if ln.strip())
) )
@@ -500,12 +504,8 @@ def parse_rooms_info(sheets: dict[str, Worksheet]) -> dict:
"available_days": room_weekdays(text), "available_days": room_weekdays(text),
} }
columns = range(_FIRST_GROUP_COL, max(len(header), len(desc))) for col in range(_FIRST_GROUP_COL, max(len(header), len(desc))):
for col in columns: # сначала видимые if col not in sheet.hidden_cols: # скрытое не парсим
if col not in sheet.hidden_cols:
add(col)
for col in columns: # потом скрытые: аудитория без видимого столбца тоже нужна
if col in sheet.hidden_cols:
add(col) add(col)
return info return info
@@ -541,6 +541,8 @@ def parse_teachers_full(sheets: dict[str, Worksheet]) -> dict:
header = sheet.rows[1] header = sheet.rows[1]
result: dict = {} result: dict = {}
for col in range(_FIRST_GROUP_COL, len(header)): for col in range(_FIRST_GROUP_COL, len(header)):
if col in sheet.hidden_cols: # скрытое не парсим
continue
value = _strip_marks(str(header[col])) value = _strip_marks(str(header[col]))
parts = [p.strip() for p in re.split(r"[\s\n]+", value) if p.strip()] parts = [p.strip() for p in re.split(r"[\s\n]+", value) if p.strip()]
if not parts: if not parts:

View File

@@ -14,7 +14,7 @@ import io
import re import re
import zipfile import zipfile
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from dataclasses import dataclass from dataclasses import dataclass, field
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from .errors import ScheduleError from .errors import ScheduleError
@@ -32,6 +32,7 @@ _REF_RE = re.compile(r"^([A-Z]+)(\d+)$")
class Worksheet: class Worksheet:
rows: list[list[str]] rows: list[list[str]]
hidden_cols: set[int] hidden_cols: set[int]
hidden_rows: set[int] = field(default_factory=set)
def cell(self, row_idx: int, col_idx: int) -> str: def cell(self, row_idx: int, col_idx: int) -> str:
"""Значение ячейки, "" вне границ матрицы.""" """Значение ячейки, "" вне границ матрицы."""
@@ -135,11 +136,14 @@ def _parse_sheet(xml_bytes: bytes, shared: list[str]) -> Worksheet:
if col.get("hidden") == "1": if col.get("hidden") == "1":
hidden_cols.update(range(c_min - 1, c_max)) hidden_cols.update(range(c_min - 1, c_max))
hidden_rows: set[int] = set()
for row in root.iter(f"{{{XLSX_NS}}}row"): for row in root.iter(f"{{{XLSX_NS}}}row"):
row_num = row.get("r") row_num = row.get("r")
if not row_num: if not row_num:
continue continue
r = int(row_num) - 1 r = int(row_num) - 1
if row.get("hidden") == "1":
hidden_rows.add(r)
for cell in row.iter(f"{{{XLSX_NS}}}c"): for cell in row.iter(f"{{{XLSX_NS}}}c"):
ref = cell.get("r") ref = cell.get("r")
if not ref: if not ref:
@@ -194,7 +198,7 @@ def _parse_sheet(xml_bytes: bytes, shared: list[str]) -> Worksheet:
[cells.get((r, c), "") for c in range(max_col + 1)] [cells.get((r, c), "") for c in range(max_col + 1)]
for r in range(max_row + 1) for r in range(max_row + 1)
] ]
return Worksheet(rows=rows, hidden_cols=hidden_cols) return Worksheet(rows=rows, hidden_cols=hidden_cols, hidden_rows=hidden_rows)
def _iter_text(node: ET.Element): def _iter_text(node: ET.Element):

View File

@@ -302,7 +302,7 @@ def test_lowercase_except_abbreviations():
def test_room_visible_column_wins_over_hidden_duplicate(): def test_room_visible_column_wins_over_hidden_duplicate():
"""У 223 в таблице два столбца: видимый (с панелью) и скрытый (без). Берём видимый.""" """Скрытый столбец-дубль (у 223) не парсится вовсе — берём видимый."""
from xlsx_builder import SheetSpec from xlsx_builder import SheetSpec
spec = SheetSpec( spec = SheetSpec(
@@ -322,8 +322,8 @@ def test_room_visible_column_wins_over_hidden_duplicate():
assert room["type"] == "комп.класс linux/windows" assert room["type"] == "комп.класс linux/windows"
def test_room_only_in_hidden_column_still_parsed(): def test_room_only_in_hidden_column_not_parsed():
"""Аудитория, у которой есть только скрытый столбец, не пропадает.""" """Скрытое не парсим: аудитория со скрытым столбцом описания не получает."""
from xlsx_builder import SheetSpec from xlsx_builder import SheetSpec
spec = SheetSpec( spec = SheetSpec(
@@ -334,5 +334,24 @@ def test_room_only_in_hidden_column_still_parsed():
hidden_cols=[(6, 7)], hidden_cols=[(6, 7)],
) )
info = parsing.parse_rooms_info(make_sheets({parsing.ROOMS_SHEET: spec})) info = parsing.parse_rooms_info(make_sheets({parsing.ROOMS_SHEET: spec}))
assert info["ауд. 402"]["available_days"] == ["четверг", "пятница"] assert info == {}
assert info["ауд. 410"]["capacity"] == 30
def test_hidden_rows_not_parsed(monkeypatch):
"""Скрытые строки листа в расписание не попадают."""
from fixtures import fixture_sheets
from xlsx_builder import SheetSpec, build_workbook
sheets = fixture_sheets()
main = sheets[parsing.MAIN_SHEET]
# строки: 0 заголовок, 1 с/по, 2..5 понедельник, 6..7 вторник — скроем строку с первым вторником
spec = SheetSpec(
rows=main.rows,
merges=main.merges,
hidden_cols=main.hidden_cols,
hidden_rows=[6],
)
data = parsing.parse_schedule(make_sheets({parsing.MAIN_SHEET: spec}))
tuesday = data["ИВТ-11БО"]["days"].get("вторник", {})
assert not tuesday, "строка вторника скрыта — занятий быть не должно"
assert data["ИВТ-11БО"]["days"]["понедельник"][1]["ч"] # понедельник на месте

View File

@@ -28,6 +28,7 @@ class SheetSpec:
rows: list[list] rows: list[list]
merges: list[str] = field(default_factory=list) merges: list[str] = field(default_factory=list)
hidden_cols: list[tuple[int, int]] = field(default_factory=list) # 0-based, инклязивно hidden_cols: list[tuple[int, int]] = field(default_factory=list) # 0-based, инклязивно
hidden_rows: list[int] = field(default_factory=list) # 0-based
def build_workbook(sheets: dict[str, SheetSpec]) -> bytes: def build_workbook(sheets: dict[str, SheetSpec]) -> bytes:
@@ -83,7 +84,8 @@ def build_workbook(sheets: dict[str, SheetSpec]) -> bytes:
t = f' t="{t_attr}"' if t_attr else "" t = f' t="{t_attr}"' if t_attr else ""
cells.append(f'<c r="{col_letter(c)}{r}"{t}><v>{escape(str(v))}</v></c>') cells.append(f'<c r="{col_letter(c)}{r}"{t}><v>{escape(str(v))}</v></c>')
if cells: if cells:
row_xml.append(f'<row r="{r}">{"".join(cells)}</row>') hidden = ' hidden="1"' if (r - 1) in spec.hidden_rows else ""
row_xml.append(f'<row r="{r}"{hidden}>{"".join(cells)}</row>')
merges_xml = "" merges_xml = ""
if spec.merges: if spec.merges: