Files
hehaiguang1123 a6f05ab2d5 Phase 0-2: Schema cleanup, typed relations, event-driven automation
- Phase 0: AGENTS.md cleanup (dedup quotes, renumber sections, merge qmd)
- Phase 1: typed relations (manage-relations.py, graph-search.py, check-staleness.py, detect-conflicts.py)
- Phase 2: frontmatter validator, weekly lint, knowledge promotion, git hooks
- Fix .gitignore to track tools/ and .githooks/
- Fix git remote URL (remove plaintext token)
- New wiki pages: 504 pages, 34 raw sources
2026-07-01 08:05:43 +08:00

531 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文献库管理器 - 教育AI研究项目
功能:
1. 添加/删除/更新文献条目
2. 搜索和筛选文献
3. 生成统计报告
4. 导出引用列表(APA/MLA/Chicago/GB/T 7714
5. 维护索引和统计
6. DOI批量验证
用法示例:
python 文献库管理器.py --stats # 显示统计摘要
python 文献库管理器.py --search LLM # 搜索关键词
python 文献库管理器.py --list all # 列出所有文献
python 文献库管理器.py --export apa # 导出APA引用列表
python 文献库管理器.py --add-doi 10.xxx # 通过DOI添加文献
python 文献库管理器.py --verify # 验证所有DOI状态
作者:狗剩
创建时间:2026-04-04
更新时间:2026-04-05
"""
import json
import os
import re
import sys
import argparse
from typing import Dict, List, Optional, Any
from datetime import datetime
from pathlib import Path
class LiteratureManager:
"""文献库管理器类"""
def __init__(self, db_path: str = None):
if db_path is None:
current_dir = Path(__file__).parent.parent
db_path = current_dir / "文献索引数据库.json"
self.db_path = Path(db_path)
self.data = self._load_database()
# ─────────────────────── 数据库读写 ───────────────────────
def _load_database(self) -> Dict:
if self.db_path.exists():
try:
with open(self.db_path, 'r', encoding='utf-8') as f:
return json.load(f)
except json.JSONDecodeError:
print("⚠️ 数据库文件损坏,创建新数据库")
return self._create_empty_database()
else:
print(f"📝 数据库不存在,创建新数据库: {self.db_path}")
return self._create_empty_database()
def _create_empty_database(self) -> Dict:
return {
"metadata": {
"version": "1.1",
"created": datetime.now().strftime("%Y-%m-%d"),
"updated": datetime.now().strftime("%Y-%m-%d"),
"description": "教育AI研究项目文献索引数据库",
"total_entries": 0,
"last_entry_id": 0
},
"schema": {},
"entries": [],
"indexes": {
"by_year": {},
"by_journal": {},
"by_technology": {},
"by_education_level": {},
"by_reliability": {}
},
"statistics": {
"total_by_year": {},
"total_by_journal": {},
"total_by_technology": {},
"high_quality_entries": 0
}
}
def _save_database(self):
self.data["metadata"]["updated"] = datetime.now().strftime("%Y-%m-%d")
self.data["metadata"]["total_entries"] = len(self.data["entries"])
with open(self.db_path, 'w', encoding='utf-8') as f:
json.dump(self.data, f, ensure_ascii=False, indent=2)
# ─────────────────────── 添加文献 ───────────────────────
def add_entry(self, entry: Dict) -> int:
"""添加文献条目,返回新条目ID"""
last_id = self.data["metadata"].get("last_entry_id", 0)
new_id = last_id + 1
entry["entry_id"] = new_id
entry.setdefault("added_date", datetime.now().strftime("%Y-%m-%d"))
entry.setdefault("added_by", "狗剩")
self.data["entries"].append(entry)
self.data["metadata"]["last_entry_id"] = new_id
self._rebuild_indexes()
self._save_database()
print(f"✅ 文献已添加,ID: {new_id}{entry.get('title', '无标题')[:50]}")
return new_id
def add_from_doi(self, doi: str) -> Optional[int]:
"""通过DOI自动获取元数据并添加"""
try:
from doi_resolver import DOIResolver
except ImportError:
print("⚠️ 无法导入DOIResolver,请手动补充元数据")
entry = {
"doi": doi,
"title": f"[待补充] {doi}",
"authors": [],
"journal": "",
"year": None,
"keywords": [],
"url": f"https://doi.org/{doi}",
"research_type": "",
"education_level": "",
"ai_technology": "",
"quality_score": 0,
"reliability": "待验证",
"notes": "通过DOI添加,元数据待补充",
"tags": [],
"related_entries": []
}
return self.add_entry(entry)
resolver = DOIResolver()
metadata = resolver.resolve_doi(doi)
if not metadata:
print(f"❌ DOI解析失败: {doi}")
return None
entry = {
"doi": metadata.get("doi", doi),
"title": metadata.get("title", ""),
"authors": metadata.get("authors", []),
"journal": metadata.get("journal", ""),
"year": metadata.get("year"),
"volume": metadata.get("volume", ""),
"issue": metadata.get("issue", ""),
"pages": metadata.get("pages", ""),
"abstract": metadata.get("abstract", ""),
"keywords": [],
"url": metadata.get("url", f"https://doi.org/{doi}"),
"pdf_path": "",
"citation_count": metadata.get("citation_count", 0),
"research_type": "",
"education_level": "",
"ai_technology": "",
"quality_score": 80,
"reliability": "",
"notes": f"通过DOI自动导入,来源: {metadata.get('source', 'API')}",
"tags": [],
"related_entries": []
}
return self.add_entry(entry)
# ─────────────────────── 搜索 ───────────────────────
def search(self, query: str, fields: List[str] = None) -> List[Dict]:
"""全文搜索"""
if fields is None:
fields = ["title", "abstract", "keywords", "tags", "journal", "authors", "notes"]
query = query.lower()
results = []
for entry in self.data["entries"]:
matched = False
for field in fields:
val = entry.get(field, "")
if isinstance(val, list):
val = " ".join(str(v) for v in val)
if query in str(val).lower():
matched = True
break
if matched:
results.append(entry)
return results
def filter_by(self, **kwargs) -> List[Dict]:
"""按字段过滤,支持 year/journal/technology/reliability/education_level"""
results = self.data["entries"]
for key, value in kwargs.items():
results = [e for e in results if str(e.get(key, "")).lower() == str(value).lower()]
return results
def get_entry(self, entry_id: int) -> Optional[Dict]:
for e in self.data["entries"]:
if e.get("entry_id") == entry_id:
return e
return None
def get_unverified(self) -> List[Dict]:
"""获取所有待验证的条目"""
return [e for e in self.data["entries"] if e.get("reliability") == "待验证"]
# ─────────────────────── 引用格式 ───────────────────────
def format_citation(self, entry: Dict, style: str = "apa") -> str:
"""生成引用格式字符串"""
authors = entry.get("authors", [])
title = entry.get("title", "Unknown Title")
journal = entry.get("journal", "")
year = entry.get("year", "n.d.")
volume = entry.get("volume", "")
issue = entry.get("issue", "")
pages = entry.get("pages", "")
doi = entry.get("doi", "")
style = style.lower()
if style == "apa":
if len(authors) == 0:
a = "[Author unknown]"
elif len(authors) == 1:
a = authors[0].split(",")[0]
elif len(authors) == 2:
a = f"{authors[0].split(',')[0]} & {authors[1].split(',')[0]}"
else:
a = f"{authors[0].split(',')[0]} et al."
c = f"{a} ({year}). {title}."
if journal:
c += f" *{journal}*"
if volume:
c += f", *{volume}*"
if issue:
c += f"({issue})"
if pages:
c += f", {pages}"
c += "."
if doi and "XXX" not in doi:
c += f" https://doi.org/{doi}"
return c
elif style == "mla":
a = authors[0] if authors else "Unknown"
c = f'{a}. "{title}." *{journal}*'
if volume:
c += f", vol. {volume}"
if issue:
c += f", no. {issue}"
c += f", {year}"
if pages:
c += f", pp. {pages}"
c += "."
if doi and "XXX" not in doi:
c += f" doi:{doi}."
return c
elif style == "chicago":
a = authors[0] if authors else "Unknown"
c = f'{a}. "{title}." *{journal}*'
if volume:
c += f" {volume}"
if issue:
c += f", no. {issue}"
c += f" ({year})"
if pages:
c += f": {pages}"
c += "."
if doi and "XXX" not in doi:
c += f" https://doi.org/{doi}."
return c
elif style in ("gb", "gb/t 7714", "gbt"):
if len(authors) == 0:
a = "佚名"
elif len(authors) > 3:
a = f"{authors[0]}"
else:
a = ", ".join(authors)
c = f"{a}. {title}[J]. {journal}"
if year:
c += f", {year}"
if volume:
c += f", {volume}"
if issue:
c += f"({issue})"
if pages:
c += f": {pages}"
c += "."
if doi and "XXX" not in doi:
c += f" DOI: {doi}."
return c
return f"[{entry.get('entry_id')}] {title} ({year}). {journal}."
# ─────────────────────── 导出 ───────────────────────
def export_citations(self, style: str = "apa", filter_verified: bool = False,
output_path: str = None) -> str:
"""导出所有文献的引用列表"""
entries = self.data["entries"]
if filter_verified:
entries = [e for e in entries if e.get("reliability") == ""]
lines = [f"# 教育AI研究 — 参考文献列表({style.upper()}格式)",
f"生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}",
f"总计:{len(entries)} 条({'仅高可靠' if filter_verified else '全部'}",
""]
for entry in sorted(entries, key=lambda e: (e.get("year") or 0), reverse=True):
citation = self.format_citation(entry, style)
note = ""
if entry.get("reliability") == "待验证":
note = " ⚠️[待验证]"
lines.append(f"- {citation}{note}")
text = "\n".join(lines)
if output_path:
with open(output_path, 'w', encoding='utf-8') as f:
f.write(text)
print(f"✅ 引用列表已导出: {output_path}")
else:
print(text)
return text
# ─────────────────────── 统计 ───────────────────────
def print_stats(self):
"""打印统计摘要"""
entries = self.data["entries"]
total = len(entries)
verified = sum(1 for e in entries if e.get("reliability") == "")
need_verify = sum(1 for e in entries if e.get("reliability") == "待验证")
with_doi = sum(1 for e in entries if e.get("doi") and "XXX" not in e.get("doi", ""))
high_q = sum(1 for e in entries if (e.get("quality_score") or 0) >= 85)
print("\n" + "=" * 55)
print(" 📚 教育AI研究文献库 — 统计摘要")
print("=" * 55)
print(f" 总文献数: {total}")
print(f" 高可靠来源: {verified} ({verified*100//total if total else 0}%)")
print(f" 待验证: {need_verify}")
print(f" 有效DOI {with_doi}")
print(f" 高质量(≥85) {high_q}")
print()
# 按技术分类
tech_counts = {}
for e in entries:
t = e.get("ai_technology", "其他")
if not t:
t = "其他"
for tag in t.split("/"):
tag = tag.strip()
tech_counts[tag] = tech_counts.get(tag, 0) + 1
print(" 技术分类:")
for t, c in sorted(tech_counts.items(), key=lambda x: -x[1]):
print(f" {t:<20} {c}")
print()
# 按年份
year_counts = {}
for e in entries:
y = str(e.get("year", "未知"))
year_counts[y] = year_counts.get(y, 0) + 1
print(" 年份分布:")
for y, c in sorted(year_counts.items(), reverse=True):
bar = "" * c
print(f" {y} {bar} ({c})")
print("=" * 55 + "\n")
# ─────────────────────── 列表 ───────────────────────
def print_list(self, entries: List[Dict] = None, verbose: bool = False):
"""打印文献列表"""
if entries is None:
entries = self.data["entries"]
if not entries:
print(" (无结果)")
return
for e in entries:
flag = "" if e.get("reliability") == "" else "⚠️"
doi_note = ""
if e.get("doi") and "XXX" not in e.get("doi", ""):
doi_note = f" DOI: {e['doi']}"
print(f"\n [{e.get('entry_id')}] {flag} {e.get('title', '无标题')[:60]}")
print(f" {e.get('journal', '')} ({e.get('year', '?')}){doi_note}")
if verbose:
if e.get("tags"):
print(f" 标签: {', '.join(e['tags'][:5])}")
if e.get("notes"):
print(f" 备注: {e['notes'][:80]}")
# ─────────────────────── 验证 ───────────────────────
def verify_dois(self):
"""列出所有DOI状态"""
print("\n📋 DOI 验证状态:\n")
for e in self.data["entries"]:
doi = e.get("doi", "")
title = e.get("title", "")[:45]
if not doi:
status = "❌ 无DOI"
elif "XXX" in doi:
status = "⚠️ DOI不完整(含XXX占位)"
elif re.match(r'^10\.\d{4,}/.+$', doi):
status = "✅ DOI格式有效"
else:
status = "❓ 格式异常"
print(f" [{e.get('entry_id'):>2}] {status:<28} {title}")
# ─────────────────────── 索引 ───────────────────────
def _rebuild_indexes(self):
indexes = {
"by_year": {},
"by_journal": {},
"by_technology": {},
"by_education_level": {},
"by_reliability": {}
}
for e in self.data["entries"]:
eid = e.get("entry_id")
y = str(e.get("year", "未知"))
indexes["by_year"].setdefault(y, []).append(eid)
j = e.get("journal", "未知")
indexes["by_journal"].setdefault(j, []).append(eid)
for t in (e.get("ai_technology") or "其他").split("/"):
t = t.strip()
indexes["by_technology"].setdefault(t, []).append(eid)
lv = e.get("education_level", "未知")
indexes["by_education_level"].setdefault(lv, []).append(eid)
rel = e.get("reliability", "待验证")
indexes["by_reliability"].setdefault(rel, []).append(eid)
self.data["indexes"] = indexes
# ─────────────────────── CLI ───────────────────────
def main():
parser = argparse.ArgumentParser(
description="教育AI研究文献库管理器",
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument("--stats", action="store_true", help="显示统计摘要")
parser.add_argument("--list", metavar="FILTER",
help="列出文献(all / verified / unverified / <年份>")
parser.add_argument("--verbose", "-v", action="store_true", help="显示详细信息")
parser.add_argument("--search", metavar="QUERY", help="搜索文献")
parser.add_argument("--export", metavar="STYLE",
help="导出引用列表(apa / mla / chicago / gb")
parser.add_argument("--export-verified", action="store_true", help="仅导出高可靠文献")
parser.add_argument("--output", metavar="FILE", help="导出到文件")
parser.add_argument("--add-doi", metavar="DOI", help="通过DOI添加文献")
parser.add_argument("--verify", action="store_true", help="验证所有DOI状态")
parser.add_argument("--show", metavar="ID", type=int, help="显示指定ID的文献详情")
parser.add_argument("--db", metavar="PATH", help="指定数据库路径")
args = parser.parse_args()
# 初始化管理器
db_path = args.db if args.db else None
mgr = LiteratureManager(db_path)
if args.stats:
mgr.print_stats()
elif args.list:
if args.list == "all":
entries = mgr.data["entries"]
elif args.list == "verified":
entries = [e for e in mgr.data["entries"] if e.get("reliability") == ""]
elif args.list == "unverified":
entries = mgr.get_unverified()
else:
entries = mgr.filter_by(year=args.list)
print(f"\n{len(entries)} 条:")
mgr.print_list(entries, verbose=args.verbose)
elif args.search:
results = mgr.search(args.search)
print(f"\n搜索 '{args.search}' 找到 {len(results)} 条:")
mgr.print_list(results, verbose=args.verbose)
elif args.export:
mgr.export_citations(
style=args.export,
filter_verified=args.export_verified,
output_path=args.output
)
elif args.add_doi:
mgr.add_from_doi(args.add_doi)
elif args.verify:
mgr.verify_dois()
elif args.show:
entry = mgr.get_entry(args.show)
if entry:
print(json.dumps(entry, ensure_ascii=False, indent=2))
else:
print(f"❌ 未找到ID: {args.show}")
else:
# 默认:显示统计
mgr.print_stats()
print("提示:使用 --help 查看所有命令")
if __name__ == "__main__":
main()