diff --git a/linker.py b/linker.py old mode 100644 new mode 100755 index 1503c40..24f2752 --- a/linker.py +++ b/linker.py @@ -1,15 +1,22 @@ #!/usr/bin/env python3 import re -import shutil import tempfile import xml.etree.ElementTree as ET from pathlib import Path from zipfile import ZipFile +from urllib.parse import urlsplit, urlunsplit +import requests +from bs4 import BeautifulSoup + + +INPUT_DOCX = "Отчет.docx" +OUTPUT_DOCX = "Отчет_fixed.docx" FIRST_NEW_SOURCE = 8 +ACCESS_DATE = "30.05.2026" NS = { "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main" @@ -22,143 +29,153 @@ def normalize_url(url): if "#:~:text=" in url: url = url.split("#:~:text=", 1)[0] - url = url.rstrip("/") + parts = urlsplit(url) + + return urlunsplit(( + parts.scheme.lower(), + parts.netloc.lower(), + parts.path.rstrip("/"), + "", + "" + )) + + +def get_title(url): + try: + response = requests.get( + url, + timeout=15, + headers={ + "User-Agent": "Mozilla/5.0" + } + ) + + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + + og = soup.find("meta", property="og:title") + if og and og.get("content"): + return " ".join(og["content"].split()) + + meta = soup.find("meta", attrs={"name": "title"}) + if meta and meta.get("content"): + return " ".join(meta["content"].split()) + + if soup.title and soup.title.string: + return " ".join(soup.title.string.split()) + + except Exception as e: + print(f"WARNING: {url}: {e}") return url -def build_url_mapping(root): - """ - Первый проход. - - Собираем все уникальные URL и - присваиваем им номера. - """ - - url_to_index = {} - next_index = FIRST_NEW_SOURCE - - for node in root.findall(".//w:instrText", NS): - - text = "".join(node.itertext()) - - match = re.search( - r'HYPERLINK\s+"([^"]+)"', - text - ) - - if not match: - continue - - url = normalize_url(match.group(1)) - - if url in url_to_index: - continue - - url_to_index[url] = next_index - next_index += 1 - - return url_to_index - - -def patch_document(root, url_to_index): - """ - Второй проход. - - Находим: - HYPERLINK "..." - - Затем ближайший следующий - [старый номер] - - И меняем его на новый. - """ - - current_url = None - waiting_for_visible_text = False - - for elem in root.iter(): - - tag = elem.tag - - if tag.endswith("instrText"): - - text = "".join(elem.itertext()) - - match = re.search( - r'HYPERLINK\s+"([^"]+)"', - text - ) - - if match: - current_url = normalize_url( - match.group(1) - ) - - waiting_for_visible_text = True - - continue - - if ( - waiting_for_visible_text - and tag.endswith("t") - and elem.text - and re.match(r"\[\d+\]", elem.text.strip()) - ): - index = url_to_index[current_url] - - old = elem.text - elem.text = f"[{index}]" - - print( - f"{old} -> {elem.text} : {current_url}" - ) - - waiting_for_visible_text = False - current_url = None - - def main(): - input_docx = Path("Отчет.docx") + with tempfile.TemporaryDirectory() as tmpdir: - output_docx = Path( - input_docx.stem + "_fixed.docx" - ) + tmpdir = Path(tmpdir) - with tempfile.TemporaryDirectory() as tmp: + with ZipFile(INPUT_DOCX) as archive: + archive.extractall(tmpdir) - tmp = Path(tmp) - - with ZipFile(input_docx) as archive: - archive.extractall(tmp) - - document_xml = ( - tmp / - "word" / - "document.xml" - ) + document_xml = tmpdir / "word" / "document.xml" tree = ET.parse(document_xml) - root = tree.getroot() - url_to_index = build_url_mapping(root) + url_to_index = {} + index_to_url = {} - print("\nURL -> INDEX\n") + next_index = FIRST_NEW_SOURCE + + # + # Первый проход: + # собираем все URL и выдаем номера + # + for node in root.findall(".//w:instrText", NS): + + text = "".join(node.itertext()) + + match = re.search(r'HYPERLINK\s+"([^"]+)"', text) + + if not match: + continue + + url = normalize_url(match.group(1)) + + if url in url_to_index: + continue + + url_to_index[url] = next_index + index_to_url[next_index] = url + + next_index += 1 + + print("\n=== URL -> INDEX ===\n") + + for index in sorted(index_to_url): + print(f"[{index}] {index_to_url[index]}") + + print() + + # + # Второй проход: + # меняем номера ссылок + # + current_url = None + waiting_for_text = False + replaced_count = 0 + + for elem in root.iter(): + + tag = elem.tag + + if tag.endswith("instrText"): + + text = "".join(elem.itertext()) + + match = re.search( + r'HYPERLINK\s+"([^"]+)"', + text + ) + + if match: + current_url = normalize_url(match.group(1)) + waiting_for_text = True + + continue + + if not waiting_for_text: + continue + + if not tag.endswith("t"): + continue + + if elem.text is None: + continue + + if not re.match(r"\[\d+\]", elem.text.strip()): + continue + + new_index = url_to_index[current_url] + + old_text = elem.text + + elem.text = f"[{new_index}]" - for url, index in url_to_index.items(): print( - f"[{index}] {url}" + f"{old_text} -> {elem.text} : {current_url}" ) - print( - f"\nFound {len(url_to_index)} unique sources\n" - ) + replaced_count += 1 - patch_document( - root, - url_to_index - ) + waiting_for_text = False + current_url = None + + print() + print(f"Replaced links: {replaced_count}") + print() tree.write( document_xml, @@ -166,27 +183,41 @@ def main(): xml_declaration=True ) - with ZipFile( - output_docx, - "w" - ) as out_zip: + with ZipFile(OUTPUT_DOCX, "w") as out_zip: - for file in tmp.rglob("*"): + for file in tmpdir.rglob("*"): if file.is_dir(): continue out_zip.write( file, - file.relative_to(tmp) + file.relative_to(tmpdir) ) - print( - f"\nSaved: {output_docx}" - ) + print(f"Saved: {OUTPUT_DOCX}") + + # + # Формируем список литературы + # + print("\n=== SOURCES ===\n") + + for index in sorted(index_to_url): + + url = index_to_url[index] + + title = get_title(url) + + print( + f"{index}. " + f"{title} " + f"(дата обращения: {ACCESS_DATE}). " + f"Режим доступа - {url}" + ) if __name__ == "__main__": main() + diff --git a/Отчет.docx b/Отчет.docx index 00b5f0d..be3b95f 100644 Binary files a/Отчет.docx and b/Отчет.docx differ