Парсер: замечания отдельной строкой — уровня всей ячейки
В ячейке с несколькими занятиями (курсы по выбору) красная заметка, занимающая строку(и) целиком, относится ко всем занятиям ячейки (пара отменяется целиком) и раздаётся каждому занятию. Заметка, написанная в одной строке с предметом/аудиторией, остаётся привязанной к своему занятию. Многострочные standalone-заметки сохраняют переводы строк. Кейс: ПИЭ-13МО, понедельник, 5-я пара. Diff по 33 группам: затронуто только поле note в multi-lesson ячейках; порядок «inline->ячейка».
This commit is contained in:
@@ -4,8 +4,10 @@
|
|||||||
необязательное: маркер, время, локация, примечания пишутся как придумается.
|
необязательное: маркер, время, локация, примечания пишутся как придумается.
|
||||||
Поэтому разбор не ветвится по количеству строк, а проходит один конвейер:
|
Поэтому разбор не ветвится по количеству строк, а проходит один конвейер:
|
||||||
|
|
||||||
1. красные фрагменты таблицы (⟪...⟫) выносятся в заметки и удаляются из текста —
|
1. красные фрагменты таблицы выносятся в примечания: заметка, занимающая
|
||||||
они служат только примечаниями, даже если внутри «вложено» другое занятие;
|
строку(и) целиком, относится ко всей ячейке (несколько курсов «по выбору»
|
||||||
|
в паре отменяются вместе) и раздаётся всем занятиям ячейки; заметка
|
||||||
|
в одной строке с предметом/аудиторией остаётся привязанной к занятию;
|
||||||
2. извлекается маркер «к.в.» (своя строка или префикс первой строки);
|
2. извлекается маркер «к.в.» (своя строка или префикс первой строки);
|
||||||
3. каждая строка классифицируется по ролям:
|
3. каждая строка классифицируется по ролям:
|
||||||
- аудитория — есть «ауд.»/«каб.», URL, «онлайн», «зал...» (вместе с названием
|
- аудитория — есть «ауд.»/«каб.», URL, «онлайн», «зал...» (вместе с названием
|
||||||
@@ -355,17 +357,56 @@ def _extract_marker(lines: list[str]) -> tuple[str, list[str]]:
|
|||||||
return "", lines
|
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]:
|
def _red_fragments(text: str) -> tuple[list[str], str]:
|
||||||
"""Красные фрагменты -> примечания; текст остаётся на месте (пробелом)."""
|
"""Красные фрагменты -> примечания; на их месте остаётся пробел."""
|
||||||
frag_re = re.escape(RED_OPEN) + r"(.*?)" + re.escape(RED_CLOSE)
|
|
||||||
frags = []
|
frags = []
|
||||||
for m in re.finditer(frag_re, text, re.S):
|
for m in re.finditer(_SPAN_RE, text, re.S):
|
||||||
frag = _squish(m.group(1))
|
frag = _squish_note(m.group(1))
|
||||||
if frag.startswith("(") and frag.endswith(")"):
|
|
||||||
frag = frag[1:-1].strip()
|
|
||||||
if frag:
|
if frag:
|
||||||
frags.append(frag)
|
frags.append(_unwrap_note(frag))
|
||||||
return frags, re.sub(frag_re, " ", text, flags=re.S)
|
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:
|
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()]
|
lines = [ln.strip() for ln in text.split("\n") if ln.strip()]
|
||||||
if not lines:
|
if not lines:
|
||||||
if red_notes:
|
if red_notes:
|
||||||
# в ячейке только примечание (например, «пары не будет»)
|
# в блоке только примечание (например, «пары не будет»)
|
||||||
return Lesson(note="\n".join(dict.fromkeys(red_notes)))
|
return Lesson(note="\n".join(dict.fromkeys(red_notes)))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
marker, lines = _extract_marker(lines)
|
marker, lines = _extract_marker(lines)
|
||||||
if not lines:
|
if not lines:
|
||||||
if red_notes:
|
if red_notes:
|
||||||
# в ячейке только примечание (например, «пары не будет»)
|
|
||||||
return Lesson(marker=marker, note="\n".join(dict.fromkeys(red_notes)))
|
return Lesson(marker=marker, note="\n".join(dict.fromkeys(red_notes)))
|
||||||
return None # в ячейке только маркер — занятия ещё нет
|
return None # в блоке только маркер — занятия ещё нет
|
||||||
|
|
||||||
subjects: list[str] = []
|
subjects: list[str] = []
|
||||||
teachers: 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]:
|
def parse_lesson_cell(cell_text: str) -> list[Lesson]:
|
||||||
"""Текст ячейки -> список занятий."""
|
"""Текст ячейки -> список занятий.
|
||||||
|
|
||||||
|
Задача: несколько занятий в одной ячейке (курсы «по выбору») отменяются
|
||||||
|
вместе, поэтому красная заметка, занимающая строку(и) целиком, — уровня
|
||||||
|
всей ячейки и раздаётся всем занятиям. Заметка, написанная в одной строке
|
||||||
|
с предметом/аудиторией, остаётся привязанной к своему занятию.
|
||||||
|
"""
|
||||||
if not cell_text or not cell_text.strip():
|
if not cell_text or not cell_text.strip():
|
||||||
return []
|
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] = []
|
lessons: list[Lesson] = []
|
||||||
last_marker = ""
|
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]):
|
if len(block) == 1 and not _line_is_room(block[0]):
|
||||||
standalone = _normalize_marker_of(block[0])
|
standalone = _normalize_marker_of(block[0])
|
||||||
if standalone:
|
if standalone:
|
||||||
@@ -476,6 +524,16 @@ def parse_lesson_cell(cell_text: str) -> list[Lesson]:
|
|||||||
elif last_marker:
|
elif last_marker:
|
||||||
lesson = replace(lesson, marker=last_marker)
|
lesson = replace(lesson, marker=last_marker)
|
||||||
lessons.append(lesson)
|
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
|
return lessons
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -347,3 +347,29 @@ def test_full_name_inline_after_subject():
|
|||||||
assert result["subject"] == "Основы тестирования веб-приложений"
|
assert result["subject"] == "Основы тестирования веб-приложений"
|
||||||
assert result["teacher"] == "Денис Верховцев (Сигма)"
|
assert result["teacher"] == "Денис Верховцев (Сигма)"
|
||||||
assert result["room"] == "онлайн"
|
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