aboutsummaryrefslogtreecommitdiffstats
path: root/.agents/skills/citation-verification/scripts/format-checker.py
diff options
context:
space:
mode:
authorsillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
committersillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
commit898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch)
tree85c6ee5ad58b860144551184d4cf86b560c62b91 /.agents/skills/citation-verification/scripts/format-checker.py
downloadwww-main.tar.gz
www-main.zip
import live www.sillylaird.ca webrootHEADmain
Diffstat (limited to '.agents/skills/citation-verification/scripts/format-checker.py')
-rw-r--r--.agents/skills/citation-verification/scripts/format-checker.py605
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}")