Парсер: замечания отдельной строкой — уровня всей ячейки
В ячейке с несколькими занятиями (курсы по выбору) красная заметка, занимающая строку(и) целиком, относится ко всем занятиям ячейки (пара отменяется целиком) и раздаётся каждому занятию. Заметка, написанная в одной строке с предметом/аудиторией, остаётся привязанной к своему занятию. Многострочные standalone-заметки сохраняют переводы строк. Кейс: ПИЭ-13МО, понедельник, 5-я пара. Diff по 33 группам: затронуто только поле note в multi-lesson ячейках; порядок «inline->ячейка».
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
необязательное: маркер, время, локация, примечания пишутся как придумается.
|
||||
Поэтому разбор не ветвится по количеству строк, а проходит один конвейер:
|
||||
|
||||
1. красные фрагменты таблицы (⟪...⟫) выносятся в заметки и удаляются из текста —
|
||||
они служат только примечаниями, даже если внутри «вложено» другое занятие;
|
||||
1. красные фрагменты таблицы выносятся в примечания: заметка, занимающая
|
||||
строку(и) целиком, относится ко всей ячейке (несколько курсов «по выбору»
|
||||
в паре отменяются вместе) и раздаётся всем занятиям ячейки; заметка
|
||||
в одной строке с предметом/аудиторией остаётся привязанной к занятию;
|
||||
2. извлекается маркер «к.в.» (своя строка или префикс первой строки);
|
||||
3. каждая строка классифицируется по ролям:
|
||||
- аудитория — есть «ауд.»/«каб.», URL, «онлайн», «зал...» (вместе с названием
|
||||
@@ -355,17 +357,56 @@ def _extract_marker(lines: list[str]) -> tuple[str, list[str]]:
|
||||
return "", lines
|
||||
|
||||
|
||||
def _unwrap_note(frag: str) -> str:
|
||||
if frag.startswith("(") and frag.endswith(")"):
|
||||
frag = frag[1:-1].strip()
|
||||
return frag
|
||||
|
||||
|
||||
def _squish_note(text: str) -> str:
|
||||
"""Как _squish, но переводы строк сохраняем: они несут смысл в заметках."""
|
||||
text = re.sub(r"[ \t]+\n", "\n", text)
|
||||
return re.sub(r"[ \t]{2,}", " ", text).strip()
|
||||
|
||||
|
||||
_SPAN_RE = re.escape(RED_OPEN) + r"(.*?)" + re.escape(RED_CLOSE)
|
||||
|
||||
|
||||
def _red_fragments(text: str) -> tuple[list[str], str]:
|
||||
"""Красные фрагменты -> примечания; текст остаётся на месте (пробелом)."""
|
||||
frag_re = re.escape(RED_OPEN) + r"(.*?)" + re.escape(RED_CLOSE)
|
||||
"""Красные фрагменты -> примечания; на их месте остаётся пробел."""
|
||||
frags = []
|
||||
for m in re.finditer(frag_re, text, re.S):
|
||||
frag = _squish(m.group(1))
|
||||
if frag.startswith("(") and frag.endswith(")"):
|
||||
frag = frag[1:-1].strip()
|
||||
for m in re.finditer(_SPAN_RE, text, re.S):
|
||||
frag = _squish_note(m.group(1))
|
||||
if frag:
|
||||
frags.append(frag)
|
||||
return frags, re.sub(frag_re, " ", text, flags=re.S)
|
||||
frags.append(_unwrap_note(frag))
|
||||
return frags, re.sub(_SPAN_RE, " ", text, flags=re.S)
|
||||
|
||||
|
||||
# Строка(и), целиком занятая красным фрагментом (начинается с ⟪ на своей
|
||||
# позиции, и весь фрагмент до ⟫ на конце строки; внутрь не попадает другой ⟪).
|
||||
_CELL_NOTE_RE = re.compile(
|
||||
r"^[ \t]*" + re.escape(RED_OPEN) + "([^" + re.escape(RED_OPEN) + r"]*?)" + re.escape(RED_CLOSE) + r"[ \t]*$",
|
||||
re.M,
|
||||
)
|
||||
|
||||
|
||||
def _extract_cell_notes(text: str) -> tuple[list[str], str]:
|
||||
"""Вычленяет красные заметки уровня всей ячейки: занимающие строку(и) целиком.
|
||||
|
||||
Такие заметки (например, «14.09 пары не будет» отдельной строкой)
|
||||
относятся ко всем занятиям ячейки — курсы «по выбору» внутри одной пары
|
||||
отменяются вместе. Заметки, приклеенные к содержательной строке
|
||||
(«онлайн ⟪начало в 14:30⟫»), остаются привязанными к своему занятию.
|
||||
"""
|
||||
notes: list[str] = []
|
||||
|
||||
def repl(m: re.Match) -> str:
|
||||
frag = _squish_note(m.group(1))
|
||||
if frag:
|
||||
notes.append(_unwrap_note(frag))
|
||||
return ""
|
||||
|
||||
return notes, _CELL_NOTE_RE.sub(repl, text)
|
||||
|
||||
|
||||
def _balanced(text: str) -> str:
|
||||
@@ -385,16 +426,15 @@ def _parse_block(block: list[str]) -> Optional[Lesson]:
|
||||
lines = [ln.strip() for ln in text.split("\n") if ln.strip()]
|
||||
if not lines:
|
||||
if red_notes:
|
||||
# в ячейке только примечание (например, «пары не будет»)
|
||||
# в блоке только примечание (например, «пары не будет»)
|
||||
return Lesson(note="\n".join(dict.fromkeys(red_notes)))
|
||||
return None
|
||||
|
||||
marker, lines = _extract_marker(lines)
|
||||
if not lines:
|
||||
if red_notes:
|
||||
# в ячейке только примечание (например, «пары не будет»)
|
||||
return Lesson(marker=marker, note="\n".join(dict.fromkeys(red_notes)))
|
||||
return None # в ячейке только маркер — занятия ещё нет
|
||||
return None # в блоке только маркер — занятия ещё нет
|
||||
|
||||
subjects: list[str] = []
|
||||
teachers: list[str] = []
|
||||
@@ -456,13 +496,21 @@ def _normalize_marker_of(line: str) -> Optional[str]:
|
||||
|
||||
|
||||
def parse_lesson_cell(cell_text: str) -> list[Lesson]:
|
||||
"""Текст ячейки -> список занятий."""
|
||||
"""Текст ячейки -> список занятий.
|
||||
|
||||
Задача: несколько занятий в одной ячейке (курсы «по выбору») отменяются
|
||||
вместе, поэтому красная заметка, занимающая строку(и) целиком, — уровня
|
||||
всей ячейки и раздаётся всем занятиям. Заметка, написанная в одной строке
|
||||
с предметом/аудиторией, остаётся привязанной к своему занятию.
|
||||
"""
|
||||
if not cell_text or not cell_text.strip():
|
||||
return []
|
||||
lines = cell_text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
||||
text = cell_text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
cell_notes, text = _extract_cell_notes(text)
|
||||
|
||||
lessons: list[Lesson] = []
|
||||
last_marker = ""
|
||||
for block in _split_blocks(lines):
|
||||
for block in _split_blocks(text.split("\n")):
|
||||
if len(block) == 1 and not _line_is_room(block[0]):
|
||||
standalone = _normalize_marker_of(block[0])
|
||||
if standalone:
|
||||
@@ -476,6 +524,16 @@ def parse_lesson_cell(cell_text: str) -> list[Lesson]:
|
||||
elif last_marker:
|
||||
lesson = replace(lesson, marker=last_marker)
|
||||
lessons.append(lesson)
|
||||
|
||||
if not cell_notes:
|
||||
return lessons
|
||||
if not lessons:
|
||||
return [Lesson(note="\n".join(dict.fromkeys(cell_notes)))]
|
||||
for idx, lesson in enumerate(lessons):
|
||||
have = [n for n in lesson.note.split("\n") if n]
|
||||
add = [c for c in cell_notes if c not in have]
|
||||
if add:
|
||||
lessons[idx] = replace(lesson, note="\n".join(have + add))
|
||||
return lessons
|
||||
|
||||
|
||||
|
||||
@@ -347,3 +347,29 @@ def test_full_name_inline_after_subject():
|
||||
assert result["subject"] == "Основы тестирования веб-приложений"
|
||||
assert result["teacher"] == "Денис Верховцев (Сигма)"
|
||||
assert result["room"] == "онлайн"
|
||||
|
||||
|
||||
def test_cell_note_applies_to_all_lessons():
|
||||
# ПИЭ-13МО, понедельник, 5-я пара: два курса по выбору, заметка о паре
|
||||
raw = (
|
||||
"к.в. Управление ИТ-проектами\nЕрофеев Александр Александрович (ПравоТех)\nонлайн\n"
|
||||
"к.в. Математические методы и модели принятия решения Наука онлайн\n"
|
||||
+ R + "14.09 пары не будет" + Cc
|
||||
)
|
||||
ls = lessons(raw)
|
||||
assert len(ls) == 2
|
||||
assert all(l["note"] == "14.09 пары не будет" for l in ls)
|
||||
assert {l["subject"] for l in ls} == {
|
||||
"Управление ИТ-проектами",
|
||||
"Математические методы и модели принятия решения",
|
||||
}
|
||||
|
||||
|
||||
def test_inline_note_stays_with_its_lesson():
|
||||
raw = (
|
||||
"Русский язык\nШаманова М.В.\nауд. 312 " + R + "с 7.09 по 20.09 пары не будет" + Cc
|
||||
+ "\n\nМатематика\nСмирнов А.В.\nауд. 203"
|
||||
)
|
||||
ls = lessons(raw)
|
||||
assert ls[0]["note"] == "с 7.09 по 20.09 пары не будет"
|
||||
assert ls[1]["note"] == ""
|
||||
|
||||
Reference in New Issue
Block a user