"""Работа с текстом: язык, токены, стемминг, предложения.""" from __future__ import annotations import re import unicodedata CYR = re.compile(r"[а-яёА-ЯЁ]") LAT = re.compile(r"[a-zA-Z]") TOKEN_RE = re.compile(r"[a-zA-Zа-яёА-ЯЁ][a-zA-Zа-яёА-ЯЁ0-9\-]{1,30}") RU_STOP = set(""" а без более больше будет будто бы был была были было быть в вам вас ведь весь вдруг вот впрочем все всегда всего всех всю вы где да даже два для до другой его ее её ей ему если есть еще ещё же за зачем здесь и из или им иметь их к как какой когда конечно кто куда ли лишь между меня мне может можно мой моя мы на над надо наш не него неё нее ней нельзя нет ни нибудь никогда ним них ничего но ну о об один он она они оно от очень перед по под после потом потому почти при про просто раз разве с сам свое своей свои свой себе себя сейчас сказать со совсем так такой там те тебя тем теперь то тогда того тоже той только том тот тут ты у уж уже хорошо хоть чего чей чем через что чтобы чуть эта эти это этой этом этот эту я является является является том числе также т е т д т п рис таблица глава раздел стр страница пример примера данной данного данные можно должен должна должны следует таким образом однако если этом при этом """.split()) EN_STOP = set(""" a about above after again against all am an and any are as at be because been before being below between both but by can cannot could did do does doing down during each few for from further had has have having he her here hers him his how i if in into is it its itself just me more most must my no nor not of off on once only or other our out over own same she should so some such than that the their them then there these they this those through to too under until up use used using very was we were what when where which while who whom why will with would you your figure table chapter section page fig eq ref see note """.split()) STOP = RU_STOP | EN_STOP # Мусорные токены, характерные для технической документации. JUNK = set(""" www http https com ru org net pdf doc docx page pages copyright reserved rights inc ltd corp all datasheet rev revision version document documents note notes """.split()) def detect_lang(text: str) -> str: """Грубое определение языка по соотношению кириллицы и латиницы.""" sample = text[:20_000] cyr = len(CYR.findall(sample)) lat = len(LAT.findall(sample)) if cyr + lat < 30: return "?" return "ru" if cyr > lat * 0.5 else "en" _RU_ENDINGS = ( "ированием", "ированию", "ирования", "ированный", "ирование", "ами", "ями", "ого", "ему", "ому", "ыми", "ими", "ая", "ое", "ые", "ый", "ой", "ий", "ин", "ов", "ев", "ам", "ям", "ах", "ях", "ею", "ью", "ия", "ии", "ие", "ем", "ом", "ах", "ешь", "ете", "ает", "ают", "ить", "ать", "ешь", "ся", "сь", "а", "е", "и", "й", "о", "у", "ы", "ь", "ю", "я", ) def stem(word: str) -> str: """Лёгкий стеммер: RU — отсечение окончаний, EN — отсечение суффиксов. Нужен не для лингвистической точности, а чтобы «инвертор», «инвертора» и «инверторы» попадали в один индексный ключ. """ w = word.lower().replace("ё", "е") if CYR.search(w): for end in _RU_ENDINGS: if len(w) - len(end) >= 4 and w.endswith(end): return w[: -len(end)] return w for end in ("ingly", "ations", "ation", "ings", "edly", "ies", "ing", "ers", "er", "es", "ed", "s"): if len(w) - len(end) >= 4 and w.endswith(end): base = w[: -len(end)] if end == "ies": base += "y" return base return w def tokens(text: str) -> list[str]: """Значимые слова текста в нижнем регистре.""" out = [] for m in TOKEN_RE.finditer(text): w = m.group(0).lower().replace("ё", "е") if len(w) < 3 or w in STOP or w in JUNK: continue if w.strip("-") != w: w = w.strip("-") if len(w) < 3: continue out.append(w) return out def clean_text(raw: str) -> str: """Убирает переносы строк внутри слов, колонтитулы и лишние пробелы.""" t = unicodedata.normalize("NFKC", raw) t = t.replace("­", "") t = re.sub(r"-\n(?=[a-zа-яё])", "", t) # перенос по слогам t = re.sub(r"(?<=[^\n\.\!\?:;])\n(?=[a-zа-яё])", " ", t) # разрыв строки t = re.sub(r"[ \t ]+", " ", t) t = re.sub(r"\n{3,}", "\n\n", t) return t.strip() SENT_SPLIT = re.compile(r"(?<=[.!?…])\s+(?=[«\"'(\[]?[A-ZА-ЯЁ0-9])|\n{2,}") def sentences(text: str) -> list[str]: """Разбивает текст на предложения, отбрасывая заведомый мусор.""" out = [] for chunk in SENT_SPLIT.split(text): if not chunk: continue s = " ".join(chunk.split()) if not (40 <= len(s) <= 400): continue letters = sum(ch.isalpha() for ch in s) if letters < len(s) * 0.55: # формулы, таблицы, номера страниц continue if s.count("|") > 2 or s.count("...") > 1 or s.count("__") > 0: continue words = s.split() if len(words) < 6: continue upper = sum(w.isupper() for w in words) if upper > len(words) * 0.6: # заголовок капсом continue out.append(s) return out def similar(a: str, b: str) -> float: """Мера пересечения двух предложений по множеству основ слов.""" sa = {stem(w) for w in tokens(a)} sb = {stem(w) for w in tokens(b)} if not sa or not sb: return 0.0 return len(sa & sb) / len(sa | sb) def pretty_name(filename: str) -> str: """Читаемое название из имени файла.""" name = re.sub(r"\.[A-Za-z0-9]{2,5}$", "", filename) name = name.replace("_", " ").replace("%20", " ") name = re.sub(r"[.\-]{2,}", " ", name) name = re.sub(r"\s{2,}", " ", name).strip(" -.") return name or filename