#!/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}")