Парсер: ФИО из трёх слов после предмета, порядок «Имя Отчество Фамилия»
В субботу у ПИЭ-41/42БО в название курса попадало «Денис»: правило разбиения забирало в преподавателя только два последних слова, а здесь их три («Основы тестирования веб-приложений Денис Сергеевич Верховцев (Сигма)»). - правило ФИО в конце строки теперь берёт 2–3 слова и использует нежадный префикс, поэтому первые слова предмета не «съедаются»; - добавлена проверка, что это действительно человек: для трёх слов нужно отчество (-вич/-вна), для двух — «фамильное» окончание, иначе название вроде «Введение в Машинное Обучение» в преподаватели не уедет; - ключ преподавателя собирается в обоих порядках («Верховцев Денис Сергеевич» и «Денис Сергеевич Верховцев») -> «Верховцев Д.С.»; организация вроде «ПравоТех» больше не теряется. Проверено на живой таблице: предмет чистый, в списке преподавателей появился «Верховцев Д.С.», подозрительных ФИО по всем группам нет. Тесты: 168.
This commit is contained in:
@@ -159,6 +159,25 @@ def _line_is_room(line: str) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_PATRONYMIC_END = re.compile(r"(вич|вна|ична|инична)$", re.I)
|
||||||
|
_SURNAME_END = re.compile(r"(ов|ев|ёв|ин|ын|ский|цкий|ая|ко|ук|юк)$", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def _looks_like_person(text: str) -> bool:
|
||||||
|
"""Похоже ли «Имя Отчество Фамилия» на ФИО (а не на название предмета).
|
||||||
|
|
||||||
|
Для трёх слов требуем отчество (-вич/-вна), для двух — «фамильное»
|
||||||
|
окончание хотя бы у одного слова: иначе «Машинное Обучение» уедет
|
||||||
|
в преподаватели.
|
||||||
|
"""
|
||||||
|
parts = [part for part in text.split() if part]
|
||||||
|
if len(parts) >= 3:
|
||||||
|
return any(_PATRONYMIC_END.search(part) for part in parts[1:])
|
||||||
|
if len(parts) == 2:
|
||||||
|
return bool(_SURNAME_END.search(parts[0]) or _SURNAME_END.search(parts[1]))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _classify_line(line: str) -> _LineRoles:
|
def _classify_line(line: str) -> _LineRoles:
|
||||||
roles = _LineRoles()
|
roles = _LineRoles()
|
||||||
|
|
||||||
@@ -259,11 +278,13 @@ def _classify_line(line: str) -> _LineRoles:
|
|||||||
if m:
|
if m:
|
||||||
roles.subject, roles.teacher = m.group(1).strip(), m.group(2).strip()
|
roles.subject, roles.teacher = m.group(1).strip(), m.group(2).strip()
|
||||||
return roles
|
return roles
|
||||||
# «Предмет Денис Верховцев (Сигма)» — ФИО без отчества в конце строки
|
# «Предмет Денис Верховцев (Сигма)» / «Предмет Денис Сергеевич Верховцев (Сигма)» —
|
||||||
|
# ФИО в конце строки, может быть из двух или трёх слов
|
||||||
m = re.search(
|
m = re.search(
|
||||||
r"^(.*\s)([А-ЯЁ][а-яё]{2,}\s+[А-ЯЁ][а-яё]{2,})(\s*\([^()]*\))?\s*$", subject
|
r"^(.*?\s)([А-ЯЁ][а-яё]{2,}(?:\s+[А-ЯЁ][а-яё]{2,}){1,2})(\s*\([^()]*\))?\s*$",
|
||||||
|
subject,
|
||||||
)
|
)
|
||||||
if m and len(subject.split()) >= 4:
|
if m and len(subject.split()) >= 4 and _looks_like_person(m.group(2)):
|
||||||
teacher = _squish(m.group(2) + " " + (m.group(3) or ""))
|
teacher = _squish(m.group(2) + " " + (m.group(3) or ""))
|
||||||
roles.subject, roles.teacher = m.group(1).strip(), teacher
|
roles.subject, roles.teacher = m.group(1).strip(), teacher
|
||||||
return roles
|
return roles
|
||||||
@@ -579,6 +600,34 @@ def is_teacher_name(text: str) -> bool:
|
|||||||
return bool(_NAME_INITIALS_RE.search(text) or _FULL_NAME_RE.match(text.strip()))
|
return bool(_NAME_INITIALS_RE.search(text) or _FULL_NAME_RE.match(text.strip()))
|
||||||
|
|
||||||
|
|
||||||
|
# 2-3 слова с большой буквы — ФИО в любом порядке (без «НПО Криста»: там Криста с большой,
|
||||||
|
# но первое слово «НПО» — аббревиатура и не подходит под [А-ЯЁ][а-яё]{2,})
|
||||||
|
_FULL_NAME_WORDS_RE = re.compile(r"^[А-ЯЁ][а-яё]{2,}(?:\s+[А-ЯЁ][а-яё]{2,}){1,2}$")
|
||||||
|
|
||||||
|
|
||||||
|
def _short_from_parts(parts: list[str]) -> str:
|
||||||
|
"""ФИО -> «Фамилия И.О.» с учётом порядка слов.
|
||||||
|
|
||||||
|
В таблице встречаются оба порядка: «Верховцев Денис Сергеевич» и
|
||||||
|
«Денис Сергеевич Верховцев». Фамилию определяем по отчеству (-вич/-вна),
|
||||||
|
а для двух слов — по «фамильному» окончанию.
|
||||||
|
"""
|
||||||
|
if len(parts) >= 3 and _PATRONYMIC_END.search(parts[1]):
|
||||||
|
surname, given, patronymic = parts[2], parts[0], parts[1]
|
||||||
|
elif len(parts) == 2 and _SURNAME_END.search(parts[1]) and not _SURNAME_END.search(parts[0]):
|
||||||
|
surname, given, patronymic = parts[1], parts[0], ""
|
||||||
|
else:
|
||||||
|
surname = parts[0]
|
||||||
|
given = parts[1] if len(parts) > 1 else ""
|
||||||
|
patronymic = parts[2] if len(parts) > 2 else ""
|
||||||
|
short = surname
|
||||||
|
if given:
|
||||||
|
short += f" {given[0].upper()}."
|
||||||
|
if patronymic:
|
||||||
|
short += f"{patronymic[0].upper()}."
|
||||||
|
return short
|
||||||
|
|
||||||
|
|
||||||
def extract_teacher_names(teacher: str) -> list[str]:
|
def extract_teacher_names(teacher: str) -> list[str]:
|
||||||
"""Строка преподавателя -> список коротких имён «Фамилия И.О.» (ключи индекса).
|
"""Строка преподавателя -> список коротких имён «Фамилия И.О.» (ключи индекса).
|
||||||
|
|
||||||
@@ -590,16 +639,9 @@ def extract_teacher_names(teacher: str) -> list[str]:
|
|||||||
names = [m.strip() for m in _NAME_INITIALS_RE.findall(teacher) if m.strip()]
|
names = [m.strip() for m in _NAME_INITIALS_RE.findall(teacher) if m.strip()]
|
||||||
if not names:
|
if not names:
|
||||||
full = re.sub(r"\s*\([^()]*\)", "", teacher).strip()
|
full = re.sub(r"\s*\([^()]*\)", "", teacher).strip()
|
||||||
full_m = _FULL_NAME_RE.match(full)
|
full_m = _FULL_NAME_RE.match(full) or _FULL_NAME_WORDS_RE.match(full)
|
||||||
if full_m:
|
if full_m:
|
||||||
parts = full_m.group(0).split()
|
return [_short_from_parts(full_m.group(0).split())]
|
||||||
short = parts[0] + " " + parts[1][0].upper() + "."
|
if len(teacher) <= 30 and not re.search(r"\d", teacher):
|
||||||
if len(parts) > 2:
|
return [teacher] # организация: «ПравоТех», «НПО Криста»
|
||||||
short += parts[2][0].upper() + "."
|
|
||||||
return [short]
|
|
||||||
if (
|
|
||||||
not re.search(r"\d|\s", teacher)
|
|
||||||
or (len(teacher) <= 30 and " " in teacher)
|
|
||||||
) and not re.search(r"[а-яё][А-ЯЁ]", teacher):
|
|
||||||
return [teacher] # организация одним словом или «НПО Криста»
|
|
||||||
return names
|
return names
|
||||||
|
|||||||
@@ -384,6 +384,14 @@ def test_inline_lesson_after_room_no_blank_line():
|
|||||||
assert ls[1]["room"] == "ауд. 231"
|
assert ls[1]["room"] == "ауд. 231"
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_name_three_words_after_subject():
|
||||||
|
"""«Предмет Денис Сергеевич Верховцев (Сигма)» — ФИО не должно попасть в предмет."""
|
||||||
|
result = one("Основы тестирования веб-приложений Денис Сергеевич Верховцев (Сигма)\nонлайн")
|
||||||
|
assert result["subject"] == "Основы тестирования веб-приложений"
|
||||||
|
assert result["teacher"] == "Денис Сергеевич Верховцев (Сигма)"
|
||||||
|
assert result["room"] == "онлайн"
|
||||||
|
|
||||||
|
|
||||||
def test_plain_time_paren_line():
|
def test_plain_time_paren_line():
|
||||||
result = one("Параллельное программирование (пр)\nМещеряков И.А.\nауд. 213\n(начало пары в 12:30)")
|
result = one("Параллельное программирование (пр)\nМещеряков И.А.\nауд. 213\n(начало пары в 12:30)")
|
||||||
assert result["note"] == "начало пары в 12:30"
|
assert result["note"] == "начало пары в 12:30"
|
||||||
@@ -420,3 +428,20 @@ def test_inline_note_stays_with_its_lesson():
|
|||||||
ls = lessons(raw)
|
ls = lessons(raw)
|
||||||
assert ls[0]["note"] == "с 7.09 по 20.09 пары не будет"
|
assert ls[0]["note"] == "с 7.09 по 20.09 пары не будет"
|
||||||
assert ls[1]["note"] == ""
|
assert ls[1]["note"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"teacher, expected",
|
||||||
|
[
|
||||||
|
("Верховцев Денис Сергеевич", ["Верховцев Д.С."]),
|
||||||
|
("Денис Сергеевич Верховцев", ["Верховцев Д.С."]),
|
||||||
|
("Денис Верховцев (Сигма)", ["Верховцев Д."]),
|
||||||
|
("Лабеев Игорь Владимирович", ["Лабеев И.В."]),
|
||||||
|
("ПравоТех", ["ПравоТех"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_teacher_keys_order(teacher, expected):
|
||||||
|
"""Ключ преподавателя — «Фамилия И.О.» в любом порядке ФИО в таблице."""
|
||||||
|
from schedule.lessons import extract_teacher_names
|
||||||
|
|
||||||
|
assert extract_teacher_names(teacher) == expected
|
||||||
|
|||||||
Reference in New Issue
Block a user