diff options
Diffstat (limited to '.agents/skills/citation-verification/scripts/format-checker.py')
| -rw-r--r-- | .agents/skills/citation-verification/scripts/format-checker.py | 605 |
1 files changed, 605 insertions, 0 deletions
diff --git a/.agents/skills/citation-verification/scripts/format-checker.py b/.agents/skills/citation-verification/scripts/format-checker.py new file mode 100644 index 0000000..c222702 --- /dev/null +++ b/.agents/skills/citation-verification/scripts/format-checker.py @@ -0,0 +1,605 @@ +#!/usr/bin/env python3 +""" +BibTeX and LaTeX Format Checker + +独立的格式检查工具,用于验证 BibTeX 和 LaTeX 引用格式。 + +功能: +1. BibTeX 格式检查 - 验证条目结构、必填字段、字段格式 +2. LaTeX 引用检查 - 提取引用、检查一致性 +3. 快速格式验证 - 无需 API 调用的快速检查 + +使用方法: + python format-checker.py references.bib + python format-checker.py paper.tex --check-latex + python format-checker.py references.bib --strict +""" + +import argparse +import sys +import re +from pathlib import Path +from typing import Dict, List, Tuple, Optional +from dataclasses import dataclass +from enum import Enum + +# 尝试导入 bibtexparser +try: + import bibtexparser + from bibtexparser.bparser import BibTexParser + BIBTEX_AVAILABLE = True +except ImportError: + print("警告: bibtexparser 未安装,BibTeX 解析功能受限") + print("运行: pip install bibtexparser") + BIBTEX_AVAILABLE = False + + +class ErrorLevel(Enum): + """错误级别""" + ERROR = "error" # 严重错误,必须修复 + WARNING = "warning" # 警告,建议修复 + INFO = "info" # 信息,可选修复 + + +@dataclass +class FormatError: + """格式错误数据类""" + level: ErrorLevel + location: str # 文件位置 (如 "entry:smith2020" 或 "line:42") + field: Optional[str] # 字段名 (如 "author", "year") + message: str # 错误描述 + suggestion: Optional[str] = None # 修复建议 + + +def parse_arguments(): + """解析命令行参数""" + parser = argparse.ArgumentParser( + description='检查 BibTeX 和 LaTeX 引用格式', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + %(prog)s references.bib + %(prog)s paper.tex --check-latex + %(prog)s references.bib --strict --output report.txt + %(prog)s references.bib --fix-common + """ + ) + + parser.add_argument( + 'input_file', + type=str, + help='BibTeX 文件(.bib)或 LaTeX 文件(.tex)' + ) + + parser.add_argument( + '--check-latex', + action='store_true', + help='检查 LaTeX 引用(需要提供 .tex 文件)' + ) + + parser.add_argument( + '--strict', + action='store_true', + help='严格模式 - 将警告视为错误' + ) + + parser.add_argument( + '--output', + type=str, + help='输出报告文件路径' + ) + + parser.add_argument( + '--fix-common', + action='store_true', + help='自动修复常见格式问题' + ) + + parser.add_argument( + '--verbose', + action='store_true', + help='显示详细信息' + ) + + parser.add_argument( + '--entry-type', + type=str, + help='只检查特定类型的条目(如 article, inproceedings)' + ) + + return parser.parse_args() + + +def load_bibtex_file(file_path: str) -> List[Dict]: + """加载 BibTeX 文件 + + Args: + file_path: BibTeX 文件路径 + + Returns: + BibTeX 条目列表 + + Raises: + FileNotFoundError: 文件不存在 + ValueError: 文件格式错误 + """ + if not BIBTEX_AVAILABLE: + raise ImportError("需要安装 bibtexparser: pip install bibtexparser") + + try: + with open(file_path, 'r', encoding='utf-8') as f: + parser = BibTexParser(common_strings=True) + bib_database = bibtexparser.load(f, parser) + return bib_database.entries + except FileNotFoundError: + raise FileNotFoundError(f"文件不存在: {file_path}") + except Exception as e: + raise ValueError(f"无法解析 BibTeX 文件: {e}") + + +def load_latex_file(file_path: str) -> str: + """加载 LaTeX 文件 + + Args: + file_path: LaTeX 文件路径 + + Returns: + 文件内容 + + Raises: + FileNotFoundError: 文件不存在 + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + return f.read() + except FileNotFoundError: + raise FileNotFoundError(f"文件不存在: {file_path}") + except Exception as e: + raise ValueError(f"无法读取 LaTeX 文件: {e}") + + +# ============================================================================ +# BibTeX 格式检查函数 +# ============================================================================ + +def get_required_fields(entry_type: str) -> List[str]: + """获取 BibTeX 条目类型的必填字段 + + Args: + entry_type: 条目类型 (如 'article', 'inproceedings') + + Returns: + 必填字段列表 + """ + required_fields = { + 'article': ['author', 'title', 'journal', 'year'], + 'inproceedings': ['author', 'title', 'booktitle', 'year'], + 'book': ['title', 'publisher', 'year'], + 'incollection': ['author', 'title', 'booktitle', 'publisher', 'year'], + 'inbook': ['author', 'title', 'chapter', 'publisher', 'year'], + 'proceedings': ['title', 'year'], + 'phdthesis': ['author', 'title', 'school', 'year'], + 'mastersthesis': ['author', 'title', 'school', 'year'], + 'techreport': ['author', 'title', 'institution', 'year'], + 'manual': ['title'], + 'misc': ['title'], + 'unpublished': ['author', 'title', 'note'], + } + return required_fields.get(entry_type.lower(), ['title']) + + +def get_optional_fields(entry_type: str) -> List[str]: + """获取 BibTeX 条目类型的可选字段 + + Args: + entry_type: 条目类型 + + Returns: + 可选字段列表 + """ + optional_fields = { + 'article': ['volume', 'number', 'pages', 'month', 'doi', 'url'], + 'inproceedings': ['editor', 'volume', 'series', 'pages', 'address', + 'month', 'organization', 'publisher', 'doi', 'url'], + 'book': ['author', 'editor', 'volume', 'series', 'address', + 'edition', 'month', 'isbn', 'doi', 'url'], + } + return optional_fields.get(entry_type.lower(), []) + + +def check_entry_structure(entry: Dict) -> List[FormatError]: + """检查 BibTeX 条目基本结构 + + Args: + entry: BibTeX 条目字典 + + Returns: + 错误列表 + """ + errors = [] + + # 检查条目类型 + if 'ENTRYTYPE' not in entry: + errors.append(FormatError( + level=ErrorLevel.ERROR, + location=f"entry:{entry.get('ID', 'unknown')}", + field='ENTRYTYPE', + message="缺少条目类型", + suggestion="添加条目类型,如 @article, @inproceedings" + )) + return errors + + # 检查 ID + if 'ID' not in entry or not entry['ID'].strip(): + errors.append(FormatError( + level=ErrorLevel.ERROR, + location="entry:unknown", + field='ID', + message="缺少 citation key", + suggestion="添加唯一的 citation key" + )) + + # 检查必填字段 + entry_type = entry.get('ENTRYTYPE', '') + required = get_required_fields(entry_type) + for field in required: + if field not in entry or not entry[field].strip(): + errors.append(FormatError( + level=ErrorLevel.ERROR, + location=f"entry:{entry.get('ID', 'unknown')}", + field=field, + message=f"缺少必填字段: {field}", + suggestion=f"添加 {field} 字段" + )) + + return errors + + +def check_field_formats(entry: Dict) -> List[FormatError]: + """检查字段格式 + + Args: + entry: BibTeX 条目字典 + + Returns: + 错误列表 + """ + errors = [] + entry_id = entry.get('ID', 'unknown') + + # 年份格式检查 + if 'year' in entry: + year = entry['year'].strip() + if not year.isdigit(): + errors.append(FormatError( + level=ErrorLevel.ERROR, + location=f"entry:{entry_id}", + field='year', + message=f"年份格式错误: {year} (应为4位数字)", + suggestion="使用4位数字年份,如 2023" + )) + elif len(year) != 4: + errors.append(FormatError( + level=ErrorLevel.ERROR, + location=f"entry:{entry_id}", + field='year', + message=f"年份格式错误: {year} (应为4位数字)", + suggestion="使用4位数字年份,如 2023" + )) + else: + year_int = int(year) + if year_int < 1900 or year_int > 2030: + errors.append(FormatError( + level=ErrorLevel.WARNING, + location=f"entry:{entry_id}", + field='year', + message=f"年份超出合理范围: {year}", + suggestion="检查年份是否正确" + )) + + # DOI 格式检查 + if 'doi' in entry: + doi = entry['doi'].strip() + if not doi.startswith('10.'): + errors.append(FormatError( + level=ErrorLevel.ERROR, + location=f"entry:{entry_id}", + field='doi', + message=f"DOI 格式错误: {doi}", + suggestion="DOI 应以 '10.' 开头,如 10.1038/nature12345" + )) + # 检查是否包含 URL 前缀 + if 'doi.org' in doi or 'dx.doi.org' in doi: + errors.append(FormatError( + level=ErrorLevel.WARNING, + location=f"entry:{entry_id}", + field='doi', + message=f"DOI 包含 URL 前缀: {doi}", + suggestion="只保留 DOI 本身,移除 https://doi.org/ 前缀" + )) + + # 作者名格式检查 + if 'author' in entry: + author = entry['author'].strip() + # 检查是否为空 + if not author: + errors.append(FormatError( + level=ErrorLevel.ERROR, + location=f"entry:{entry_id}", + field='author', + message="作者字段为空", + suggestion="添加作者信息" + )) + # 检查格式一致性 + elif ' and ' in author: + authors = author.split(' and ') + formats = [] + for a in authors: + if ',' in a: + formats.append('last_first') # "Last, First" + else: + formats.append('first_last') # "First Last" + + if len(set(formats)) > 1: + errors.append(FormatError( + level=ErrorLevel.WARNING, + location=f"entry:{entry_id}", + field='author', + message="作者名格式不一致", + suggestion="统一使用 'Last, First' 或 'First Last' 格式" + )) + + # 页码格式检查 + if 'pages' in entry: + pages = entry['pages'].strip() + # 检查是否使用了正确的分隔符 + if '-' in pages and '--' not in pages: + errors.append(FormatError( + level=ErrorLevel.INFO, + location=f"entry:{entry_id}", + field='pages', + message=f"页码使用单连字符: {pages}", + suggestion="建议使用双连字符 '--',如 123--145" + )) + + # URL 格式检查 + if 'url' in entry: + url = entry['url'].strip() + if not url.startswith(('http://', 'https://')): + errors.append(FormatError( + level=ErrorLevel.WARNING, + location=f"entry:{entry_id}", + field='url', + message=f"URL 缺少协议前缀: {url}", + suggestion="添加 http:// 或 https:// 前缀" + )) + + return errors + + +def check_consistency(entries: List[Dict]) -> List[FormatError]: + """检查条目间的一致性 + + Args: + entries: BibTeX 条目列表 + + Returns: + 错误列表 + """ + errors = [] + + # 检查重复的 citation key + ids = [e.get('ID', '') for e in entries] + duplicates = [id for id in ids if ids.count(id) > 1] + if duplicates: + for dup_id in set(duplicates): + errors.append(FormatError( + level=ErrorLevel.ERROR, + location=f"entry:{dup_id}", + field='ID', + message=f"重复的 citation key: {dup_id}", + suggestion="使用唯一的 citation key" + )) + + # 检查作者名格式一致性 + author_formats = {} + for entry in entries: + if 'author' in entry and ' and ' in entry['author']: + entry_id = entry.get('ID', 'unknown') + authors = entry['author'].split(' and ') + for author in authors: + if ',' in author: + author_formats[entry_id] = 'last_first' + else: + author_formats[entry_id] = 'first_last' + break + + if len(set(author_formats.values())) > 1: + errors.append(FormatError( + level=ErrorLevel.WARNING, + location="global", + field='author', + message="不同条目使用了不同的作者名格式", + suggestion="统一使用 'Last, First' 或 'First Last' 格式" + )) + + return errors + + +# ============================================================================ +# LaTeX 引用检查函数 +# ============================================================================ + +def extract_latex_citations(tex_content: str) -> List[str]: + """从 LaTeX 文件中提取引用 + + Args: + tex_content: LaTeX 文件内容 + + Returns: + 引用 key 列表 + """ + # 匹配 \cite{...} 命令 + cite_pattern = r'\\cite(?:\[[^\]]*\])?(?:\[[^\]]*\])?\{([^}]+)\}' + citations = re.findall(cite_pattern, tex_content) + + # 展开多个引用 + all_keys = [] + for cite in citations: + keys = [k.strip() for k in cite.split(',')] + all_keys.extend(keys) + + return list(set(all_keys)) # 去重 + + +def check_latex_consistency(tex_keys: List[str], bib_keys: List[str]) -> List[FormatError]: + """检查 LaTeX 引用与 BibTeX 的一致性 + + Args: + tex_keys: LaTeX 中的引用 key 列表 + bib_keys: BibTeX 中的 key 列表 + + Returns: + 错误列表 + """ + errors = [] + + tex_set = set(tex_keys) + bib_set = set(bib_keys) + + # 未定义的引用 + undefined = tex_set - bib_set + if undefined: + for key in sorted(undefined): + errors.append(FormatError( + level=ErrorLevel.ERROR, + location=f"latex:cite", + field=key, + message=f"未定义的引用: {key}", + suggestion=f"在 BibTeX 文件中添加 {key} 条目" + )) + + # 未使用的引用 + unused = bib_set - tex_set + if unused: + for key in sorted(unused): + errors.append(FormatError( + level=ErrorLevel.WARNING, + location=f"bibtex:entry", + field=key, + message=f"未使用的引用: {key}", + suggestion=f"在 LaTeX 文件中引用 {key} 或从 BibTeX 中删除" + )) + + return errors + + +# ============================================================================ +# 报告生成函数 +# ============================================================================ + +def print_errors(errors: List[FormatError], verbose: bool = False): + """打印错误列表 + + Args: + errors: 错误列表 + verbose: 是否显示详细信息 + """ + if not errors: + print("✅ 未发现格式错误") + return + + # 按级别分组 + errors_by_level = { + ErrorLevel.ERROR: [], + ErrorLevel.WARNING: [], + ErrorLevel.INFO: [] + } + + for error in errors: + errors_by_level[error.level].append(error) + + # 打印统计 + print("\n" + "="*60) + print("格式检查结果") + print("="*60) + print(f"❌ 错误: {len(errors_by_level[ErrorLevel.ERROR])}") + print(f"⚠️ 警告: {len(errors_by_level[ErrorLevel.WARNING])}") + print(f"ℹ️ 信息: {len(errors_by_level[ErrorLevel.INFO])}") + print("="*60) + + # 打印详细错误 + for level in [ErrorLevel.ERROR, ErrorLevel.WARNING, ErrorLevel.INFO]: + level_errors = errors_by_level[level] + if not level_errors: + continue + + level_symbol = { + ErrorLevel.ERROR: "❌", + ErrorLevel.WARNING: "⚠️", + ErrorLevel.INFO: "ℹ️" + }[level] + + print(f"\n{level_symbol} {level.value.upper()} ({len(level_errors)}):\n") + + for error in level_errors: + print(f" [{error.location}]", end="") + if error.field: + print(f" {error.field}:", end="") + print(f" {error.message}") + + if verbose and error.suggestion: + print(f" 💡 建议: {error.suggestion}") + print() + + +def generate_report(errors: List[FormatError], output_file: str): + """生成文本格式的检查报告 + + Args: + errors: 错误列表 + output_file: 输出文件路径 + """ + # 按级别分组 + errors_by_level = { + ErrorLevel.ERROR: [], + ErrorLevel.WARNING: [], + ErrorLevel.INFO: [] + } + + for error in errors: + errors_by_level[error.level].append(error) + + with open(output_file, 'w', encoding='utf-8') as f: + f.write("# BibTeX/LaTeX 格式检查报告\n\n") + + # 总体统计 + f.write("## 总体统计\n\n") + f.write(f"- **错误**: {len(errors_by_level[ErrorLevel.ERROR])}\n") + f.write(f"- **警告**: {len(errors_by_level[ErrorLevel.WARNING])}\n") + f.write(f"- **信息**: {len(errors_by_level[ErrorLevel.INFO])}\n\n") + + # 详细错误 + for level in [ErrorLevel.ERROR, ErrorLevel.WARNING, ErrorLevel.INFO]: + level_errors = errors_by_level[level] + if not level_errors: + continue + + level_name = { + ErrorLevel.ERROR: "错误", + ErrorLevel.WARNING: "警告", + ErrorLevel.INFO: "信息" + }[level] + + f.write(f"## {level_name} ({len(level_errors)})\n\n") + + for error in level_errors: + f.write(f"### [{error.location}]") + if error.field: + f.write(f" {error.field}") + f.write("\n\n") + f.write(f"**问题**: {error.message}\n\n") + if error.suggestion: + f.write(f"**建议**: {error.suggestion}\n\n") + + print(f"\n报告已保存到: {output_file}") |
