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
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DOI解析工具 - 教育AI研究项目文献库
|
||||
功能:
|
||||
1. 解析DOI获取文献元数据
|
||||
2. 生成标准引用格式
|
||||
3. 验证DOI有效性
|
||||
4. 批量处理DOI列表
|
||||
|
||||
作者:狗剩
|
||||
创建时间:2026-04-04
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class DOIResolver:
|
||||
"""DOI解析器类"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = "https://doi.org/"
|
||||
self.crossref_api = "https://api.crossref.org/works/"
|
||||
self.datacite_api = "https://api.datacite.org/dois/"
|
||||
|
||||
def validate_doi(self, doi: str) -> bool:
|
||||
"""
|
||||
验证DOI格式是否有效
|
||||
|
||||
Args:
|
||||
doi: DOI字符串
|
||||
|
||||
Returns:
|
||||
bool: 是否有效
|
||||
"""
|
||||
if not doi:
|
||||
return False
|
||||
|
||||
# 清理DOI字符串
|
||||
doi = doi.strip()
|
||||
|
||||
# 移除可能的URL前缀
|
||||
if doi.startswith('http'):
|
||||
doi = doi.split('doi.org/')[-1]
|
||||
|
||||
# DOI正则表达式模式
|
||||
pattern = r'^10\.\d{4,}\/.+$'
|
||||
return bool(re.match(pattern, doi))
|
||||
|
||||
def clean_doi(self, doi: str) -> str:
|
||||
"""
|
||||
清理DOI字符串,提取纯DOI
|
||||
|
||||
Args:
|
||||
doi: 可能包含URL的DOI字符串
|
||||
|
||||
Returns:
|
||||
str: 纯DOI
|
||||
"""
|
||||
doi = doi.strip()
|
||||
|
||||
# 移除URL前缀
|
||||
if 'doi.org/' in doi:
|
||||
doi = doi.split('doi.org/')[-1]
|
||||
elif doi.startswith('http'):
|
||||
doi = doi.split('/')[-1]
|
||||
|
||||
return doi
|
||||
|
||||
def resolve_doi(self, doi: str, timeout: int = 10) -> Optional[Dict]:
|
||||
"""
|
||||
解析DOI获取文献元数据
|
||||
|
||||
Args:
|
||||
doi: DOI字符串
|
||||
timeout: 请求超时时间(秒)
|
||||
|
||||
Returns:
|
||||
Dict: 文献元数据,失败返回None
|
||||
"""
|
||||
doi = self.clean_doi(doi)
|
||||
|
||||
if not self.validate_doi(doi):
|
||||
print(f"❌ 无效的DOI格式: {doi}")
|
||||
return None
|
||||
|
||||
try:
|
||||
# 尝试Crossref API
|
||||
metadata = self._fetch_from_crossref(doi, timeout)
|
||||
if metadata:
|
||||
return metadata
|
||||
|
||||
# 尝试DataCite API
|
||||
metadata = self._fetch_from_datacite(doi, timeout)
|
||||
if metadata:
|
||||
return metadata
|
||||
|
||||
print(f"⚠️ 无法解析DOI: {doi}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 解析DOI时出错: {doi}, 错误: {str(e)}")
|
||||
return None
|
||||
|
||||
def _fetch_from_crossref(self, doi: str, timeout: int) -> Optional[Dict]:
|
||||
"""从Crossref API获取元数据"""
|
||||
try:
|
||||
url = f"{self.crossref_api}{doi}"
|
||||
headers = {
|
||||
'User-Agent': '教育AI研究项目文献库 (mailto:research@example.com)'
|
||||
}
|
||||
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
data = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
if data.get('status') == 'ok':
|
||||
message = data.get('message', {})
|
||||
return self._parse_crossref_data(message)
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
raise
|
||||
|
||||
return None
|
||||
|
||||
def _fetch_from_datacite(self, doi: str, timeout: int) -> Optional[Dict]:
|
||||
"""从DataCite API获取元数据"""
|
||||
try:
|
||||
url = f"{self.datacite_api}{doi}"
|
||||
headers = {
|
||||
'User-Agent': '教育AI研究项目文献库 (mailto:research@example.com)'
|
||||
}
|
||||
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
data = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
if 'data' in data:
|
||||
return self._parse_datacite_data(data['data'])
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
raise
|
||||
|
||||
return None
|
||||
|
||||
def _parse_crossref_data(self, message: Dict) -> Dict:
|
||||
"""解析Crossref返回的数据"""
|
||||
# 提取作者信息
|
||||
authors = []
|
||||
for author in message.get('author', []):
|
||||
given = author.get('given', '')
|
||||
family = author.get('family', '')
|
||||
if given and family:
|
||||
authors.append(f"{family}, {given}")
|
||||
elif family:
|
||||
authors.append(family)
|
||||
|
||||
# 提取日期
|
||||
published = message.get('published-print', message.get('published-online', {}))
|
||||
year = published.get('date-parts', [[None]])[0][0] if published else None
|
||||
|
||||
# 构建元数据
|
||||
metadata = {
|
||||
'doi': message.get('DOI', ''),
|
||||
'title': message.get('title', [''])[0] if message.get('title') else '',
|
||||
'authors': authors,
|
||||
'journal': message.get('container-title', [''])[0] if message.get('container-title') else '',
|
||||
'year': year,
|
||||
'volume': message.get('volume', ''),
|
||||
'issue': message.get('issue', ''),
|
||||
'pages': message.get('page', ''),
|
||||
'abstract': message.get('abstract', ''),
|
||||
'publisher': message.get('publisher', ''),
|
||||
'url': f"https://doi.org/{message.get('DOI', '')}",
|
||||
'citation_count': message.get('is-referenced-by-count', 0),
|
||||
'source': 'Crossref',
|
||||
'parsed_date': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return metadata
|
||||
|
||||
def _parse_datacite_data(self, data: Dict) -> Dict:
|
||||
"""解析DataCite返回的数据"""
|
||||
attributes = data.get('attributes', {})
|
||||
|
||||
# 提取作者信息
|
||||
authors = []
|
||||
for creator in attributes.get('creators', []):
|
||||
name = creator.get('name', '')
|
||||
if name:
|
||||
authors.append(name)
|
||||
|
||||
# 提取日期
|
||||
dates = attributes.get('dates', [])
|
||||
year = None
|
||||
for date in dates:
|
||||
if date.get('dateType') == 'Issued':
|
||||
year = date.get('date', '')[:4]
|
||||
break
|
||||
|
||||
# 构建元数据
|
||||
metadata = {
|
||||
'doi': attributes.get('doi', ''),
|
||||
'title': attributes.get('titles', [{}])[0].get('title', ''),
|
||||
'authors': authors,
|
||||
'journal': attributes.get('container', {}).get('title', ''),
|
||||
'year': int(year) if year and year.isdigit() else None,
|
||||
'publisher': attributes.get('publisher', ''),
|
||||
'url': f"https://doi.org/{attributes.get('doi', '')}",
|
||||
'source': 'DataCite',
|
||||
'parsed_date': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return metadata
|
||||
|
||||
def generate_citation(self, metadata: Dict, style: str = 'apa') -> str:
|
||||
"""
|
||||
生成标准引用格式
|
||||
|
||||
Args:
|
||||
metadata: 文献元数据
|
||||
style: 引用格式 (apa, mla, chicago, gb/t 7714)
|
||||
|
||||
Returns:
|
||||
str: 格式化引用
|
||||
"""
|
||||
authors = metadata.get('authors', [])
|
||||
title = metadata.get('title', '')
|
||||
journal = metadata.get('journal', '')
|
||||
year = metadata.get('year', '')
|
||||
volume = metadata.get('volume', '')
|
||||
issue = metadata.get('issue', '')
|
||||
pages = metadata.get('pages', '')
|
||||
doi = metadata.get('doi', '')
|
||||
|
||||
if style.lower() == 'apa':
|
||||
return self._format_apa(authors, title, journal, year, volume, issue, pages, doi)
|
||||
elif style.lower() == 'mla':
|
||||
return self._format_mla(authors, title, journal, year, volume, issue, pages, doi)
|
||||
elif style.lower() == 'chicago':
|
||||
return self._format_chicago(authors, title, journal, year, volume, issue, pages, doi)
|
||||
elif style.lower() == 'gb' or style.lower() == 'gb/t 7714':
|
||||
return self._format_gb(authors, title, journal, year, volume, issue, pages, doi)
|
||||
else:
|
||||
return self._format_apa(authors, title, journal, year, volume, issue, pages, doi)
|
||||
|
||||
def _format_apa(self, authors, title, journal, year, volume, issue, pages, doi):
|
||||
"""APA格式"""
|
||||
if len(authors) == 1:
|
||||
author_str = authors[0].split(',')[0]
|
||||
elif len(authors) == 2:
|
||||
author_str = f"{authors[0].split(',')[0]} & {authors[1].split(',')[0]}"
|
||||
elif len(authors) > 2:
|
||||
author_str = f"{authors[0].split(',')[0]} et al."
|
||||
else:
|
||||
author_str = "Unknown"
|
||||
|
||||
citation = f"{author_str} ({year}). {title}."
|
||||
if journal:
|
||||
citation += f" *{journal}*"
|
||||
if volume:
|
||||
citation += f", *{volume}*"
|
||||
if issue:
|
||||
citation += f"({issue})"
|
||||
if pages:
|
||||
citation += f", {pages}"
|
||||
citation += "."
|
||||
if doi:
|
||||
citation += f" https://doi.org/{doi}"
|
||||
|
||||
return citation
|
||||
|
||||
def _format_mla(self, authors, title, journal, year, volume, issue, pages, doi):
|
||||
"""MLA格式"""
|
||||
if authors:
|
||||
author_str = authors[0]
|
||||
else:
|
||||
author_str = "Unknown"
|
||||
|
||||
citation = f'"{title}." *{journal}*, vol. {volume}, no. {issue}, {year}, pp. {pages}.'
|
||||
if doi:
|
||||
citation += f" doi:{doi}."
|
||||
|
||||
return citation
|
||||
|
||||
def _format_chicago(self, authors, title, journal, year, volume, issue, pages, doi):
|
||||
"""Chicago格式"""
|
||||
if authors:
|
||||
author_str = authors[0]
|
||||
else:
|
||||
author_str = "Unknown"
|
||||
|
||||
citation = f'{author_str}. "{title}." *{journal}* {volume}, no. {issue} ({year}): {pages}.'
|
||||
if doi:
|
||||
citation += f" doi:{doi}."
|
||||
|
||||
return citation
|
||||
|
||||
def _format_gb(self, authors, title, journal, year, volume, issue, pages, doi):
|
||||
"""GB/T 7714格式(中国国家标准)"""
|
||||
if authors:
|
||||
if len(authors) > 3:
|
||||
author_str = f"{authors[0]} 等"
|
||||
else:
|
||||
author_str = ", ".join(authors)
|
||||
else:
|
||||
author_str = "佚名"
|
||||
|
||||
citation = f"{author_str}. {title}[J]. {journal}"
|
||||
if year:
|
||||
citation += f", {year}"
|
||||
if volume:
|
||||
citation += f", {volume}"
|
||||
if issue:
|
||||
citation += f"({issue})"
|
||||
if pages:
|
||||
citation += f": {pages}"
|
||||
citation += "."
|
||||
if doi:
|
||||
citation += f" DOI: {doi}."
|
||||
|
||||
return citation
|
||||
|
||||
def batch_resolve(self, doi_list: List[str], delay: float = 1.0) -> List[Tuple[str, Optional[Dict]]]:
|
||||
"""
|
||||
批量解析DOI列表
|
||||
|
||||
Args:
|
||||
doi_list: DOI列表
|
||||
delay: 请求间隔(秒)
|
||||
|
||||
Returns:
|
||||
List[Tuple]: (doi, metadata) 元组列表
|
||||
"""
|
||||
import time
|
||||
|
||||
results = []
|
||||
total = len(doi_list)
|
||||
|
||||
print(f"开始批量解析 {total} 个DOI...")
|
||||
|
||||
for i, doi in enumerate(doi_list, 1):
|
||||
print(f"[{i}/{total}] 解析: {doi}")
|
||||
metadata = self.resolve_doi(doi)
|
||||
results.append((doi, metadata))
|
||||
|
||||
if i < total:
|
||||
time.sleep(delay)
|
||||
|
||||
success = sum(1 for _, m in results if m is not None)
|
||||
print(f"\n解析完成: {success}/{total} 成功")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数 - 命令行接口"""
|
||||
import sys
|
||||
|
||||
resolver = DOIResolver()
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("用法:")
|
||||
print(" python doi_resolver.py <DOI> [style]")
|
||||
print(" python doi_resolver.py --batch <doi_list_file>")
|
||||
print("")
|
||||
print("示例:")
|
||||
print(' python doi_resolver.py "10.1038/s41599-026-07019-z" apa')
|
||||
print(' python doi_resolver.py --batch doi_list.txt')
|
||||
sys.exit(1)
|
||||
|
||||
if sys.argv[1] == '--batch':
|
||||
# 批量处理模式
|
||||
if len(sys.argv) < 3:
|
||||
print("错误: 请提供DOI列表文件路径")
|
||||
sys.exit(1)
|
||||
|
||||
file_path = sys.argv[2]
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
doi_list = [line.strip() for line in f if line.strip()]
|
||||
|
||||
results = resolver.batch_resolve(doi_list)
|
||||
|
||||
# 保存结果
|
||||
output_file = file_path.replace('.txt', '_resolved.json')
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump([{'doi': d, 'metadata': m} for d, m in results], f,
|
||||
ensure_ascii=False, indent=2)
|
||||
print(f"\n结果已保存: {output_file}")
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"错误: 文件不存在: {file_path}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# 单DOI处理模式
|
||||
doi = sys.argv[1]
|
||||
style = sys.argv[2] if len(sys.argv) > 2 else 'apa'
|
||||
|
||||
print(f"解析DOI: {doi}")
|
||||
print("-" * 50)
|
||||
|
||||
metadata = resolver.resolve_doi(doi)
|
||||
|
||||
if metadata:
|
||||
print("✅ 解析成功!")
|
||||
print("\n元数据:")
|
||||
for key, value in metadata.items():
|
||||
if value:
|
||||
print(f" {key}: {value}")
|
||||
|
||||
print(f"\n引用格式 ({style.upper()}):")
|
||||
citation = resolver.generate_citation(metadata, style)
|
||||
print(citation)
|
||||
else:
|
||||
print("❌ 解析失败")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,530 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user