一个面向科研工作者的全自动学术引用分析系统 —— 从 Google Scholar 抓取到 AI 驱动的作者画像,再到自包含的 HTML 分析仪表盘,端到端无缝流水线。
手动追踪一篇论文的数百个引用来源、判断引用者的学术影响力,是科研工作中极为耗时的任务。CitationClaw 将这一过程完全自动化。
通过 ScraperAPI 代理绕过 Google Scholar 的反爬机制,支持三级重试与地理节点轮换,稳定抓取所有引用论文。
调用 OpenAI-compatible LLM(含 Web Search)识别每位第一作者的机构、国家、Scholar 引用数,以及院士/Fellow/杰青等学术头衔。
生成单个 HTML 文件,内嵌 Chart.js 交互图表、关键词词云、趋势预测、引用描述摘要,无需服务器即可打开分享。
每一个技术决策的背后都有明确的工程理由。以下是驱动 CitationClaw 架构的核心原则。
系统通过标准 openai.AsyncOpenAI SDK 与任何兼容 OpenAI Chat Completion 协议的服务对话。使用 extra_body={"web_search_options": {}} 扩展字段启用 Web Search,这使得系统天然兼容 Gemini via Proxy、GPT-4o、DeepSeek、Qwen 等多种后端,切换成本为零。
Phase 2(作者搜索)和 Phase 4(引用描述)均为 I/O-bound 工作负载(HTTP 调用 LLM API),使用 asyncio.Semaphore 控制并发数,Phase 1 中 ScraperAPI 的同步调用通过 asyncio.to_thread 包装,不阻塞事件循环。理论并发限制取决于 API 配额,而非线程池大小。
Google Scholar 的反爬策略在三个维度上发力:网络层(HTTP 错误)、应用层(CAPTCHA/登录页)、基础设施层(数据中心地理不一致)。CitationClaw 针对每层单独设计对策,而非用一个简单的指数退避处理所有错误——这大幅提升了大规模抓取的成功率。
作者信息缓存以 JSON 文件存储在 data/cache/author_info_cache.json,以论文 URL 或标题为 key,支持字段级增量更新。这意味着用不同配置重跑流水线时,已搜索的作者信息无需重新查询,极大节省 LLM API 费用。
仪表盘的所有数据以 JS 变量形式 inline 在 HTML 中,Chart.js 和 marked.js 通过 CDN 加载,其余 CSS/JS 全部内嵌。这使得非技术用户可以直接双击打开,邮件附件发送,或上传至任何静态托管服务,而无需部署任何后端。
通过 Shell 脚本构建标准 .app 包结构,用 rumps 实现原生菜单栏 Daemon,Python 环境隔离在 ~/Library/Application Support 的 venv 中,使用 arch -arm64 强制 Apple Silicon 原生编译。相比 PyInstaller 或 Electron,打包产物更小,启动更快,无跨平台运行时依赖。
Phase 5(仪表盘)的每个 LLM 分析步骤都有一个不依赖 LLM 的确定性算法作为后备:关键词分析用词频统计,趋势预测用线性回归(含同比增长率截断),洞察生成用模板字符串。这确保了即使 LLM API 完全不可用,系统仍能输出有意义的报告。
从用户输入到最终产物的完整数据流与模块拓扑。
接收用户提供的 Google Scholar 论文 URL 或 cites= 参数,构造引用列表的起始 URL。核心逻辑是用 BeautifulSoup 解析 Scholar 搜索结果页,从 <a href="..."> 中提取 cites=XXXXX 参数。
通过 ScraperAPI 代理发送 HTTP 请求,返回包含引用计数的引用列表 URL,作为 Phase 1 抓取的入口。
cites=XXXXX这是整个系统中工程难度最高的模块。Google Scholar 有三种主要的反爬手段,系统针对每种手段设计了独立的对策。
# 年份遍历模式:绕过 Google Scholar 每次只返回 ~1000 篇的硬限制 # 解析 .gs_hist_g_a[data-year][data-count] 柱状图 async def _scrape_by_year(self, base_url: str): # 先获取年份直方图 histogram = await self._fetch_year_histogram(base_url) # {2018: 43, 2019: 127, 2020: 289, ...} for year, count in histogram.items(): # 对每个年份单独构造 URL 并抓取 year_url = base_url + f"&as_ylo={year}&as_yhi={year}" papers = await self._scrape_url(year_url) yield papers await self._write_jsonl_page(papers)
# 20 个优先国家代码(学术流量大,被封概率低) PREFERRED_GEO_COUNTRIES = [ 'us', 'uk', 'de', 'fr', 'ca', 'au', 'jp', 'kr', 'sg', 'nl', 'se', 'ch', 'at', 'dk', 'fi', 'no', 'nz', 'ie', 'be', 'it', ] async def _detect_dc_inconsistency(self, page_html: str) -> bool: # 短页面检测:正常页面应有 10 篇论文 papers_found = self.parser.count_results(page_html) return papers_found < 2 async def _rotate_geo(self, attempt: int) -> str: if attempt < 20: return self.PREFERRED_GEO_COUNTRIES[attempt % 20] else: return random.choice(self.ALL_GEO_COUNTRIES)
Google Scholar 对任意查询最多返回约 1000 篇论文。对于高引用论文(如引用数超过 2000),不分年份直接抓取会丢失大量数据。年份遍历模式将问题分解为每年独立的子查询,每年通常不超过 300 篇,从而突破硬限制。
每篇引用论文需要经过多轮 LLM 对话来完成完整的作者画像。整个过程通过 asyncio.Semaphore 控制并发,避免超出 API 速率限制。
| 步骤 | 操作 | 输入 | 输出 | 模型 |
|---|---|---|---|---|
| Step 1 | LLM Web Search → 作者+机构(自由文本) | 论文标题 + 第一作者名 | 机构名、所在国家(非结构化) | 主模型 |
| Step 1b | JSON 提取 | Step 1 输出 | first_author_institution, country | 主模型 |
| Step 2 | LLM Web Search → Scholar 引用数 + 学术头衔 | 作者名 + 机构 | 引用数、院士/Fellow/杰青等识别 | 主模型 |
| Step 3 | 事实核验(可选) | Step 2 输出 | 核实后的信息 | verify模型 |
| Step 4 | 自引检测 | 引用作者集合 vs 目标作者 | is_self_citation (bool) | 主模型 |
| Step 5 | 重要学者识别 + 结构化 JSON | 所有已收集信息 | 完整结构化 JSON | renowned模型 |
class AuthorSearcher: async def search_all(self, papers: list, parallel_workers: int = 5): sem = asyncio.Semaphore(parallel_workers) async def _bounded_search(paper): async with sem: # 先检查缓存 cached = self.cache.get(paper['url']) if cached and cached.is_complete(): return cached # 执行多步 LLM 搜索 return await self._search_single(paper) tasks = [_bounded_search(p) for p in papers] return await asyncio.gather(*tasks) async def _llm_web_search(self, prompt: str) -> str: # extra_body 启用 Web Search(OpenAI-compatible 扩展) resp = await self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], extra_body={"web_search_options": {}} ) return resp.choices[0].message.content
使用 Pandas 构建 DataFrame,通过 OpenPyXL 写入 Excel。最关键的特性是按学术影响力进行颜色编码,使分析者一眼识别重要引用来源。
三种 Excel 输出:
main_table.xlsx — 完整引用表renowned_only.xlsx — 仅重要学者all_papers.xlsx — 保留所有字段的原始数据对每篇引用论文,LLM 通过 Web Search 访问其 URL,定位并提取准确引用被引论文的原句(citation sentence)。
# 对目标论文标题加锁,防止并发时重复查找同一目标 self._title_locks: dict[str, asyncio.Lock] = {} async def _get_lock(self, title: str) -> asyncio.Lock: if title not in self._title_locks: self._title_locks[title] = asyncio.Lock() return self._title_locks[title] # 三种抓取范围 # "all" → 所有引用论文 # "renowned_only" → 仅重要学者的论文 # "specified_only" → 用户指定的论文列表
通过 4 次 LLM 调用生成深度分析报告,并将所有数据 inline 在单个 HTML 文件中。
| LLM 调用 | 功能 | Fallback 算法 |
|---|---|---|
_llm_keywords() | 关键词提取与权重分析 | 词频统计(TF) |
_llm_description_analysis() | 引用描述语义分析 | 模板字符串 |
_llm_trend_prediction() | 未来 3 年引用趋势预测 | 线性回归 + YoY 增长率截断 |
_llm_insights() | 洞察报告生成(Markdown) | 基于统计数据的模板 |
所有 Chart.js 数据(labels, datasets)通过 Python json.dumps() 序列化后,以 <script>const data = ...</script> 形式内嵌在 HTML 中。Markdown 洞察通过 marked.js CDN 在浏览器端渲染。CSS 和逻辑 JS 全部 inline,最终产物为单个约 2000+ 行的 .html 文件。
FastAPI 在 /ws 端点维护一个 WebSocket 连接,将流水线运行时的日志、进度和阶段事件实时推送到前端。
系统在任务执行前后分别调用 GET /api/user/self 快照配额,差值即为本次任务消耗。支持两种计费模型:
每次 HTTP 响应的 sa-credit-cost 响应头携带本次消耗的 credits 数量。
sa-credit-cost: 10
# 费率:$49 / 100,000 credits
通过 API 配额快照差值计算 token 消耗,换算为人民币费用显示在日志中。
token_diff = quota_before - quota_after
units = token_diff ÷ 500,000
cost_rmb = units × ¥2
app/config_manager.py 负责读写 config.json,并通过 _safe_data_path() 对所有文件路径进行路径穿越攻击防护。
def _safe_data_path(relative_path: str) -> Path: """防止路径穿越攻击 (e.g. ../../etc/passwd)""" base = Path("data").resolve() target = (base / relative_path).resolve() if not str(target).startswith(str(base)): raise ValueError(f"Path traversal attempt detected: {relative_path}") return target
Google Scholar 的反爬机制在三个不同层面运作,系统针对每一层设计了独立的检测与对策逻辑:
5,10,20 秒)指数退避;最多 N 次(默认 3)。
country_code 参数切换出口节点地理位置。前 20 次循环使用优先国家列表,之后随机抽取全球 60+ 国家池。
{
"https://paper.url/...": {
"first_author": "Zhang Wei",
"institution": "MIT",
"country": "US",
"scholar_citations": 12400,
"is_academician": false,
"is_fellow": true,
"fellow_type": "IEEE Fellow",
"is_self_citation": false,
"cached_at": "2026-03-01T10:23:00"
}
}
force_refresh=True)无需 Electron,无需 PyInstaller,通过 Shell 脚本构建标准 .app 包,实现真正的 macOS 原生体验。
CitationClaw.app/ └── Contents/ ├── Info.plist # 应用元数据 ├── MacOS/ │ └── CitationClaw # Shell 启动脚本 └── Resources/ └── icon.icns # 应用图标
arch -arm64 强制 Apple Silicon 原生 venv 创建~/Library/Application Support/CitationClaw/venvpip install -r requirements.txtrumps 实现原生 macOS 菜单栏 Daemonlocalhost:8000#!/bin/bash # 强制使用 arm64 架构创建 venv(避免 Rosetta 下创建 x86_64 环境) VENV_DIR="$HOME/Library/Application Support/CitationClaw/venv" if [ ! -d "$VENV_DIR" ]; then # arch -arm64 确保 Python 解释器以原生 ARM64 运行 arch -arm64 python3 -m venv "$VENV_DIR" "$VENV_DIR"/bin/pip install -r requirements.txt fi # 启动 FastAPI 服务(后台运行) "$VENV_DIR"/bin/python start.py & SERVER_PID=$! # 启动 rumps 菜单栏 Daemon "$VENV_DIR"/bin/python menubar_app.py
PyInstaller 的产物体积通常 100MB+,且对动态导入(如 BeautifulSoup 的 parser)不友好。Electron 引入了完整的 Node.js 和 Chromium 运行时(500MB+)。Shell .app 方案产物极小,启动仅需激活 venv,所有依赖明确且可审计,完全符合 macOS 的应用沙盒规范。
不同的使用场景对数据深度和成本的要求不同。CitationClaw 提供五个预设档位,覆盖从"快速概览"到"完整深度分析"的全谱需求。
| Method | Path | 功能 | 备注 |
|---|---|---|---|
| POST | /api/run |
启动完整分析流水线 | 异步执行,通过 WS 推送进度 |
| POST | /api/scholar/papers |
抓取 Scholar 个人主页论文列表 | 独立功能,不触发完整流水线 |
| GET | /api/quota/check |
查询 LLM API 配额余额 | 调用 /api/user/self 快照 |
| GET | /api/results/list |
列出 data/ 目录下的结果文件 | 路径穿越保护 |
| GET | /api/results/view/{path} |
在浏览器中查看文件(HTML/Excel) | 路径穿越保护 |
| GET | /api/results/download/{path} |
下载文件 | Content-Disposition: attachment |
| WS | /ws |
实时日志与进度推送 | 60s timeout → server ping 保活 |
{
"scholar_url": "https://scholar.google.com/scholar?cites=12345",
"service_tier": "standard",
"parallel_workers": 5,
"year_traverse": true,
"geo_rotate": true,
"dc_retry_max_attempts": 10,
"enable_renowned_scholar": true,
"citing_description_scope": "renowned_only"
}
每个工程决策都有代价。以下是当前版本的已知限制,以及有价值的改进方向。
当前三层重试机制基于已观察到的规律,但 Scholar 的反爬策略随时可能更新。若 Scholar 引入新型检测机制(如行为分析、更复杂的 CAPTCHA),现有重试逻辑可能需要相应调整。改进方向:引入本地浏览器驱动(Playwright)作为终极 fallback,绕过网络层检测。
对于同名学者(如"Li Wei"在中国学术界有数百人),LLM 可能混淆人物。当前依赖 LLM 自身的推理能力消歧,无专门的实体链接(Entity Linking)模块。改进方向:集成 OpenAlex API 或 Semantic Scholar API 进行结构化作者消歧,降低 LLM 错误率。
对于超高引用论文(5000+ 篇),年份遍历模式需要对每年分别发送多页请求,总体耗时可能达到数小时。当前无法中断并恢复(断点续抓)。改进方向:实现 JSONL append 模式 + 年份级别的进度检查点,支持中断恢复。
当前系统设计为单用户本地工具,FastAPI 端点无认证保护,data/ 目录为全局共享。若部署到多用户环境,数据存在互相污染的风险。改进方向:引入任务 ID(UUID)隔离每次运行的数据,并添加 API Key 认证中间件。
Phase 5 的 4 次 LLM 调用使用 openai.OpenAI(同步客户端)按顺序执行,总等待时间约为 4 个 LLM 响应时间之和(可能 60-120 秒)。改进方向:迁移至 asyncio.gather() 并发执行,总时间缩减为最慢单次调用时间。
作者信息缓存一旦写入,永不自动过期。若作者获得新的学术头衔(如新当选院士),缓存中的旧数据不会自动更新。改进方向:为每个缓存条目添加 TTL(如 30 天),或提供 --invalidate-cache-before YYYY-MM-DD 参数。
| Package | Version | 用途 |
|---|---|---|
| fastapi | latest | Web 框架 |
| openai | ≥1.x | LLM SDK |
| beautifulsoup4 | 4.x | HTML 解析 |
| pandas | 2.x | 数据处理 |
| pydantic | v2 | 数据校验 |
| httpx | latest | HTTP 客户端 |
| rumps | latest | macOS 菜单栏 |
| openpyxl | 3.x | Excel 写入 |
| 术语 | 含义 |
|---|---|
cites=XXXXX | Google Scholar 引用关系 ID,用于构造"引用了这篇论文"的列表 URL |
ScraperAPI | 反爬 HTTP 代理服务,提供 IP 轮换、CAPTCHA 解决、地理节点选择等功能 |
extra_body | OpenAI SDK 的扩展字段,用于传递非标准参数(如 web_search_options)给兼容 API |
asyncio.Semaphore | Python 异步信号量,用于限制并发协程数量,避免超出 API 速率限制 |
JSONL | JSON Lines 格式,每行一个 JSON 对象,便于流式写入和逐行读取大文件 |
rumps | Ridiculously Uncomplicated macOS Python Statusbar apps,macOS 菜单栏应用框架 |
year_traverse | 年份遍历模式,将整体查询按年份分解,绕过 Google Scholar 1000 篇硬限制 |
dc_inconsistency | 数据中心不一致,Google Scholar 不同地理节点的缓存不同步导致的短页面现象 |