1. 当"日报"变成一种产品:我为什么要做这件事
每天早上打开手机,信息流里塞满了各种"AI快讯""行业动态""今日要闻"。我数过,光是订阅的几个资讯渠道,一天推送的条目加起来超过两百条。真正值得花时间读的,可能不到十条。剩下的要么是标题党,要么是同一件事被反复转述,要么干脆是几天前的旧闻换个日期重新发一遍。
这个痛点催生了"AI 日报"这个项目。它的核心逻辑很简单:用自动化的方式,把一天之内散落在各个信息源里的AI相关动态收集起来,经过筛选、去重、分类、摘要,最终生成一份结构清晰、信息密度高、可以直接阅读的日报。它不是简单的RSS聚合,也不是把标题堆在一起就完事。它要解决的是"信息过载"和"信息质量参差不齐"这两个问题。
适合谁来参考这篇内容?如果你是一个独立开发者,想给自己或小团队做一个信息聚合工具;如果你是一个内容运营,每天需要快速掌握行业动态但不想被垃圾信息淹没;如果你是一个对自动化流程感兴趣的技术爱好者,想了解一个完整的数据采集-处理-输出链路是怎么搭起来的——那这篇内容应该能给你一些可以直接抄作业的思路。
我做这个项目的出发点很朴素:我不想每天早上花四十分钟刷各种渠道,我想花五分钟看一份已经整理好的东西。这个需求足够具体,也足够普遍。下面我把整个项目的设计思路、技术选型、实操步骤、踩过的坑,以及一些只有真正跑起来才会发现的细节,完整地拆开来讲。
2. 信息源的选择与采集策略:不是越多越好
2.1 信息源的分类与优先级划分
一开始我犯了一个很典型的错误:觉得信息源越多越好。我列了一个清单,把能想到的AI相关网站、博客、论坛、社交媒体账号全部加了进去,大概有三十多个。结果第一天跑下来,采集到的原始条目超过一千条,去重之后还有六百多条,人工根本看不过来。更糟糕的是,很多源的内容质量很低,要么是搬运,要么是标题党,要么是纯粹的广告。
后来我重新梳理了信息源的分类逻辑。我把它们分成三个层级:
- 核心源:内容质量高、更新频率稳定、与AI领域直接相关。这类源每天贡献的条目不多,但几乎每条都值得看。比如几个头部AI研究机构的官方博客、几个我长期跟踪的技术团队的个人博客。
- 补充源:覆盖面广、更新频繁,但内容质量参差不齐。这类源需要配合更严格的筛选规则使用。比如一些综合科技媒体的AI频道、几个活跃的技术社区。
- 观察源:更新不规律,但偶尔会有独家信息。这类源不纳入每日采集,而是每周扫一次,作为补充。
这个分类的关键在于:不同层级的源,采集频率、筛选阈值、处理方式都应该不同。核心源可以全量采集,补充源需要关键词过滤,观察源只需要定期扫描。
2.2 采集方式的技术选型
采集方式的选择取决于源的类型。我实际用到的有三种:
第一种是RSS/Atom订阅。这是最省事的方式,大部分博客和新闻站点都提供RSS输出。用Python的feedparser库就能解析,代码量极少。但问题是,现在很多站点已经不再维护RSS了,或者只输出摘要不输出全文。
第二种是API调用。一些平台提供公开的API接口,返回结构化的JSON数据。这种方式最稳定,但需要处理认证、限流、分页等问题。我一般会用requests库配合简单的重试逻辑来处理。
第三种是网页解析。对于没有RSS也没有API的站点,只能用BeautifulSoup或lxml来解析HTML。这种方式最脆弱,因为页面结构一变,解析规则就失效了。我的做法是给每个需要网页解析的源单独写一个解析函数,并且加上异常捕获和日志记录,一旦解析失败就发通知给我,而不是静默失败。
import feedparser import requests from bs4 import BeautifulSoup from datetime import datetime, timedelta def fetch_rss(url, timeout=10): """采集RSS源,返回条目列表""" try: feed = feedparser.parse(url) entries = [] for entry in feed.entries: published = entry.get('published_parsed') if published: pub_time = datetime(*published[:6]) # 只保留最近24小时的条目 if datetime.now() - pub_time > timedelta(hours=24): continue entries.append({ 'title': entry.get('title', ''), 'link': entry.get('link', ''), 'summary': entry.get('summary', ''), 'source': feed.feed.get('title', url), 'published': entry.get('published', '') }) return entries except Exception as e: print(f"RSS采集失败: {url}, 错误: {e}") return []这段代码看起来简单,但有几个细节值得注意。时间过滤是必须的,否则你会反复采集到旧内容。异常捕获也是必须的,因为网络请求随时可能失败。另外,feedparser对格式不规范的RSS容忍度比较高,但偶尔也会解析出空条目,需要在后续处理中去掉。
2.3 采集频率与去重机制
采集频率我设定为每天两次:早上六点和下午两点。早上那次是主力采集,覆盖前一天下午到当天早上的更新;下午那次是补充采集,抓取上午新增的内容。这样做的原因是,很多源在北京时间凌晨更新,如果只在早上采集,可能会漏掉一些下午发布的内容。
去重是采集环节最容易被忽视但最重要的一步。我的去重策略分两层:
第一层是基于URL的去重。同一个链接如果已经采集过,就不再重复处理。我用一个简单的SQLite表来存储已采集的URL,每次采集前先查询。
第二层是基于标题相似度的去重。不同源可能会报道同一件事,标题措辞不同但内容高度相似。我用difflib.SequenceMatcher计算标题的相似度,超过0.8的视为重复,只保留最早采集到的那条。
from difflib import SequenceMatcher def is_similar(title1, title2, threshold=0.8): """判断两个标题是否相似""" return SequenceMatcher(None, title1, title2).ratio() > threshold def deduplicate(entries): """对条目列表进行去重""" unique = [] for entry in entries: is_dup = False for existing in unique: if is_similar(entry['title'], existing['title']): is_dup = True break if not is_dup: unique.append(entry) return unique这个去重逻辑在条目数量不大的时候完全够用。但如果条目超过几百条,两两比较的时间复杂度是O(n²),会明显变慢。我后来加了一个优化:先按标题的前十个字符分组,只在同一组内做相似度比较。这样能把比较次数降低一个数量级。
3. 从原始条目到可用日报:筛选、分类与摘要生成
3.1 筛选规则的设计逻辑
采集到的原始条目,质量参差不齐。如果直接全部输出,日报就变成了信息垃圾场。筛选的目的是把真正有价值的内容留下来。
我的筛选规则分三个维度:
关键词匹配。我维护了一个关键词列表,包括AI领域的核心术语、我关注的技术方向、以及一些我明确不想看到的内容的排除词。条目标题或摘要中命中核心关键词的加分,命中排除词的直接丢弃。
来源权重。核心源的条目默认保留,补充源的条目需要命中至少两个核心关键词才保留,观察源的条目需要命中三个以上才保留。
时效性。超过48小时的条目直接丢弃,除非它来自核心源且标题中包含"发布""开源""上线"等动作词。
这套规则听起来有点复杂,但实际跑起来效果很好。它把每天六百多条原始条目压缩到了三十到五十条左右,而且大部分都是我真正想看的内容。
提示:筛选规则不要一开始就设得太严。我最初把阈值设得很高,结果每天只剩五六条,漏掉了很多有价值的内容。后来我先把阈值放宽,跑了一周,人工检查哪些该留的没留、哪些不该留的留了,再反过来调整规则。这个过程大概迭代了三次才稳定下来。
3.2 自动分类的实现方式
日报如果不分类,读起来还是很累。我把条目分成了几个大类:模型与算法、工具与框架、行业动态、开源项目、论文速递。分类的方式有两种:
一种是基于关键词的规则分类。比如标题里出现"开源""GitHub""repo"就归到开源项目,出现"论文""arXiv""paper"就归到论文速递。这种方式简单直接,准确率大概在七成左右。
另一种是基于文本分类模型的自动分类。我用一个轻量的文本分类模型,把标题和摘要作为输入,输出类别标签。这个方式准确率更高,但需要标注数据来训练。我的做法是先用规则分类跑一段时间,把人工修正过的结果作为训练数据,再训练模型来替换规则。
实际使用中,我采用的是混合策略:规则分类先跑一遍,对于置信度不高的条目,再用模型做二次判断。这样既保证了速度,又提高了准确率。
3.3 摘要生成的取舍
摘要生成是日报的核心价值之一。原始条目的摘要往往要么太长,要么太短,要么干脆没有。我的目标是生成一句话到两句话的摘要,让读者快速判断这条内容是否值得点进去看。
我试过几种方案:
直接截取原文前N个字符。最简单,但效果最差。经常截到一半句子就断了,读起来很难受。
基于规则的抽取式摘要。从原文中抽取包含关键词的句子,拼在一起。效果比截取好一些,但有时候抽出来的句子之间缺乏连贯性。
基于生成式模型的摘要。用一个小型的生成模型,把原文输入,输出摘要。效果最好,但需要额外的计算资源,而且偶尔会产生事实性错误。
最终我采用的是抽取式为主、生成为辅的策略。对于大部分条目,用抽取式方法生成摘要;对于核心源的重要条目,用生成式模型重新生成摘要,并且我会人工过一遍。这样在效果和成本之间找到了一个平衡点。
def extract_summary(text, max_length=120): """基于规则的抽取式摘要""" if not text: return "" # 按句号、问号、感叹号分句 import re sentences = re.split(r'[。!?.!?]', text) sentences = [s.strip() for s in sentences if len(s.strip()) > 10] if not sentences: return text[:max_length] # 取前两句,如果太长就截断 summary = '。'.join(sentences[:2]) if len(summary) > max_length: summary = summary[:max_length] + '...' return summary这个函数很粗糙,但实际用下来,对于新闻类内容效果还可以。它的优点是快、稳定、不会产生幻觉。对于技术博客类的长文,我会把max_length调大一些,并且优先抽取包含技术术语的句子。
4. 日报的排版与输出:让阅读体验对得起采集成本
4.1 输出格式的选择
日报最终要输出成什么格式?我考虑过几种:
纯文本。最简单,但可读性差,无法做富文本排版。
Markdown。可读性好,兼容性强,可以方便地转换成其他格式。我最终选择了Markdown作为主要输出格式。
HTML。排版能力最强,但需要额外的样式表,而且在不同平台上显示效果不一致。
PDF。适合打印和存档,但生成成本高,不适合每日更新。
Markdown的好处是,它既可以作为纯文本阅读,也可以渲染成网页,还可以直接粘贴到大多数内容管理系统中。我生成的日报Markdown文件,可以直接推送到我的笔记软件、博客系统或者邮件里。
4.2 排版结构的实际设计
一份日报的排版结构,直接决定了阅读体验。我经过多次调整,最终确定的排版结构是这样的:
开头是一段简短的概览,用两三句话总结今天最重要的几条动态。然后是分类列表,每个分类下面用无序列表列出条目,每条包含标题、来源、一句话摘要和链接。最后是一个"今日观察"小节,用一两段话写我对某些条目的个人看法或补充信息。
这个结构的关键在于:概览要短,列表要清晰,观察要有信息增量。概览如果太长,读者就不往下看了;列表如果格式不统一,读起来会很累;观察如果只是复述列表内容,就没有存在的必要。
# AI 日报 · 2026-10-05 今日共收录 32 条动态,其中模型与算法 8 条,工具与框架 7 条, 行业动态 9 条,开源项目 5 条,论文速递 3 条。 ## 模型与算法 - **某研究团队发布新一代多模态模型** 该模型在多个基准测试上取得了领先成绩,支持文本、图像和音频的联合理解。 [来源:某研究机构博客](链接) - **某公司开源轻量级推理框架** 框架针对边缘设备优化,内存占用降低约四成,支持多种量化格式。 [来源:某技术社区](链接) ## 今日观察 今天最值得关注的是某研究团队的多模态模型。从公开的技术报告来看, 它在训练数据规模和架构设计上都有明显创新。不过目前还没有开放权重, 实际效果有待验证。另外,某公司的推理框架开源,对于需要在本地部署 模型的开发者来说是个好消息,我打算这周抽时间跑一下它的示例。这个排版看起来简单,但有几个细节是经过反复调整的。比如,每条条目的摘要控制在两行以内,超过两行的内容会被截断。比如,来源和链接放在摘要后面,用方括号标注,方便快速识别。比如,分类之间的顺序是固定的,不会因为当天某个分类条目多就把它提前。
4.3 推送渠道的配置
日报生成之后,需要推送到我日常使用的渠道。我配置了三个推送方式:
邮件推送。用SMTP协议把Markdown渲染成HTML邮件发送。这种方式最稳定,而且邮件客户端对HTML的支持很好。
笔记软件同步。通过API把Markdown文件推送到我的笔记软件里,方便后续检索和整理。
本地存档。每天生成的Markdown文件按日期命名,保存在本地目录里,作为长期存档。
这三个渠道的配置都不复杂,但有一个共同的注意事项:推送失败要有通知。我遇到过好几次因为网络问题或者API变更导致推送失败,但因为没有通知机制,过了好几天才发现。后来我加了一个简单的检查:每次推送后,如果返回状态不是成功,就发一条通知到我的手机上。
5. 跑起来之后才会遇到的坑:我的踩坑记录与修复过程
5.1 编码问题导致的乱码
这个问题看起来很小,但折腾了我整整一个下午。采集回来的内容里,中文、英文、日文混杂,有些源用的是UTF-8,有些用的是GBK,还有些用的是Latin-1。如果不做编码处理,生成的日报里就会出现乱码。
我的修复方案是在采集环节统一做编码转换。对于RSS源,feedparser会自动处理编码,一般不需要额外操作。对于网页解析,我在请求头里明确指定Accept-Charset: utf-8,并且在解析之前先用response.encoding检查编码,如果不是UTF-8就手动转换。
def fetch_html(url, timeout=10): """采集网页内容,统一编码为UTF-8""" headers = { 'User-Agent': 'Mozilla/5.0 (compatible; DailyBot/1.0)', 'Accept-Charset': 'utf-8' } try: resp = requests.get(url, headers=headers, timeout=timeout) if resp.encoding and resp.encoding.lower() != 'utf-8': resp.encoding = 'utf-8' return resp.text except Exception as e: print(f"网页采集失败: {url}, 错误: {e}") return ""这个函数的关键在于resp.encoding = 'utf-8'这一行。requests库会根据响应头自动猜测编码,但猜测经常不准。手动指定UTF-8之后,大部分乱码问题就解决了。
5.2 时间戳的时区混乱
另一个让我头疼的问题是时间戳。不同源返回的时间格式不一样,有的带时区信息,有的不带,有的用的是Unix时间戳,有的用的是字符串。如果不统一处理,按时间筛选和排序就会出错。
我的做法是:所有时间戳在采集阶段就统一转换成UTC时间,存储时也存UTC,只在最终输出日报的时候转换成北京时间。这样中间环节不会因为时区问题产生混乱。
from datetime import datetime, timezone, timedelta def parse_time(time_str): """解析各种格式的时间字符串,返回UTC时间""" if not time_str: return None # 尝试常见的几种格式 formats = [ '%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%d %H:%M:%S', '%a, %d %b %Y %H:%M:%S %z', '%a, %d %b %Y %H:%M:%S %Z', ] for fmt in formats: try: dt = datetime.strptime(time_str, fmt) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(timezone.utc) except ValueError: continue return None这个函数覆盖了我遇到的大部分时间格式。如果遇到解析不了的,就返回None,在后续处理中把这条条目的时间标记为"未知",而不是直接丢弃。
5.3 摘要生成中的事实性错误
这个问题出现在我使用生成式模型做摘要的时候。有一次,模型把一条关于"某公司发布新模型"的新闻,摘要成了"某公司发布了新的硬件产品"。虽然只有几个字的差别,但意思完全变了。如果我没有人工检查,这条错误信息就会出现在日报里。
我的修复方案是:生成式摘要只用于核心源的重要条目,并且必须经过人工确认才能发布。对于其他条目,一律使用抽取式摘要。抽取式摘要虽然不够精炼,但它不会产生事实性错误,因为所有内容都来自原文。
另外,我在生成式摘要的提示词里加了一条约束:"只使用原文中出现的信息,不要添加任何原文没有的内容。"这条约束在一定程度上减少了幻觉,但不能完全消除。所以人工确认这一步不能省。
5.4 采集频率过高导致的访问限制
项目刚跑起来的时候,我把采集频率设成了每小时一次。结果第二天就发现,有几个源开始返回429状态码,意思是请求太频繁了。更严重的是,有一个源的IP直接把我封了。
我的修复方案是:降低采集频率,并且给每个源加上独立的延迟。具体来说,采集频率从每小时一次降到每天两次;对于同一个源,如果连续两次请求之间的间隔小于五秒,就强制等待。另外,我在请求头里加上了合理的User-Agent,表明这是一个自动化的日报工具,而不是恶意爬虫。
import time last_request_time = {} def rate_limited_fetch(url, source_name, min_interval=5): """带频率限制的采集函数""" now = time.time() if source_name in last_request_time: elapsed = now - last_request_time[source_name] if elapsed < min_interval: time.sleep(min_interval - elapsed) result = fetch_html(url) last_request_time[source_name] = time.time() return result这个简单的频率限制逻辑,基本上解决了访问限制的问题。如果某个源特别敏感,我会把min_interval调得更大,比如十秒或十五秒。
6. 让日报持续跑下去:维护心得与扩展思路
6.1 日常维护中最耗时的环节
项目跑稳定之后,日常维护最耗时的环节不是技术问题,而是信息源的更新。互联网上的信息源变化很快,有的博客停止更新了,有的网站改版了,有的RSS地址失效了。如果不定期检查,采集成功率会慢慢下降。
我的做法是每周花十五分钟做一次巡检:检查每个源的最近采集时间,如果超过一周没有新内容,就手动去看一下是源本身停止更新了,还是采集规则失效了。如果是后者,就更新解析规则;如果是前者,就把这个源从列表里移除,或者移到观察源里。
这个巡检工作看起来不起眼,但它是保证日报质量的关键。我见过很多类似的聚合项目,一开始效果很好,但因为没有持续维护,几个月后采集成功率降到一半以下,最终就废弃了。
6.2 日报内容的个性化调整
日报的内容不是一成不变的。随着我对AI领域的关注点变化,筛选规则和分类方式也需要调整。比如,有一段时间我对某个技术方向特别感兴趣,就在关键词列表里加了相关的术语,并且把相关条目的权重调高。过了一段时间兴趣转移了,再把权重调回来。
这种个性化调整是日报工具相对于通用资讯平台的优势所在。通用平台为了照顾大多数用户,只能做平均化的推荐;而自己的日报工具,可以完全按照自己的需求来定制。
6.3 后续可以扩展的方向
这个项目目前只覆盖了文本类信息源。如果继续扩展,有几个方向可以考虑:
多模态内容的采集。现在很多AI动态是以视频、播客、直播的形式发布的。如果能把这些内容也纳入采集范围,日报的覆盖面会更广。技术上的难点在于,视频和音频需要先转写成文本,才能进行后续的筛选和摘要。
个性化推荐的引入。目前筛选主要靠关键词和来源权重,比较粗糙。如果引入一个简单的推荐模型,根据我过去的阅读行为来调整条目的排序,日报的阅读效率会更高。
协作与分享功能。如果能把日报分享给团队成员,并且支持多人标注和评论,它就从个人工具变成了团队工具。这个方向的想象空间很大,但实现复杂度也高很多。
不过,这些都是后话。目前这个版本已经能满足我的核心需求:每天早上花五分钟,看完一份经过筛选和整理的AI日报。这个价值已经足够让我继续维护它了。
注意:如果你也想搭一个类似的工具,我的建议是从最小的可用版本开始。先接两三个核心源,跑通采集-筛选-输出的完整链路,再逐步增加源和功能。不要一开始就追求大而全,那样很容易在细节里迷失,最后什么都做不出来。