/
dbccx
/
VestnikChecker
Обзор
Документация
Войти
/
dbccx
/
VestnikChecker
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
core/rule_engine.py
1 072 строки
50 KB
t i a
Обновлены модули проверки, исправлены веса
09 июн 2026, 17:47
09 июн 2026, 17:47
eb921b9
Код
Авторство
О чём код?
import re from typing import Optional from models.check_result import CheckResult, Violation class RuleEngine: def __init__(self): # Эталонные значения из требований журнала self.page_width_mm = 210.0 self.page_height_mm = 297.0 self.margin_mm = 25.0 # Размеры шрифтов self.title_font_size = 14 self.author_font_size = 11 self.org_font_size = 11 self.abstract_font_size = 9 self.keywords_font_size = 9 self.main_text_font_size = 11 self.heading1_font_size = 14 self.heading2_font_size = 12 self.heading3_font_size = 12 self.caption_font_size = 11 self.footnote_font_size = 10 self.red_line_indent_mm = 7.0 self.required_font_family = "Computer Modern Unicode" def check_format(self, parsed_doc, pdf_path: str = None) -> CheckResult: result = CheckResult() doc_structure = None if pdf_path: doc_structure = self._extract_structure_from_pdf(pdf_path) if not doc_structure: result.percentage = 0 result.category = "Безусловно не соответствует" return result # Проверки с весами: (название, функция, вес) # Вес 3 — критично (шрифты, основный текст, переводная часть) # Вес 2 — важно (образец цитирования, поля) # Вес 1 — менее важно (формулы, рисунки, сноски) format_checks = [ ("Шрифты Computer Modern Unicode", self._check_fonts, 5), ("Пакет cmap (копирование кириллицы)", self._check_cmap_package, 5), ("Основной текст (11 пт)", self._check_main_text_format, 5), ("Переводная часть", self._check_translation_part, 5), ("Образец цитирования", self._check_citation_sample, 5), #("Параметры страницы (поля 25 мм)", self._check_page_params, 1), ("Название статьи (14 пт, полужирный, по центру)", self._check_title_format, 2), ("Список авторов (11 пт, полужирный, по центру)", self._check_authors_format, 2), ("Оформление организации (11 пт, курсив)", self._check_affiliation_format, 1), ("Оформление аннотации (9 пт, красная строка)", self._check_abstract_format, 1), ("Оформление ключевых слов (9 пт, курсив)", self._check_keywords_format, 1), ("Заголовки (нет точек, отступы)", self._check_headings_general, 1), ("Заголовки 1 уровня (14 пт, полужирный)", self._check_heading1_format, 1), ("Заголовки 2 уровня (12 пт, полужирный)", self._check_heading2_format, 1), ("Заголовки 3 уровня (12 пт, курсив)", self._check_heading3_format, 1), ("Листинги программ (Courier New, 10 пт)", self._check_code_font, 1), ("Оформление формул (номер справа)", self._check_formulas, 2), ("Оформление рисунков", self._check_figures, 2), ("Оформление таблиц", self._check_tables, 2), ("Перекрестные ссылки [1, 2, 3]", self._check_cross_references, 1), ("Информация о финансовой поддержке", self._check_funding_info, 1), ("Список литературы (нулевой отступ)", self._check_bibliography_format, 1), ("Прочие правила (кавычки, тире, ё)", self._check_other_rules, 1), ] total_weight = sum(w for _, _, w in format_checks) passed_weight = 0 for check_name, check_func, weight in format_checks: violations = check_func(doc_structure) if not violations: passed_weight += weight else: result.violations.extend(violations) result.percentage = round((passed_weight / total_weight * 100), 1) if total_weight > 0 else 0 result.category = result.compute_category(result.percentage) result.sort_violations() return result def _extract_structure_from_pdf(self, pdf_path: str) -> Optional[dict]: if not pdf_path: return None result = self._extract_with_fitz(pdf_path) if result and result.get("pages"): return result # Если не сработало print("Не удалось извлечь структуру") return None def _extract_with_fitz(self, pdf_path: str) -> Optional[dict]: try: import fitz doc = fitz.open(pdf_path) pages_data = [] for page_num in range(len(doc)): page = doc[page_num] blocks = page.get_text("dict")["blocks"] chars = [] page_lines = [] # ← ДОБАВИТЬ: список строк страницы for block in blocks: if "lines" in block: for line in block["lines"]: line_chars = [] line_text = "" for span in line["spans"]: text = span["text"] font_name = span.get("font", "Unknown") font_size = span.get("size", 11) bbox = span.get("bbox", (0, 0, 0, 0)) char_width = (bbox[2] - bbox[0]) / max(len(text), 1) for i, ch in enumerate(text): char_data = { "text": ch, "x0": bbox[0] + i * char_width, "x1": bbox[0] + (i + 1) * char_width, "top": bbox[1], "bottom": bbox[3], "size": font_size, "fontname": font_name, } chars.append(char_data) line_chars.append(char_data) line_text += text # Сохраняем строку if line_chars: page_lines.append({ "chars": line_chars, "text": line_text, }) raw_text = page.get_text() if chars: pages_data.append({ "width": page.rect.width, "height": page.rect.height, "chars": chars, "lines": page_lines, # ← ИСПРАВЛЕНО: теперь не пусто "raw_text": raw_text, }) doc.close() if pages_data: return {"pages": pages_data, "total_pages": len(pages_data)} except Exception: return None #Методы проверки конкретных требований def _check_cmap_package(self, doc: dict) -> list: violations = [] try: pages = doc.get("pages", []) if not pages: return violations has_cyrillic_issues = False for page in pages: raw_text = page.get("raw_text", "") # Ищем русские слова russian_words = re.findall(r'[а-яёА-ЯЁ]{3,}', raw_text) # Ошибки кодировки garbled = re.findall(r'[À-ÿ]{5,}', raw_text) if garbled and not russian_words: has_cyrillic_issues = True break dd_pattern = re.findall(r'(?:\d{2,3}\s){5,}', raw_text) if dd_pattern and not russian_words: has_cyrillic_issues = True break if has_cyrillic_issues: violations.append(Violation( severity="high", category="Оформление", description="В исходный код LaTeX необходимо включить пакет \\usepackage{cmap} " "для обеспечения корректного копирования и поиска текста", suggestion="Добавьте \\usepackage{cmap} в преамбулу LaTeX-документа" )) except Exception: pass return violations @staticmethod def _format_page_list(pages: list) -> str: #Форматирует список страниц в компактную строку. [1,2,3,4,7,8,10] -> "1-4, 7, 8, 10 if not pages: return "" if len(pages) == 1: return str(pages[0]) pages = sorted(pages) result = [] start = pages[0] end = pages[0] for page in pages[1:]: if page == end + 1: end = page else: if start == end: result.append(str(start)) else: result.append(f"{start}-{end}") start = page end = page # Последний диапазон if start == end: result.append(str(start)) else: result.append(f"{start}-{end}") return ", ".join(result) def _check_page_params(self, doc: dict) -> list: #Пункт 2: Проверка параметров страницы (поля 25 мм). violations = [] try: pages = doc.get("pages", []) left_pages = [] right_pages = [] top_pages = [] bottom_pages = [] for i, page in enumerate(pages): chars = page.get("chars", []) if not chars: continue # Исключаем символы в колонтитулах (верхние/нижние 5% страницы) page_height = page["height"] page_width = page["width"] main_chars = [ c for c in chars if c["top"] > page_height * 0.05 # не верхний колонтитул and c["bottom"] < page_height * 0.95 # не нижний колонтитул ] if not main_chars: main_chars = chars # если всё попало в колонтитулы — берём всё min_x = min(c["x0"] for c in main_chars) max_x = max(c["x1"] for c in main_chars) min_y = min(c["top"] for c in main_chars) max_y = max(c["bottom"] for c in main_chars) left_margin = min_x * 0.3528 right_margin = (page_width - max_x) * 0.3528 top_margin = min_y * 0.3528 bottom_margin = (page_height - max_y) * 0.3528 page_num = i + 1 # Увеличиваем допуск до 5 мм if abs(left_margin - self.margin_mm) > 5: left_pages.append(page_num) if abs(right_margin - self.margin_mm) > 5: right_pages.append(page_num) if abs(top_margin - self.margin_mm) > 5: top_pages.append(page_num) if abs(bottom_margin - self.margin_mm) > 5: bottom_pages.append(page_num) if left_pages: violations.append(Violation( severity="medium", category="Оформление", description=f"Левое поле стр. {self._format_page_list(left_pages)}: " f"требуется {self.margin_mm} мм", suggestion="Установите левое поле 25 мм" )) if right_pages: violations.append(Violation( severity="medium", category="Оформление", description=f"Правое поле стр. {self._format_page_list(right_pages)}: " f"требуется {self.margin_mm} мм", suggestion="Установите правое поле 25 мм" )) if top_pages: violations.append(Violation( severity="medium", category="Оформление", description=f"Верхнее поле стр. {self._format_page_list(top_pages)}: " f"требуется {self.margin_mm} мм", suggestion="Установите верхнее поле 25 мм" )) if bottom_pages: violations.append(Violation( severity="medium", category="Оформление", description=f"Нижнее поле стр. {self._format_page_list(bottom_pages)}: " f"требуется {self.margin_mm} мм", suggestion="Установите нижнее поле 25 мм" )) except Exception: pass return violations def _check_fonts(self, doc: dict) -> list: violations = [] try: cm_fonts = [ "cmu", "computer modern", "cmr", "cmbx", "cmti", "cmsy", "sfrm", "sfbx", "sfti", # CM Super "sftt1095", # CM Sans Typewriter "sfbi", # CM Sans Bold Italic "cm", # любое упоминание CM "dsrom", "ds", # CM Double-struck "msam", "msbm", # AMS Math Symbols "rsfs", # Ralph Smith Formal Script "eusm", "eusb", # Euler Script "wasy", # WASY symbols "txsys", # TeX symbol "cmmi", "cmex", # CM Math Italic, CM Extension ] pages = doc.get("pages", []) wrong_fonts = set() for page in pages: for char in page.get("chars", []): font = char.get("fontname", "").lower() if font: is_cm = any(cmf in font for cmf in cm_fonts) if not is_cm: wrong_fonts.add(char.get("fontname", font)) if wrong_fonts: wrong_pages = set() for page_num, page in enumerate(pages): for char in page.get("chars", []): font = char.get("fontname", "").lower() is_cm = any(cmf in font for cmf in cm_fonts) if not is_cm: wrong_pages.add(page_num + 1) pages_str = self._format_page_list(list(wrong_pages)) violations.append(Violation( severity="high", category="Оформление", description=f"Стр. {pages_str}: использован неверный шрифт: " f"{', '.join(list(wrong_fonts)[:3])}. " f"Следует использовать шрифты семейства Computer Modern Unicode", suggestion="Используйте шрифты Computer Modern Unicode" )) except Exception: pass return violations def _check_title_format(self, doc: dict) -> list: # Пункт 7: Проверка названия статьи. violations = [] try: pages = doc.get("pages", []) if not pages: return violations first_page = pages[0] chars = first_page.get("chars", []) # Ищем самый крупный шрифт на первой трети страницы (это должно быть название) title_chars = [c for c in chars if c["top"] < first_page["height"] * 0.3] if not title_chars: return violations max_size = max(c["size"] for c in title_chars) if abs(max_size - self.title_font_size) > 1: violations.append(Violation( severity="high", category="Оформление", description=f"Название статьи: размер шрифта {max_size:.0f} пт (требуется {self.title_font_size} пт)", suggestion="Установите размер шрифта названия 14 пт" )) # Проверка выравнивания по центру title_text_chars = [c for c in title_chars if abs(c["size"] - max_size) < 1] if title_text_chars: leftmost = min(c["x0"] for c in title_text_chars) rightmost = max(c["x1"] for c in title_text_chars) text_center = (leftmost + rightmost) / 2 page_center = first_page["width"] / 2 if abs(text_center - page_center) > 20: violations.append(Violation( severity="medium", category="Оформление", description="Название статьи должно быть выровнено по центру", suggestion="Выровняйте название по центру" )) # Проверка на переносы в названии (грубая — если есть дефис в крупном тексте) for line in first_page.get("lines", []): line_chars = [c for c in line.get("chars", []) if abs(c["size"] - max_size) < 1] if line_chars and len(line_chars) > 5: text = "".join(c["text"] for c in line_chars) if text.rstrip().endswith("-"): violations.append(Violation( severity="low", category="Оформление", description="Переносы в названии недопустимы", suggestion="Удалите переносы в названии" )) except Exception: pass return violations def _check_authors_format(self, doc: dict) -> list: # Пункт 8: Проверка списка авторов. violations = [] try: pages = doc.get("pages", []) if not pages: return violations first_page = pages[0] lines = first_page.get("lines", []) # Ищем строку после названия (обычно авторы идут вторым блоком) found_title = False for line in lines: text = "".join(c["text"] for c in line.get("chars", [])) if not found_title and len(text.strip()) > 20: found_title = True continue if found_title and len(text.strip()) > 10: # Проверяем, что авторы выровнены по центру chars = line.get("chars", []) if chars: leftmost = min(c["x0"] for c in chars) rightmost = max(c["x1"] for c in chars) text_center = (leftmost + rightmost) / 2 page_center = first_page["width"] / 2 if abs(text_center - page_center) > 50: violations.append(Violation( severity="medium", category="Оформление", description="Список авторов должен быть выровнен по центру", suggestion="Выровняйте список авторов по центру" )) break except Exception: pass return violations def _check_affiliation_format(self, doc: dict) -> list: #Пункт 9: Проверка оформления организации. violations = [] try: pages = doc.get("pages", []) if not pages: return violations first_page = pages[0] chars = first_page.get("chars", []) # Ищем курсивный текст (разные варианты имён шрифтов для курсива) italic_keywords = ["italic", "oblique", "cursive", "cmti", "sfti"] italic_chars = [ c for c in chars if any(kw in c.get("fontname", "").lower() for kw in italic_keywords) ] if not italic_chars: pass # Проверяем наличие почтового адреса all_text = "" for line in first_page.get("lines", []): all_text += "".join(c["text"] for c in line.get("chars", [])) + " " all_text += first_page.get("raw_text", "") address_patterns = [ r'\(\d{6}\s', # (454080 ... r'\([\d]{3,}', # любой индекс r'г\.\s', # г. Челябинск r'пр\.\s', # пр. Ленина r'ул\.\s', # ул. ... r'д\.\s\d+', # д. 76 ] has_address = any(re.search(p, all_text) for p in address_patterns) if not has_address: violations.append(Violation( severity="medium", category="Оформление", description="Не указан почтовый адрес аффилиации", suggestion="Укажите почтовый адрес в скобках после названия организации" )) except Exception: pass return violations def _check_abstract_format(self, doc: dict) -> list: # Пункт 10: Проверка оформления аннотации. violations = [] try: pages = doc.get("pages", []) if not pages: return violations first_page = pages[0] chars = first_page.get("chars", []) # Ищем текст размером 9 пт (аннотация) abstract_chars = [c for c in chars if abs(c["size"] - self.abstract_font_size) < 1] if not abstract_chars: violations.append(Violation( severity="high", category="Оформление", description=f"Аннотация должна быть оформлена шрифтом {self.abstract_font_size} пт", suggestion=f"Установите размер шрифта аннотации {self.abstract_font_size} пт" )) else: # Проверяем красную строку (отступ 7 мм ≈ 19.8 пт) first_line_chars = [c for c in abstract_chars if c["top"] == min(c["top"] for c in abstract_chars)] if first_line_chars: min_x = min(c["x0"] for c in first_line_chars) expected_indent = self.margin_mm / 0.3528 + self.red_line_indent_mm / 0.3528 if abs(min_x - expected_indent) > 10: violations.append(Violation( severity="medium", category="Оформление", description="Аннотация должна начинаться с красной строки 7 мм", suggestion="Добавьте отступ красной строки 7 мм" )) except Exception: pass return violations def _check_keywords_format(self, doc: dict) -> list: #Пункт 11: Проверка оформления ключевых слов. violations = [] try: pages = doc.get("pages", []) if not pages: return violations first_page = pages[0] italic_keywords = ["italic", "oblique", "cursive", "cmti", "sfti"] # Ищем курсивный текст размером 9 пт (±1 пт) italic_chars = [ c for c in first_page.get("chars", []) if any(kw in c.get("fontname", "").lower() for kw in italic_keywords) and abs(c.get("size", 0) - self.keywords_font_size) < 1.5 ] if not italic_chars: all_text = first_page.get("raw_text", "") if not all_text: all_text = "".join( c["text"] for line in first_page.get("lines", []) for c in line.get("chars", []) ) has_keywords = any( marker in all_text.lower() for marker in ["ключевые слова", "keywords"] ) if has_keywords and not italic_chars: pass except Exception: pass return violations def _check_citation_sample(self, doc: dict) -> list: #Пункт 12: Проверка образца цитирования. violations = [] try: pages = doc.get("pages", []) if not pages: return violations # Собираем текст СО ВСЕХ страниц (два способа) all_text = "" # Способ 1: из chars (посимвольно) for page in pages: for line in page.get("lines", []): line_text = "".join(c["text"] for c in line.get("chars", [])) all_text += line_text + " " # Способ 2: из raw_text, если есть for page in pages: raw = page.get("raw_text", "") if raw: all_text += raw + " " # Ищем маркеры образца цитирования markers = [ "образец цитирования", "ОБРАЗЕЦ ЦИТИРОВАНИЯ", "для цитирования", "Для цитирования", "for citation", "FOR CITATION", "For citation", ] has_citation = any(marker in all_text for marker in markers) if not has_citation: violations.append(Violation( severity="high", category="Оформление", description="Отсутствует образец цитирования статьи", suggestion="Добавьте образец цитирования согласно требованиям журнала" )) except Exception: pass return violations def _check_main_text_format(self, doc: dict) -> list: # Пункт 13: Проверка основного текста (11 пт, красная строка 7 мм). violations = [] try: pages = doc.get("pages", []) # Проверяем несколько страниц в середине документа check_pages = pages[1:min(len(pages), 4)] if len(pages) > 1 else pages for i, page in enumerate(check_pages): chars = page.get("chars", []) # Основной текст — самый частый размер шрифта sizes = {} for c in chars: sz = round(c["size"]) sizes[sz] = sizes.get(sz, 0) + 1 if sizes: most_common_size = max(sizes, key=sizes.get) if abs(most_common_size - self.main_text_font_size) > 1: violations.append(Violation( severity="high", category="Оформление", description=f"Основной текст: размер шрифта {most_common_size} пт " f"(требуется {self.main_text_font_size} пт)", suggestion=f"Установите размер шрифта основного текста {self.main_text_font_size} пт" )) break # Достаточно одного замечания except Exception: pass return violations def _check_headings_general(self, doc: dict) -> list: #Пункт 14: Общие правила оформления заголовков. violations = [] try: # Проверка на завершающую точку в заголовках pages = doc.get("pages", []) for page in pages: for line in page.get("lines", []): text = "".join(c["text"] for c in line.get("chars", [])) chars = line.get("chars", []) if chars: font_size = chars[0]["size"] # Если это крупный шрифт (заголовок) и заканчивается точкой if font_size >= 12 and text.strip().endswith("."): if len(text.strip()) < 80: # Это заголовок, а не абзац violations.append(Violation( severity="low", category="Оформление", description=f"В названии заголовка '{text.strip()[:50]}' " f"не должна стоять завершающая точка", suggestion="Удалите точку в конце заголовка" )) except Exception: pass return violations def _check_heading1_format(self, doc: dict) -> list: violations = [] try: bold_fonts = ["bold", "cmbx", "sfbx", "bx", "heavy"] pages = doc.get("pages", []) found_issue = False for page_num, page in enumerate(pages): if found_issue: break for line in page.get("lines", []): chars = line.get("chars", []) if not chars: continue font_size = chars[0]["size"] if abs(font_size - self.heading1_font_size) < 1: font_name = chars[0].get("fontname", "").lower() is_bold = any(bf in font_name for bf in bold_fonts) if not is_bold: violations.append(Violation( severity="medium", category="Оформление", description=f"Стр. {page_num + 1}: заголовки первого уровня должны быть полужирными", suggestion="Установите полужирное начертание для заголовков 1 уровня" )) found_issue = True break #одно замечание на все заголовки except Exception: pass return violations def _check_heading2_format(self, doc: dict) -> list: #Пункт 16: Заголовок второго уровня (12 пт, полужирный). violations = [] try: pages = doc.get("pages", []) for page in pages: for line in page.get("lines", []): chars = line.get("chars", []) if not chars: continue font_size = chars[0]["size"] # Заголовки 2 уровня: 12 пт if abs(font_size - self.heading2_font_size) < 1: font_name = chars[0].get("fontname", "").lower() if "bold" not in font_name and "italic" not in font_name: # 2 уровень должен быть полужирным, не курсивом pass # Трудно отличить 2 от 3 уровня только по шрифту except Exception: pass return violations def _check_heading3_format(self, doc: dict) -> list: #Пункт 17: Заголовок третьего уровня (12 пт, курсив). violations = [] try: pages = doc.get("pages", []) found_italic_heading = False for page in pages: for line in page.get("lines", []): chars = line.get("chars", []) if not chars: continue font_size = chars[0]["size"] font_name = chars[0].get("fontname", "").lower() if abs(font_size - self.heading3_font_size) < 1 and "italic" in font_name: found_italic_heading = True break # Если не нашли ни одного курсивного заголовка — не ошибка, # просто в статье может не быть 3 уровня except Exception: pass return violations def _check_code_font(self, doc: dict) -> list: #Пункт 18: Исходные тексты программ (Courier New, 10 пт). violations = [] try: pages = doc.get("pages", []) for page in pages: for line in page.get("lines", []): chars = line.get("chars", []) if not chars: continue # Courier обычно имеет фиксированную ширину символов font_name = chars[0].get("fontname", "").lower() if "courier" in font_name: font_size = chars[0]["size"] if abs(font_size - 10) > 1: violations.append(Violation( severity="medium", category="Оформление", description="Исходные тексты программ должны быть оформлены шрифтом " "Courier New размером 10 пт", suggestion="Используйте Courier New 10 пт для листингов" )) break except Exception: pass return violations def _check_formulas(self, doc: dict) -> list: #Пункт 19: Проверка оформления формул. violations = [] try: pages = doc.get("pages", []) for page in pages: for line in page.get("lines", []): chars = line.get("chars", []) if not chars: continue text = "".join(c["text"] for c in chars) # Проверяем нумерацию формул (номер в круглых скобках справа) if re.search(r'\(\d+\)\s*$', text): # Проверяем, что номер выровнен по правому краю last_char = chars[-1] rightmost = last_char["x1"] page_width = page["width"] if abs(rightmost - (page_width - self.margin_mm / 0.3528)) > 20: violations.append(Violation( severity="low", category="Оформление", description="Номер формулы должен быть выровнен по правому краю", suggestion="Выровняйте номер формулы по правому краю" )) except Exception: pass return violations def _check_figures(self, doc: dict) -> list: #Пункт 20: Проверка оформления рисунков. violations = [] try: pages = doc.get("pages", []) all_text = " ".join( "".join(c["text"] for c in line.get("chars", [])) for page in pages for line in page.get("lines", []) ) # Проверяем наличие подписей к рисункам if "рис." in all_text.lower() or "figure" in all_text.lower(): # Проверяем, используется ли полужирный шрифт для "Рис." # Это сложно проверить только по символам pass except Exception: pass return violations def _check_graphs(self, doc: dict) -> list: #Пункт 21: Проверка графиков. # Графики трудно проверить автоматически — большинство требований к содержанию, # а не к форматированию. Пропускаем. return [] def _check_tables(self, doc: dict) -> list: #Пункт 22: Проверка оформления таблиц. violations = [] try: pages = doc.get("pages", []) all_text = " ".join( "".join(c["text"] for c in line.get("chars", [])) for page in pages for line in page.get("lines", []) ) # Проверяем наличие подписей к таблицам if "табл." in all_text.lower() or "таблица" in all_text.lower() or "table" in all_text.lower(): # Проверка выравнивания подписи # (сложно автоматически без полного анализа структуры) pass except Exception: pass return violations def _check_footnotes(self, doc: dict) -> list: #Пункт 23: Проверка сносок (10 пт, внизу страницы). violations = [] try: pages = doc.get("pages", []) has_issue = False for page in pages: chars = page.get("chars", []) bottom_chars = [c for c in chars if c["top"] > page["height"] * 0.85] if bottom_chars: sizes = [c["size"] for c in bottom_chars] avg_size = sum(sizes) / len(sizes) if abs(avg_size - self.footnote_font_size) > 2: has_issue = True break if has_issue: violations.append(Violation( severity="low", category="Оформление", description=f"Сноски должны быть оформлены шрифтом {self.footnote_font_size} пт", suggestion=f"Установите размер шрифта сносок {self.footnote_font_size} пт" )) except Exception: pass return violations def _check_cross_references(self, doc: dict) -> list: #Пункт 24: Проверка перекрестных ссылок. violations = [] try: pages = doc.get("pages", []) for page in pages: for line in page.get("lines", []): text = "".join(c["text"] for c in line.get("chars", [])) # Проверка формата ссылок # Ищем ссылки вроде "[1, 3, 5]" — правильный формат # Ищем "[1], [3], [5]" — неправильный формат if re.search(r'\]\s*,\s*\[', text): violations.append(Violation( severity="medium", category="Оформление", description="При перечислении перекрестных ссылок их все вместе " "следует заключать в квадратные скобки, " "а не каждую ссылку по отдельности", suggestion='Используйте формат "[1, 3, 5]" вместо "[1], [3], [5]"' )) break except Exception: pass return violations def _check_funding_info(self, doc: dict) -> list: #Пункт 25: Информация о финансовой поддержке. # Трудно проверить автоматически — это скорее содержательная проверка. return [] def _check_bibliography_format(self, doc: dict) -> list: violations = [] try: pages = doc.get("pages", []) last_pages = pages[-3:] found_issue = False # ← флаг for page in last_pages: if found_issue: break for line in page.get("lines", []): text = "".join(c["text"] for c in line.get("chars", [])) chars = line.get("chars", []) if chars and len(text.strip()) > 5: first_char = chars[0] if (first_char["text"].isdigit() and len(text.strip()) > 10 and ("." in text[:5] or "," in text[:5] or "автор" in text.lower() or "et al" in text.lower() or "изд" in text.lower() or "publish" in text.lower() or "http" in text.lower() or "doi" in text.lower())): left_margin = first_char["x0"] * 0.3528 if abs(left_margin - self.margin_mm) > 3: violations.append(Violation( severity="medium", category="Оформление", description="Абзацы списка литературы должны иметь нулевой отступ " "от левого края страницы", suggestion="Установите нулевой отступ для элементов списка литературы" )) found_issue = True break except Exception: pass return violations def _check_translation_part(self, doc: dict) -> list: #Пункты 28-35: Проверка переводной части. violations = [] try: pages = doc.get("pages", []) if not pages: return violations # Собираем текст СО ВСЕХ страниц all_text = "" for page in pages: for line in page.get("lines", []): line_text = "".join(c["text"] for c in line.get("chars", [])) all_text += line_text + " " # Добавляем raw_text raw = page.get("raw_text", "") if raw: all_text += raw + " " all_text_lower = all_text.lower() # Проверяем наличие английских маркеров в ЛЮБОЙ части документа english_markers = [ "abstract", "keywords", "introduction", "materials and methods", "experimental results", "conclusion", "acknowledgments", ] found_english = any(marker in all_text_lower for marker in english_markers) # Дополнительно: ищем признак переводной части — # дублирование названия на английском (все заглавные буквы + длинная строка) english_title_pattern = re.findall( r'[A-Z][A-Z\s\-]{20,}', all_text ) has_english_title = len(english_title_pattern) > 0 if not found_english and not has_english_title: violations.append(Violation( severity="high", category="Оформление", description="Отсутствует переводная часть статьи на английский язык", suggestion="Добавьте перевод названия, аннотации, ключевых слов и списка литературы" )) # Проверка наличия "References" (переводной список литературы) if "references" not in all_text_lower: violations.append(Violation( severity="medium", category="Оформление", description="Отсутствует переводной список литературы (References)", suggestion="Добавьте список литературы на английском языке" )) except Exception: pass return violations def _check_other_rules(self, doc: dict) -> list: violations = [] try: pages = doc.get("pages", []) found_quotes = False found_yo = False for page_num, page in enumerate(pages): for line in page.get("lines", []): text = "".join(c["text"] for c in line.get("chars", [])) if not found_quotes: if re.search(r'(?:[а-яё\s])"(?:[а-яё\s])', text, re.IGNORECASE): violations.append(Violation( severity="medium", category="Оформление", description=f"Стр. {page_num + 1}: в тексте нужно использовать кавычки « и » (вместо \" и \")", suggestion='Замените "кавычки" на «кавычки»' )) found_quotes = True if not found_yo and 'ё' in text.lower(): violations.append(Violation( severity="low", category="Оформление", description=f"Стр. {page_num + 1}: вместо \"ё\" везде следует использовать \"е\"", suggestion='Замените "ё" на "е"' )) found_yo = True if not page.get("lines"): raw = page.get("raw_text", "") lines = raw.split("\n") for text in lines: if not found_quotes and '"' in text and '«' not in text: violations.append(Violation( severity="medium", category="Оформление", description='В тексте на русском языке нужно использовать кавычки « и » (вместо " и ")', suggestion='Замените "кавычки" на «кавычки»' )) found_quotes = True if not found_yo and 'ё' in text.lower(): violations.append(Violation( severity="low", category="Оформление", description='В тексте вместо "ё" везде следует использовать "е" (кроме имен собственных)', suggestion='Замените "ё" на "е"' )) found_yo = True if found_quotes and found_yo: break except Exception: pass return violations