FEATURED · 精选文章

如何用Python构建AI论文周报系统:从arXiv采集到自动化推送

发布时间 / 2026/8/30 14:19:53
来源 / 创域科博编辑部
栏目 / 资讯中心
如何用Python构建AI论文周报系统:从arXiv采集到自动化推送 这段时间 AI 领域的论文更新速度几乎快到“一天不刷就可能错过一个重要方向”的程度。我自己在跟进大模型相关研究时就有很深的体会arXiv 上每天新增成百上千篇论文光靠收藏夹和微信群转发的链接很难形成系统认知。DAIR.AI 上线 AI 论文周报平台本质上就是在解决这个信息过载问题把散落在 arXiv、学术会议和各大预印本站点的研究进展按周整理成一份结构化的阅读清单。DAIR.AI 不是某个简单的论文仓库我更愿意把它理解成一个面向 AI 学习与研究的开放社区。它过去以开源的学习资料和提示工程相关项目被人熟知而上线论文周报平台相当于把“跟进前沿研究”这件事产品化了。下面我会从平台定位、核心功能、技术架构再到如何用 Python 从零搭建一套类似的论文周报系统逐一展开。无论你是想快速获取论文信息的学生还是需要做技术调研的工程师都能在本文找到可落地的内容。1. DAIR.AI 与 AI 论文周报平台1.1 什么是 DAIR.AIDAIR.AI 是一个开放的人工智能学习与研究社区名字可以理解为 “Demo AI Research” 或 “Distill AI Research” 的缩写重点是“开放”和“去碎片化”。它早期做的内容更多偏向 AI 教育、入门资源整理和工程实践分享很多开发者第一次接触 DAIR.AI是因为它的提示工程相关内容后来平台逐步拓展开始覆盖更广的 AIGC、大模型和 Agent 相关主题。论文周报平台可以看作是 DAIR.AI 的一次产品化尝试。它不追求代替研究者阅读而是把每周 AI 领域值得关注的论文、技术方向和工程经验整理成一份聚合内容。这个思路和传统 RSS 阅读器不同RSS 只是把源拉过来而论文周报更强调筛选、分类、摘要和解读。换句话说它既要解决“论文从哪来”也要解决“哪些论文值得读”。1.2 为什么要做独立的论文周报平台AI 论文的增量已经大到人力无法逐个跟踪的程度。以 arXiv 的部分子类为例单日新增可能就是几十到上百篇一周下来数量相当可观。如果只靠个人手动收藏很容易出现两个问题信息遗漏重要论文发表一周后才被转发等你看到时别人已经基于它做了二次工作。信息过载每天都刷到大量相关链接但真正仔细读的时间有限收藏夹越积越长阅读率却很低。论文周报平台的价值在于把“追论文”变成一个可持续的流程。系统自动采集论文元数据通过关键词、分类模型甚至大模型辅助筛选再按主题分组最后以周报形式推送。这样研究者只需要每周花相对固定的时间就能了解领域内的主要进展。1.3 平台解决的核心问题从用户视角来看论文周报平台解决的核心问题有三个。第一是时效性。论文从预印本发布到被解读中间存在时间差。平台通过定时采集和自动解析可以显著缩短这个时间差。第二是筛选效率。AI 领域论文质量参差不齐关键词命中不等于值得读。周报平台通常会在标题、摘要、分类、代码可用性等维度做初步过滤让读者优先看到高价值内容。第三是知识沉淀。周报不是一次性推送而是按时间线持续累积。历史周报可以形成检索库当你需要回看某个方向的演进脉络时能直接定位到具体论文。2. 论文周报平台的核心功能拆解2.1 论文采集与数据源管理论文采集是整个平台的地基。数据源最常用的是 arXiv API此外还有 ACL Anthology、OpenReview、Semantic Scholar API 等。以 arXiv 为例它的官方 API 支持按分类、关键词、时间范围查询接口协议是 Atom XML。常见的用法有两种按分类拉取例如cat:cs.CL表示计算语言学cat:cs.AI表示人工智能cat:cs.LG表示机器学习。按关键词搜索例如all:large language model表示在所有字段中检索该短语。实际工程中通常不会只依赖单个数据源。不同会议的论文可能只出现在 OpenReview而不是 arXiv某些系统论文可能挂在作者主页。所以论文采集层应当设计成可插拔的源每个源输出统一的数据结构方便后续处理。2.2 筛选、分类与摘要采集到的原始论文不能直接进入周报需要经过筛选和分类。最简单的策略是维护一组关键词规则例如“LLM”“Agent”“RAG”“多模态”等。命中关键词的论文进入候选池再计算相关度或按时间排序。更智能的方案是利用嵌入模型或大模型对摘要做语义分类。比如把论文摘要映射到向量空间再用聚类算法发现本周的研究主题或者直接用 LLM 生成一篇论文的“一句话总结”帮助读者判断是否值得深读。这个环节是周报平台和普通列表页的最大区别也是内容质量的关键。我个人的建议是初期先用规则引擎跑通流程再逐步引入模型。规则可以解释、可以调试对数据量和成本都更可控。直接上来就用大模型做全量摘要不仅成本高摘要质量也不稳定。2.3 阅读、订阅与推送周报最终要通过某种形式触达用户。常见形态有网页列表按周归档支持按分类筛选。邮件推送用户订阅后每周定时收到周报。RSS/Atom供技术用户导入阅读器。JSON API支持下游应用二次开发。订阅系统必须有退订入口这是邮件服务的基本合规要求。网页端则需要考虑数据分页、检索和移动端阅读体验。好的周报平台不会强迫用户每天访问而是让用户在自己习惯的渠道里接收信息。3. 技术架构与系统设计思路3.1 整体架构分层如果把论文周报平台当成一个真实项目来设计可以按采集层、处理层、存储层、服务层、触达层这五层来划分。采集层负责定时从 arXiv、OpenReview 等数据源拉取论文元数据做 HTTP 请求和响应解析。处理层负责规则过滤、分类、摘要和去重。存储层保存论文原始信息、处理结果、用户订阅关系和周报发送记录。服务层对外提供 Web 页面和 API。触达层负责邮件、RSS、Webhook 等消息推送。这个分层的好处是各模块职责清晰。某个数据源变更接口格式时只需要修改采集层用户反馈邮件被拦截时只需要排查触达层。早期项目可以适当简化比如把处理逻辑写在脚本里但分层思想应该保留。3.2 核心数据流从数据流视角来看可以拆成下面几步定时任务启动采集最近 N 天的论文数据。原始数据经过字段清洗写入论文主表。处理任务读取未处理记录执行关键词或模型过滤。过滤后的论文生成周报版本关联到对应的“周报批次”。对已订阅用户执行推送记录发送状态。这套流程里最容易出问题的不是单个环节而是环节之间的衔接。例如采集入库成功但处理任务失败用户会收到空周报处理任务执行了但没有推进截止时间会导致重复处理。工程上通常会给每个批次生成一个批次号所有任务都围绕批次号做幂等控制。3.3 技术选型建议技术选型没有标准答案取决于团队规模和维护成本。后端Python FastAPI 适合快速开发数据处理生态也好如果是 Java 团队Spring Boot 也可以但类似论文解析的库相对少一些。定时任务简单场景用系统 crontab 或 APScheduler复杂场景可以引入 Celery Beat或者使用云厂商的定时触发器。数据库MySQL/PostgreSQL 保存论文和订阅关系SQLite 适合本地原型。关键词较多时可以引入 Elasticsearch 或 SQLite FTS。邮件发送SMTP 直发适合小规模生产环境建议使用云邮件服务避免 IP 信誉问题导致进垃圾箱。前端如果只做展示SSR 模板就行如果需要复杂交互React/Vue 独立部署。4. 从零搭建一个 AI 论文周报生成系统这一节我们不看 DAIR.AI 内部的实现而是实战搭建一个简化版论文周报生成器。它会调用 arXiv API 拉取论文按关键词分类去重后生成 Markdown 周报并支持邮件发送。整个系统足够小而完整适合作为课程设计或个人工具的起点。4.1 环境准备与项目结构环境要求不高只要本机有 Python 3.10 或更高版本即可。本项目依赖较少只需要requests一个第三方库。python3 -m venv .venv source .venv/bin/activate pip install requests建议的项目结构如下ai-paper-weekly/ ├── requirements.txt ├── arxiv_client.py ├── filter.py ├── report.py ├── mailer.py ├── run_weekly.py └── data/data目录用来存放 SQLite 数据库文件实现本地持久化和去重。4.2 编写 arXiv 数据采集模块arXiv API 的基础地址是http://export.arxiv.org/api/query支持search_query、sortBy、sortOrder、max_results等参数。我们使用 Python 的urllib.parse构造查询参数再通过requests发起请求。这里要特别提一下arXiv 官方要求调用方设置User-Agent并且不要过于密集地请求。教程中的代码加入了一个轻量延迟实际项目中建议使用指数退避和重试机制。# 文件路径arxiv_client.py import time import urllib.parse import xml.etree.ElementTree as ET from datetime import datetime, timedelta, timezone import requests ARXIV_API_URL http://export.arxiv.org/api/query ATOM_NS {http://www.w3.org/2005/Atom} def fetch_recent_papers( category: str cs.CL, days: int 7, max_results: int 100, user_agent: str ai-paper-weekly/1.0, ) - list[dict]: since_time datetime.now(timezone.utc) - timedelta(daysdays) query fcat:{category} params { search_query: query, sortBy: submittedDate, sortOrder: descending, max_results: max_results, } headers { User-Agent: user_agent, } resp requests.get(ARXIV_API_URL, paramsparams, headersheaders, timeout30) resp.raise_for_status() root ET.fromstring(resp.text) papers [] for entry in root.findall(f{ATOM_NS}entry): title entry.findtext(f{ATOM_NS}title, ).strip() summary entry.findtext(f{ATOM_NS}summary, ).strip() published entry.findtext(f{ATOM_NS}published, ).strip() paper_id entry.findtext(f{ATOM_NS}id, ).strip() authors [ author.findtext(f{ATOM_NS}name, ).strip() for author in entry.findall(f{ATOM_NS}author) ] published_dt datetime.fromisoformat(published.replace(Z, 00:00)) if published_dt since_time: continue papers.append( { paper_id: paper_id, title: title, summary: summary, published: published_dt.isoformat(), authors: authors, } ) # 对 arXiv 保持礼貌的请求频率 time.sleep(1.0) return papers这段代码的核心逻辑并不复杂。先构造查询参数请求 arXiv API然后用 Python 内置的xml.etree.ElementTree解析 Atom XML。需要留意的是命名空间Atom 中所有字段都带有http://www.w3.org/2005/Atom这个前缀所以解析时不能写entry.findall(entry)而要写entry.findall(f{ATOM_NS}entry)。4.3 编写关键词筛选与分类模块筛选模块维护一个简单的分类词表。每个分类对应一组关键词只要标题或摘要命中其中一个就认为该论文属于这个分类。这是一个很初级的规则但它可控、可解释适合作为第一版实现。# 文件路径filter.py from collections import Counter KEYWORDS { llm: [large language model, llm, foundation model], agent: [llm agent, ai agent, multi-agent, autonomous agent], rag: [retrieval augmented, rag, retrieval-augmented], multimodal: [multimodal, vision-language, vlm], alignment: [alignment, reinforcement learning from human feedback, rlhf], } def classify_paper(paper: dict, keyword_rules: dict | None None) - list[str]: rules keyword_rules or KEYWORDS text f{paper[title]}\n{paper[summary]}.lower() matched_categories [] for category, keywords in rules.items(): for keyword in keywords: if keyword in text: matched_categories.append(category) break return matched_categories def filter_papers(papers: list[dict]) - list[dict]: filtered [] for paper in papers: categories classify_paper(paper) if categories: paper[categories] categories filtered.append(paper) return filtered def category_stats(papers: list[dict]) - dict: stats Counter() for paper in papers: for category in paper.get(categories, []): stats[category] 1 return dict(stats)这里使用了最简单的大小写归一化和子串匹配。实际论文标题中可能出现 “LLM-based” 或 “RAG-Enhanced”子串匹配通常会低估或过估一些结果。后续优化方向有两个一是引入正则表达式把llm匹配为单词边界二是对关键词做词形还原例如agent和agents都归到同一类。4.4 生成 Markdown 周报有了论文和分类结果下一步把它们渲染成 Markdown 报告。Markdown 的好处是通用性强既可以直接展示也可以后续转成 HTML 或 PDF。# 文件路径report.py from datetime import datetime, timezone def generate_markdown_report(papers: list[dict], stats: dict) - str: lines [] lines.append(# AI Paper Weekly) lines.append() lines.append(f 数据生成时间{datetime.now(timezone.utc).strftime(%Y-%m-%d %H:%M UTC)}) lines.append() lines.append(## 本周分类统计) lines.append() for category, count in stats.items(): lines.append(f- {category}: {count} 篇) lines.append() for idx, paper in enumerate(papers, start1): title paper[title] categories , .join(paper.get(categories, [])) authors , .join(paper.get(authors, [])[:5]) paper_url paper.get(paper_id, ) summary_preview paper.get(summary, ).strip().replace(\n, ) if len(summary_preview) 300: summary_preview summary_preview[:300] ... lines.append(f### {idx}. {title}) lines.append() lines.append(f- 分类{categories}) lines.append(f- 发布时间{paper.get(published, )}) lines.append(f- 作者{authors}) lines.append(f- 链接{paper_url}) lines.append() lines.append(summary_preview) lines.append() return \n.join(lines)摘要字段在生成时截断到 300 字避免周报过长。作者字段最多展示前 5 位如果论文作者数量很多全部列出会让版面变得混乱。4.5 开发 SQLite 去重模块采集任务反复执行后数据库中会积累历史论文。如果不做去重同一篇论文可能出现在多期周报中。一个实用的做法是给论文的paper_id建立唯一索引重复插入时让数据库直接拒绝。# 文件路径storage.py import sqlite3 DB_PATH data/papers.db def get_connection(db_path: str DB_PATH) - sqlite3.Connection: conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS papers ( paper_id TEXT PRIMARY KEY, title TEXT NOT NULL, published TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() return conn def save_new_papers(conn: sqlite3.Connection, papers: list[dict]) - int: new_count 0 for paper in papers: try: conn.execute( INSERT INTO papers(paper_id, title, published) VALUES (?, ?, ?), (paper[paper_id], paper[title], paper[published]), ) new_count 1 except sqlite3.IntegrityError: # 已经存在的论文跳过 continue conn.commit() return new_count这一小段代码的价值在于当论文重复出现时数据库的唯一索引会拦截插入程序只需要捕获sqlite3.IntegrityError并继续执行即可。这里不采用INSERT OR IGNORE是因为我想在代码里统计新增数量方便日志输出。4.6 邮件发送与定时任务邮件模块在多数场景下只是一个发送工具。要特别注意不要把邮箱密码或授权码直接写在源码里最好通过环境变量或配置文件注入。# 文件路径mailer.py import os import smtplib from email.header import Header from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText def send_weekly_email( markdown_text: str, to_addrs: list[str], html_text: str , ) - None: smtp_host os.environ.get(SMTP_HOST, ) smtp_port int(os.environ.get(SMTP_PORT, 465)) sender os.environ.get(SMTP_USER, ) password os.environ.get(SMTP_PASSWORD, ) msg MIMEMultipart(alternative) msg[Subject] Header(AI Paper Weekly 周报, utf-8) msg[From] sender msg[To] , .join(to_addrs) msg.attach(MIMEText(markdown_text, plain, utf-8)) if html_text: msg.attach(MIMEText(html_text, html, utf-8)) server smtplib.SMTP_SSL(smtp_host, smtp_port) server.login(sender, password) server.sendmail(sender, to_addrs, msg.as_string()) server.quit()邮件正文支持纯文本和 HTML 两种格式。MIMEMultipart(alternative)的作用是让邮件客户端优先展示 HTML不支持 HTML 时自动切换到纯文本。定时任务在 Linux 服务器上可以直接使用 crontab0 9 * * 1 cd /path/to/ai-paper-weekly .venv/bin/python run_weekly.py logs/weekly.log 21这条规则表示每周一早上 9 点执行一次周报生成脚本并把日志写入logs/weekly.log。4.7 组装完整运行脚本最后是主入口脚本。它把采集、筛选、去重、生成报告和发送邮件串起来。为了安全我默认注释掉邮件发送先让脚本生成 Markdown 文件确认内容没问题后再开启发送。# 文件路径run_weekly.py from arxiv_client import fetch_recent_papers from filter import category_stats, filter_papers from report import generate_markdown_report from storage import get_connection, save_new_papers from mailer import send_weekly_email def main(): # 1. 拉取最近 7 天的论文 papers fetch_recent_papers(categorycs.CL, days7, max_results100) print(f[1] 拉取论文总数{len(papers)}) # 2. 按关键词过滤 filtered_papers filter_papers(papers) print(f[2] 过滤后论文数{len(filtered_papers)}) # 3. 入库去重 conn get_connection() new_papers_count save_new_papers(conn, filtered_papers) print(f[3] 新增论文数{new_papers_count}) conn.close() # 4. 重新读取未入库的新论文生成报告 # 说明这里为了演示直接用过滤结果生成报告 # 生产环境建议从数据库读取未生成过报告的论文批次。 stats category_stats(filtered_papers) markdown_report generate_markdown_report(filtered_papers, stats) with open(weekly_report.md, w, encodingutf-8) as f: f.write(markdown_report) print([4] 周报已生成weekly_report.md) # 5. 发送邮件 # send_weekly_email(markdown_report, [your_emailexample.com]) if __name__ __main__: main()运行结果大致如下[1] 拉取论文总数97 [2] 过滤后论文数23 [3] 新增论文数21 [4] 周报已生成weekly_report.md真实环境中步骤 3 和步骤 4 之间应当以数据库中的“周报批次”为边界。即先保存本次去重后的论文再生成一个批次 ID后续再根据批次 ID 聚合生成周报。教程中的代码为了可读性做了一定简化。5. 常见问题与排查思路5.1 arXiv 请求返回 403 或 429这是调用 arXiv API 时最常遇到的问题。403 表示请求被拒绝429 表示请求过于频繁。常见原因有没有设置User-Agent、请求频率过高、IP 被临时限制。排查思路确认每次请求都带了明确的User-Agent。在日志中记录每次请求的响应状态码。在两次请求之间增加延迟例如 1 到 3 秒。如果仍然报错退避重试第一次等 30 秒第二次等 60 秒。问题现象常见原因解决思路请求返回 403缺少 User-Agent 或 IP 受限添加 User-Agent降低请求频率请求返回 429请求过于密集增加延迟使用指数退避XML 解析不到数据命名空间错误使用 Atom 命名空间前缀解析邮件进垃圾箱发件 IP 信誉低配置 SPF/DKIM或改用云邮件服务周报重复收录旧论文缺少去重逻辑为 paper_id 建立唯一索引半夜运行时间不对时区未统一统一使用 UTC 时间5.2 XML 解析不到论文标题和摘要很多人第一次使用 arXiv API 时会发现自己写entry.findall(entry)返回空列表。这不是论文数据不存在而是因为没有处理命名空间。Atom 格式中所有元素都带有命名空间前缀。正确写法是ATOM_NS {http://www.w3.org/2005/Atom} entries root.findall(f{ATOM_NS}entry)如果你使用的是feedparser这类库它可以自动处理命名空间但在标准库xml.etree.ElementTree中必须显式指定。5.3 摘要字段里的换行问题arXiv 摘要往往包含多个换行直接拼进 Markdown 会让表格和文本结构错乱。在生成报告前建议把摘要中的换行替换为空格并且对超过长度限制的部分做截断。教程中的generate_markdown_report已经演示了.replace(\n, )和截断逻辑。5.4 关键词误匹配关键词规则最大的问题就是误匹配。例如关键词agent可能命中一些把 agent 作为“代理”而非“智能体”的论文。解决方法有几种使用更长的短语例如llm agent、multi-agent system。对标题和摘要分开计分标题命中给更高权重。引入人工审核环节由编辑决定是否收录。随着数据积累还可以用标注好的样本训练分类模型而不是只依赖规则。5.5 邮件发送失败或进入垃圾箱本地 SMTP 直发很容易因为 IP 信誉不足而被对方服务器拒收。排查时先看退信内容如果是550开头的错误通常是发件人或 IP 被拒。如果是连接超时检查 SMTP 端口和防火墙。如果邮件能收到但进了垃圾箱检查 SPF、DKIM 和 DMARC 配置。小规模订阅阶段可以直接用个人邮箱发送但连续发送大量邮件前务必换成正规邮件服务否则账号容易被封。6. 最佳实践与工程建议6.1 数据采集要“文明”和“合规”无论是调用 arXiv API 还是爬取其他论文站点都要注意对方的使用条款。合理做法包括设置明确的User-Agent标明项目名称和联系方式。控制请求频率避免高峰期密集请求。优先使用官方 API而不是无差别爬虫。大批量抓取前评估数据使用是否符合协议要求。如果只是做个人周报控制单次请求数量和频率不会给源站造成压力。6.2 周报生成必须具备幂等性“幂等”这个词看起来学术但在周报系统里非常重要。同一个批次如果被重复执行两次不应该生成两份内容相同的周报。实现思路有两种给论文唯一主键paper_id重复插入报错。给每个周报定义批次号例如2025-W14同一批次只能生成一次。在代码层面即使任务异常中断重跑只要数据库里的唯一索引存在重复数据就不会被写入。6.3 订阅与退订管理要清晰如果周报平台开放订阅那么邮件管理必须包含退订入口。最基础的做法是在邮件底部放一个退订链接链接指向一个带有 token 的 URL用户点击后即退订。订阅记录通常包括用户邮箱。订阅的分类列表。订阅状态启用/停用。最近一次推送时间。不要在没有授权的情况下向任何邮箱发送邮件也不要长期保存用户不需要的数据。6.4 用大模型辅助摘要时要设置边界周报平台可以引入 LLM 来做论文摘要但大模型在总结专业论文时会有幻觉尤其是出现公式、数据集名称和实验数据时很容易“一本正经地编造”。使用大模型的建议是让模型基于摘要生成一句话解读而不是让模型补充摘要中没有的信息。标注“AI 生成解读仅供参考”不要和论文原始摘要混在一起。对关键信息进行抽查例如方法名、数据集名称、实验结论。对成本做配额控制避免每个批次调用过多 token。目前在真实项目里“大模型辅助 人工抽查”是性价比较高的组合。6.5 日志、监控与告警周报系统看似简单真正跑起来后失败点比想象中多。采集任务可能失败邮件可能发送超时数据库可能磁盘满。建议从第一天就加上结构化日志。每一条日志至少包含时间戳。任务名称。成功/失败状态。消耗时间。关键数据量例如拉取数量、过滤数量、新增数量。当连续几次任务都失败时应该通过告警通知负责人。最简单的方式是任务脚本里在异常时发送一条通知消息而不是等到用户发现“这周没收到周报”。6.6 提前规划数据模型论文数据模型是后续所有功能的基础。哪怕第一版先用 SQLite表结构也要尽量规范。一个合理的论文表结构可以包含paper_id论文唯一标识。title标题。summary摘要。authors作者 JSON 数组。published发布时间。categories分类结果 JSON。source数据源。created_at入库时间。预留这些字段后续无论做检索、数据分析还是用户推荐都不用推翻表结构。7. 总结与后续学习建议论文周报平台本身并不复杂核心就三件事采集、筛选、推送。但要把这三件事做好需要跨模块的工程经验。本文从 DAIR.AI 上线的 AI 论文周报平台切入拆解了平台的功能定位和技术分层然后提供了一个基于 Python 和 arXiv API 的可运行案例。通过这个项目最容易掌握的点有三个如何调用 arXiv API 并解析 Atom 格式数据。如何设计可解释的关键词过滤和分类逻辑。如何用唯一索引实现数据去重。如果想让项目更接近生产环境下一步可以继续拓展四个方向引入 Redis 做缓存和队列、接入向量检索做语义相似度推荐、使用前端框架搭建可视化阅读页面、把定时任务迁移到云平台并配置监控告警。技术工具的最终价值是节省时间而不是占用更多时间。与其收藏一堆论文链接不如让它自动变成一份结构化的周报每周花半小时精读就够了。你可以把这个方案当作练手项目也可以直接改造成自己领域的技术雷达动手跑一遍远比只看不练更有效。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻