fix: enrich HSE profile parsing with publications and theses
This commit is contained in:
@@ -164,6 +164,7 @@ def parse_person_profile(
|
||||
header = extract_person_header(soup, normalized_url)
|
||||
tabs = extract_person_tabs(soup, normalized_url)
|
||||
sections = extract_sections(soup, normalized_url)
|
||||
sections = enrich_sections_from_hse_widgets(session, soup, normalized_url, headers, timeout, sections)
|
||||
internal_links = [tab["href"] for tab in tabs if tab.get("href")]
|
||||
|
||||
return {
|
||||
@@ -183,6 +184,25 @@ def parse_person_profile(
|
||||
}
|
||||
|
||||
|
||||
def enrich_sections_from_hse_widgets(
|
||||
session: Session,
|
||||
soup: BeautifulSoup,
|
||||
source_url: str,
|
||||
headers: dict[str, str],
|
||||
timeout: int,
|
||||
sections: list[dict],
|
||||
) -> list[dict]:
|
||||
enriched = list(sections)
|
||||
publications = _load_widget_publications(session, soup, headers, timeout)
|
||||
if publications:
|
||||
enriched = _upsert_publications_section(enriched, publications)
|
||||
|
||||
theses = _load_widget_graduation_theses(session, soup, source_url, headers, timeout)
|
||||
if theses:
|
||||
enriched = _upsert_graduation_theses_section(enriched, theses)
|
||||
return enriched
|
||||
|
||||
|
||||
def _render_with_playwright(source_url: str, fallback_html: str) -> str:
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
@@ -206,6 +226,89 @@ def _render_with_playwright(source_url: str, fallback_html: str) -> str:
|
||||
return fallback_html
|
||||
|
||||
|
||||
def _load_widget_publications(session: Session, soup: BeautifulSoup, headers: dict[str, str], timeout: int) -> list[dict]:
|
||||
script = soup.select_one('script[data-widget-name="AuthorSearch"][data-author]')
|
||||
if not script:
|
||||
return []
|
||||
author_id = normalize_ws(script.get("data-author"))
|
||||
if not author_id:
|
||||
return []
|
||||
|
||||
publications = []
|
||||
page_id = 1
|
||||
per_page = 100
|
||||
while page_id <= 20:
|
||||
payload = {
|
||||
"type": "ANY",
|
||||
"filterParams": (
|
||||
f'"acceptLanguage":"ru"|"fullTextPublicEnabled": 1|'
|
||||
f'"pubsAuthor": {author_id}|"widgetName": "AuthorSearch"'
|
||||
),
|
||||
"paginationParams": {
|
||||
"publsSort": ["TITLE_ASC"],
|
||||
"publsCount": per_page,
|
||||
"pageId": page_id,
|
||||
},
|
||||
}
|
||||
try:
|
||||
response = session.post(
|
||||
"https://publications.hse.ru/api/searchPubs",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except Exception:
|
||||
return publications
|
||||
|
||||
result = data.get("result") if isinstance(data, dict) else {}
|
||||
items = result.get("items") if isinstance(result, dict) else []
|
||||
if not isinstance(items, list) or not items:
|
||||
break
|
||||
publications.extend(_normalize_publication_item(item) for item in items if isinstance(item, dict))
|
||||
|
||||
total = int(result.get("total") or 0)
|
||||
if not result.get("more") and len(publications) >= total:
|
||||
break
|
||||
page_id += 1
|
||||
return _dedupe_publications(publications)
|
||||
|
||||
|
||||
def _load_widget_graduation_theses(
|
||||
session: Session,
|
||||
soup: BeautifulSoup,
|
||||
source_url: str,
|
||||
headers: dict[str, str],
|
||||
timeout: int,
|
||||
) -> list[dict]:
|
||||
script = soup.select_one('script[src*="/n/stat/vkr/app.js"][data-person-id]')
|
||||
if not script:
|
||||
return []
|
||||
person_id = normalize_ws(script.get("data-person-id"))
|
||||
api_url = normalize_ws(script.get("data-api-url")) or "/n/vkr/api/"
|
||||
if not person_id:
|
||||
return []
|
||||
|
||||
request_headers = {**headers, "x-portal-language": "ru"}
|
||||
try:
|
||||
response = session.get(
|
||||
urljoin(source_url, api_url),
|
||||
params={"supervisorId": person_id},
|
||||
headers=request_headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
items = data.get("data") if isinstance(data, dict) else []
|
||||
if not isinstance(items, list):
|
||||
return []
|
||||
return [_normalize_vkr_item(item, source_url) for item in items if isinstance(item, dict)]
|
||||
|
||||
|
||||
def _collect_between_h2(start_h2: Tag) -> list[Tag | NavigableString | str]:
|
||||
nodes = []
|
||||
for sibling in start_h2.next_siblings:
|
||||
@@ -353,6 +456,122 @@ def _parse_vkr_items(nodes: list) -> list[str]:
|
||||
return [item for item in dict.fromkeys(items) if item]
|
||||
|
||||
|
||||
def _normalize_publication_item(item: dict) -> dict:
|
||||
publication_id = str(item.get("id") or "").strip()
|
||||
title = _html_to_text(item.get("title"))
|
||||
year = item.get("year")
|
||||
publication_type = str(item.get("type") or "").strip() or None
|
||||
description = item.get("description") if isinstance(item.get("description"), dict) else {}
|
||||
short_description = _localized_value(description.get("short")) or _localized_value(description.get("shortLeft"))
|
||||
text = normalize_ws(" ".join(part for part in [title, str(year or ""), short_description] if part))
|
||||
return {
|
||||
"id": publication_id or None,
|
||||
"title": title or publication_id,
|
||||
"year": year,
|
||||
"type": publication_type,
|
||||
"url": f"https://publications.hse.ru/view/{publication_id}" if publication_id else None,
|
||||
"text": text or title or publication_id,
|
||||
}
|
||||
|
||||
|
||||
def _normalize_vkr_item(item: dict, source_url: str) -> dict:
|
||||
thesis_id = item.get("id")
|
||||
program = item.get("learnProgram") if isinstance(item.get("learnProgram"), dict) else {}
|
||||
org_unit = item.get("orgUnit") if isinstance(item.get("orgUnit"), dict) else {}
|
||||
supervisors = []
|
||||
for supervisor in item.get("supervisors") or []:
|
||||
if not isinstance(supervisor, dict):
|
||||
continue
|
||||
name = normalize_ws(supervisor.get("name"))
|
||||
url = normalize_ws(supervisor.get("url"))
|
||||
if name or url:
|
||||
supervisors.append({"name": name or url, "url": url or None})
|
||||
return {
|
||||
"id": thesis_id,
|
||||
"student": normalize_ws(item.get("student")),
|
||||
"title": normalize_ws(item.get("title")),
|
||||
"defense_year": item.get("year"),
|
||||
"level": normalize_ws(item.get("level")),
|
||||
"rating": item.get("rating"),
|
||||
"project_url": urljoin(source_url, f"/edu/vkr/{thesis_id}") if thesis_id else None,
|
||||
"program": normalize_ws(program.get("title")),
|
||||
"program_url": urljoin(source_url, program.get("url")) if program.get("url") else None,
|
||||
"org_unit": normalize_ws(org_unit.get("title")),
|
||||
"org_unit_url": urljoin(source_url, org_unit.get("url")) if org_unit.get("url") else None,
|
||||
"supervisors": supervisors,
|
||||
"text": normalize_ws(" ".join(str(part) for part in [item.get("student"), item.get("title"), item.get("year")] if part)),
|
||||
}
|
||||
|
||||
|
||||
def _upsert_publications_section(sections: list[dict], publications: list[dict]) -> list[dict]:
|
||||
merged = []
|
||||
inserted = False
|
||||
for section in sections:
|
||||
if section.get("type") != "publications":
|
||||
merged.append(section)
|
||||
continue
|
||||
existing = section.get("publications") or []
|
||||
section = {
|
||||
**section,
|
||||
"publications_count": max(section.get("publications_count") or 0, len(publications)),
|
||||
"publications": _dedupe_publications([*existing, *publications]),
|
||||
}
|
||||
section["items"] = [item["text"] for item in section["publications"] if item.get("text")]
|
||||
merged.append(section)
|
||||
inserted = True
|
||||
if not inserted:
|
||||
merged.append(
|
||||
{
|
||||
"title": "Публикации и исследования",
|
||||
"slug": "publikacii_i_issledovaniya",
|
||||
"type": "publications",
|
||||
"raw_text": "",
|
||||
"paragraphs": [],
|
||||
"items": [item["text"] for item in publications if item.get("text")],
|
||||
"links": [],
|
||||
"publications_count": len(publications),
|
||||
"publications": publications,
|
||||
}
|
||||
)
|
||||
return merged
|
||||
|
||||
|
||||
def _upsert_graduation_theses_section(sections: list[dict], theses: list[dict]) -> list[dict]:
|
||||
section = {
|
||||
"title": "Выпускные квалификационные работы студентов НИУ ВШЭ",
|
||||
"slug": "vypusknye_kvalifikacionnye_raboty_studentov_niu_vshe",
|
||||
"type": "graduation_theses",
|
||||
"raw_text": "",
|
||||
"paragraphs": [],
|
||||
"items": [item["text"] for item in theses if item.get("text")],
|
||||
"links": [{"text": item["title"], "url": item["project_url"]} for item in theses if item.get("title") and item.get("project_url")],
|
||||
"theses_count": len(theses),
|
||||
"theses": theses,
|
||||
}
|
||||
return [item for item in sections if item.get("type") != "graduation_theses"] + [section]
|
||||
|
||||
|
||||
def _dedupe_publications(items: list[dict]) -> list[dict]:
|
||||
seen = set()
|
||||
unique = []
|
||||
for item in items:
|
||||
key = item.get("id") or item.get("url") or item.get("title")
|
||||
if key and key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(item)
|
||||
return unique
|
||||
|
||||
|
||||
def _html_to_text(value: object) -> str:
|
||||
return normalize_ws(BeautifulSoup(str(value or ""), "html.parser").get_text(" ", strip=True))
|
||||
|
||||
|
||||
def _localized_value(value: object) -> str:
|
||||
if isinstance(value, dict):
|
||||
return normalize_ws(value.get("ru") or value.get("publ") or value.get("en"))
|
||||
return normalize_ws(str(value or ""))
|
||||
|
||||
|
||||
def _slugify(value: str) -> str:
|
||||
cleaned = re.sub(r"[^\w\s-]", "", value.lower(), flags=re.UNICODE)
|
||||
return re.sub(r"[-\s]+", "_", cleaned).strip("_") or "section"
|
||||
|
||||
Reference in New Issue
Block a user