基于Scrapy与BERT的AI新闻聚合系统:从爬虫到NLP的自动化实践
1. 项目概述一个AI新闻聚合器的诞生最近在捣鼓一个叫“Daily AI News”的小项目起因很简单作为一个每天需要追踪大量AI领域动态的从业者我发现自己被信息淹没了。每天打开十几个网站、订阅几十个邮件列表光是筛选和整理就要花掉一两个小时。更头疼的是很多信息源质量参差不齐重复内容多真正有价值的前沿论文、重磅产品更新或者深度行业分析反而容易被淹没在噪音里。于是我决定自己动手打造一个能自动聚合、筛选并结构化呈现每日AI新闻的工具。这个项目不是为了替代专业媒体而是想做一个为我以及像我一样需要高效获取信息的同行们服务的“信息过滤器”。这个“Daily AI News — 2026-04-09”就是一个具体的产出样例。它不是一个简单的链接列表而是一个经过处理的、包含摘要、分类、关键实体如公司、模型名甚至初步情感倾向分析的结构化日报。它的核心目标是在每天早晨用最短的时间比如5-10分钟让读者对前一天AI领域发生的关键事件有一个清晰、可靠的概览。适合的人群包括AI工程师、产品经理、投资人、研究者以及任何希望系统化跟进AI进展但又不想在信息海洋里手动游泳的人。2. 核心设计思路从信息洪流到知识简报2.1 需求拆解我们到底需要什么样的“新闻”在动手之前我花了些时间明确需求。一个理想的AI日报应该具备以下几个特征全面性与权威性覆盖主流学术平台ArXiv, OpenReview、头部科技媒体TechCrunch, The Verge的AI板块、知名公司官方博客OpenAI, Google AI, Meta AI、以及重要的开源社区动态Hugging Face, GitHub Trending。不能有重大遗漏。去重与聚合同一事件被多个来源报道时应合并为一条并标注主要信息来源避免重复阅读。结构化与可读性每条新闻不应只是一个标题和链接。需要包含核心摘要用一两句话说明发生了什么。关键实体提取涉及的公司、研究机构、模型/产品名称、核心技术术语。分类标签如“大语言模型”、“计算机视觉”、“机器人”、“伦理与治理”、“融资并购”等。影响力预估一个简单的分级如高、中、低帮助判断优先级。自动化与可扩展整个过程应尽可能自动化减少人工干预。同时数据源和处理逻辑应该易于增删改查。基于这些需求整个系统的设计思路就清晰了一个由爬虫信息收集、NLP管道信息处理、生成与发布模块信息呈现组成的自动化流水线。2.2 技术栈选型为什么是它们在技术选型上我遵循“成熟、高效、易于维护”的原则。信息收集层爬虫Scrapy Scrapy-Redis对于需要处理JavaScript渲染的现代网站如很多科技媒体Scrapy框架成熟稳定结合scrapy-splash或playwright可以解决动态加载问题。使用Scrapy-Redis是为了实现分布式爬取提高效率并为后续可能的扩展留有余地。RSS/Atom订阅对于提供标准订阅源如ArXiv, 官方博客的网站这是最规范、对服务器最友好的方式。使用feedparser库进行解析。GitHub API / 其他开放API对于GitHub趋势项目或一些提供开放API的平台直接调用API是首选比爬取HTML更稳定、更高效。注意严格遵守网站的robots.txt协议设置合理的请求间隔如1-2秒/请求避免对目标服务器造成压力。这是基本的网络礼仪和合规要求。信息处理层NLP管道核心模型我选择了开源模型而非直接调用闭源API如GPT-4。原因有三一是成本可控二是数据隐私有保障所有处理在本地或私有服务器三是可定制化程度高。具体来说我使用BERT或RoBERTa的变体进行文本分类打标签和命名实体识别NER使用BART或T5进行文本摘要。向量数据库与去重使用Sentence-BERT将新闻标题和摘要转换为向量存入ChromaDB或Qdrant这类轻量级向量数据库。每天的新内容在入库前会与历史向量进行相似度计算如余弦相似度超过阈值如0.85则判定为重复新闻进行合并处理。轻量级情感/影响力分析这里没有用复杂的模型而是基于关键词词典和规则结合分类模型输出的置信度做一个简单的影响力分级。例如涉及“GPT-5”、“重大突破”、“融资数亿美元”等关键词的影响力标记为“高”。生成与发布层模板引擎使用Jinja2来生成结构化的日报Markdown/HTML。模板里定义了日期、分类区块、每条新闻的呈现格式标题、摘要、来源、标签等。静态站点生成将生成的日报内容通过Hugo或Jekyll这类静态站点生成器发布成一个独立的网页。这样做的好处是部署简单、访问速度快、成本极低可以放在GitHub Pages或Cloudflare Pages上。通知推送通过Telegram Bot或邮件列表使用sendgrid或mailchimp的API将日报摘要推送给订阅者。这个技术栈的核心思想是用成熟的、模块化的开源工具搭建管道将复杂的AI能力NLP作为管道中的一环最终输出一个轻量级、可静态部署的产品。3. 实操构建一步步搭建你的AI新闻流水线3.1 第一步搭建信息收集网络信息源的质量决定了日报的上限。我建立了一个名为sources.yaml的配置文件来管理所有信息源分为以下几类sources: academic: - name: ArXiv CS.AI url: http://arxiv.org/rss/cs.AI type: rss - name: OpenReview Recent url: https://api.openreview.net/notes?invitationICLR.cc/* type: api tech_news: - name: TechCrunch AI url: https://techcrunch.com/category/artificial-intelligence/ type: scrapy # 需要配置具体的XPath或CSS选择器来提取文章标题、链接和摘要 - name: The Verge AI url: https://www.theverge.com/ai-artificial-intelligence type: scrapy company_blogs: - name: OpenAI Blog url: https://openai.com/blog type: rss - name: Google AI Blog url: https://ai.googleblog.com/ type: rss community: - name: Hugging Face Blog url: https://huggingface.co/blog type: rss - name: GitHub Trending AI/ML url: https://api.github.com/search/repositories?qtopic:aicreated:2024-04-08sortstars type: api对于type: scrapy的源需要编写对应的Spider。以TechCrunch为例一个简化的Spider核心部分如下import scrapy from datetime import datetime, timedelta class TechCrunchAISpider(scrapy.Spider): name techcrunch_ai start_urls [https://techcrunch.com/category/artificial-intelligence/] def parse(self, response): # 提取文章列表 articles response.css(div.river article) for article in articles: title article.css(h2 a::text).get() link article.css(h2 a::attr(href)).get() # 提取发布时间用于过滤只抓取最近24小时的 time_str article.css(time::attr(datetime)).get() pub_time datetime.fromisoformat(time_str.replace(Z, 00:00)) if datetime.utcnow() - pub_time timedelta(hours24): yield scrapy.Request(urllink, callbackself.parse_article, meta{title: title, pub_time: pub_time}) def parse_article(self, response): # 进入文章详情页提取正文和摘要 content .join(response.css(div.article-content p::text).getall()) # 这里可以调用后续的摘要函数简易版可以取前两段 summary self.extract_summary(content) yield { title: response.meta[title], url: response.url, summary: summary, source: TechCrunch, publish_time: response.meta[pub_time], raw_content: content[:5000] # 存储部分原始内容供后续处理 }实操心得防反爬策略一定要设置DOWNLOAD_DELAY并使用scrapy-rotating-proxies中间件轮换用户代理User-Agent。对于特别顽固的网站可能需要模拟登录或使用更高级的浏览器自动化工具如Playwright。错误处理网络请求总会有失败。Scrapy有重试机制但要为每个Spider设置合理的重试次数和超时时间并做好日志记录方便排查哪些源经常出问题。时间过滤这是保证日报“每日”属性的关键。在爬取时就要根据文章的发布时间进行过滤只处理过去24小时内的内容。否则数据量会失控。3.2 第二步构建NLP处理管道收集到的原始数据Raw Item会先存入一个临时数据库如SQLite或PostgreSQL然后由NLP管道依次处理。我构建了一个processors.py模块里面包含几个关键的处理器import torch from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification from sentence_transformers import SentenceTransformer import chromadb class NewsProcessor: def __init__(self): # 1. 加载摘要模型 self.summarizer pipeline(summarization, modelfacebook/bart-large-cnn) # 2. 加载分类模型预训练或微调过的 self.classifier pipeline(zero-shot-classification, modelfacebook/bart-large-mnli) self.candidate_labels [大语言模型, 多模态, 计算机视觉, 强化学习, 机器人, 伦理与安全, 行业应用, 融资并购, 开源项目, 硬件] # 3. 加载NER模型 self.ner pipeline(ner, modeldslim/bert-base-NER) # 4. 加载句子编码模型用于去重 self.encoder SentenceTransformer(all-MiniLM-L6-v2) # 5. 连接向量数据库 self.chroma_client chromadb.PersistentClient(path./news_db) self.collection self.chroma_client.get_or_create_collection(namedaily_news) def process_item(self, item): 处理单条新闻 # 去重检查 if self.is_duplicate(item[title], item[summary]): return None # 重复丢弃 # 生成摘要如果原始摘要不够好 if len(item[summary]) 50: # 假设原始摘要太短 item[summary] self.summarizer(item[raw_content][:1024], max_length100, min_length30, do_sampleFalse)[0][summary_text] # 分类 classification self.classifier(item[title] item[summary], self.candidate_labels) item[tags] classification[labels][:3] # 取置信度最高的3个标签 # 命名实体识别 ner_results self.ner(item[title] item[summary]) # 过滤出组织ORG和产品/技术MISC类实体 companies set([ent[word] for ent in ner_results if ent[entity] in [B-ORG, I-ORG]]) technologies set([ent[word] for ent in ner_results if ent[entity] in [B-MISC, I-MISC]]) item[entities] { companies: list(companies), technologies: list(technologies) } # 简单影响力判断基于规则和关键词 item[impact] self.assess_impact(item) # 处理完成后将向量存入数据库以备下次去重 self.collection.add( documents[item[title]], embeddings[self.encoder.encode(item[title]).tolist()], metadatas[{url: item[url]}] ) return item def is_duplicate(self, title, summary): 基于向量相似度判断是否重复 query_embedding self.encoder.encode(title).tolist() results self.collection.query( query_embeddings[query_embedding], n_results3 ) if results[distances][0] and min(results[distances][0]) 0.15: # 阈值可调 return True return False def assess_impact(self, item): 简易影响力评估 high_impact_keywords [突破, 重大, 发布, gpt-4, 融资, 收购, 合并] text (item[title] item[summary]).lower() if any(kw in text for kw in high_impact_keywords): return 高 elif 研究 in text or 新方法 in text: return 中 else: return 低实操心得模型选择与本地部署facebook/bart-large-cnn和facebook/bart-large-mnli在摘要和零样本分类上表现不错且模型大小相对适中。如果服务器资源有限可以考虑更小的模型如t5-small。所有模型最好提前下载到本地避免运行时下载。处理速度与批处理逐条处理新闻会很慢。可以将一批新闻如10-20条的标题和摘要拼接起来批量进行分类和NER能显著提升速度。transformers库的pipeline支持传入列表进行批处理。向量去重的阈值相似度阈值代码中的0.15需要根据实际效果调整。太严格会导致同一事件的不同报道无法合并太宽松则会漏掉重复。建议先用一批历史数据测试观察不同阈值下的合并效果。分类标签的维护candidate_labels不是一成不变的。需要定期回顾根据AI领域的热点变化进行调整。例如如果“AI代理”或“代码生成”成为新热点就应该加入标签候选列表。3.3 第三步日报生成与自动化发布处理完的所有新闻条目会按impact影响力降序排列然后按tags标签分组。接下来使用Jinja2模板生成最终的日报。template.md.j2模板示例# Daily AI News — {{ date }} 本日报由自动化系统生成汇总了过去24小时内AI领域的重要动态。共收录 {{ total_news }} 条新闻。 {% for tag, items in news_by_tag.items() if items %} ## {{ tag }} {% for item in items %} ### {{ item.title }} **摘要**: {{ item.summary }} **来源**: [{{ item.source }}]({{ item.url }}) **关键实体**: {{ item.entities.companies | join(, ) }} | {{ item.entities.technologies | join(, ) }} **影响力**: {{ item.impact }} --- {% endfor %} {% endfor %} *生成于 {{ generation_time }} 分类模型: BART-MNLI, 摘要模型: BART-CNN*生成脚本generate_daily.pyfrom datetime import datetime import jinja2 import json def generate_daily(news_items, output_path./output): today datetime.utcnow().strftime(%Y-%m-%d) # 按标签分组 news_by_tag {} for item in news_items: for tag in item[tags]: news_by_tag.setdefault(tag, []).append(item) # 每个标签下的新闻按影响力排序 for tag in news_by_tag: news_by_tag[tag].sort(keylambda x: {高: 0, 中: 1, 低: 2}[x[impact]]) # 渲染模板 env jinja2.Environment(loaderjinja2.FileSystemLoader(./templates)) template env.get_template(daily.md.j2) output template.render( datetoday, total_newslen(news_items), news_by_tagnews_by_tag, generation_timedatetime.utcnow().strftime(%Y-%m-%d %H:%M UTC) ) # 保存 filename f{output_path}/daily_ai_news_{today}.md with open(filename, w, encodingutf-8) as f: f.write(output) print(f日报已生成: {filename}) return filename最后通过Git Actions或简单的Cron Job将整个流程自动化。一个典型的cron任务设置如下假设在Linux服务器上0 8 * * * cd /path/to/daily-ai-news /usr/bin/python3 /path/to/daily-ai-news/run_pipeline.py /path/to/logs/cron.log 21这表示每天UTC时间早上8点可根据你希望的发布时间调整运行一次完整的流水线。实操心得发布时机选择在目标读者早晨开始工作前发布例如本地时间早上7-8点这样他们一上班就能看到新鲜出炉的日报。静态化部署将生成的Markdown文件提交到GitHub仓库利用GitHub Pages自动构建并发布成网站。这样完全免费且全球访问速度快。备份与监控数据库存储历史新闻和日志文件一定要定期备份。监控脚本的运行状态如果爬虫失败或NLP模型报错能及时收到通知可以通过邮件或Telegram Bot发送报警信息。4. 常见问题与优化实录在开发和运行这个系统的过程中我遇到了不少坑也总结了一些优化经验。4.1 信息源失效与反爬升级这是最常遇到的问题。科技媒体网站的前端结构经常变动导致XPath或CSS选择器失效。问题某天发现TechCrunch的新闻列表CSS类名从div.river改成了div.feed爬虫抓不到数据了。排查检查爬虫日志发现返回的HTML结构里没有预期的内容。手动访问页面用浏览器开发者工具检查元素结构确认了变化。解决更健壮的选择器避免使用过于具体、易变的类名。尝试使用标签语义或相对位置来定位如article h2 a。备用方案对于关键源可以同时配置RSS源如果可用作为备份。RSS的结构通常更稳定。定期检查脚本将“源健康检查”作为每周例行任务。写一个简单的测试脚本定期访问各信息源检查是否能正常解析出标题和链接失败则报警。优化技巧为每个Spider配置一个“降级解析器”。当主解析逻辑失败时尝试一个更通用但可能不够精确的备用解析方法比如用readability这样的库直接提取正文至少保证有内容而不是完全失败。4.2 NLP模型处理效果不佳预训练模型在特定领域如充满专业术语的AI新闻上的表现可能打折扣。问题分类模型经常把一些关于“AI芯片”的新闻错误地分到“硬件”或“行业应用”而不是更具体的“硬件”。排查分析错误分类的样本发现模型对“TPU”、“NPU”、“光计算”等专业硬件术语不敏感。解决微调模型收集几百条正确标注的AI新闻数据在预训练模型如BERT的基础上进行轻量级微调。即使数据量不大针对特定领域的微调也能显著提升效果。后处理规则在模型预测的基础上增加一层基于关键词的规则修正。例如如果新闻正文中出现“晶体管”、“算力”、“功耗”等词且模型预测的标签是“行业应用”则强制将其改为“硬件”。优化候选标签将“硬件”标签细化为“AI芯片”和“计算基础设施”让模型做更细粒度的区分。优化技巧摘要模型有时会生成“车轱辘话”或丢失关键信息。可以尝试“抽取式摘要”与“生成式摘要”结合。先用TextRank等算法从原文中提取关键句再让生成模型对这些关键句进行润色和压缩效果往往更稳定。4.3 去重逻辑的“过度”与“不足”去重是保证日报简洁的关键但阈值很难一劳永逸。问题同一研究论文被ArXiv预印本和多家科技媒体报道系统有时无法合并不足有时又把两个不同但主题相近的新闻合并了过度。排查查看被错误合并或分开的新闻对分析它们的标题、摘要向量相似度。解决多维度去重不仅仅依赖标题向量相似度。可以结合以下特征综合判断URL域名来自同一域名的相似内容合并概率更高。发布时间非常接近的发布时间是重复报道的强信号。关键实体重叠度如果两篇新闻都提到了相同的公司、模型名合并的可能性增加。聚类而非简单阈值对于一天内的所有新闻可以先进行聚类如使用DBSCAN算法将相似的新闻聚成一类再从每一类中选出一条最具代表性如来源最权威、内容最全面的作为主新闻其他作为“相关报道”附在后面。优化技巧维护一个“白名单”和“黑名单”。对于某些总是报道独家深度内容的源如某位知名研究者的博客即使标题相似也尽量不与其他源合并白名单。对于某些经常转载、内容质量不高的源可以设置更严格的合并阈值甚至直接将其内容作为已有新闻的补充而不单独显示黑名单。4.4 系统性能与成本随着信息源增加爬取和处理时间会变长。问题流水线运行时间从最初的15分钟增加到超过1小时影响了日报的及时性。排查使用性能分析工具如Python的cProfile发现瓶颈主要在两个方面一是网络请求尤其是动态渲染的页面二是NLP模型推理。解决异步爬取将Scrapy替换为aiohttpasyncio或Scrapy与asyncio结合实现真正的并发请求大幅缩短IO等待时间。模型服务化将NLP模型分类、摘要、NER部署为独立的API服务如使用FastAPI并启用批处理接口。这样爬虫收集完数据后可以一次性发送一批数据给模型服务减少模型加载和通信开销。可以使用Docker容器化部署方便扩展。缓存策略对于一天内不变的信息如GitHub Trending列表可以缓存结果避免重复API调用。优化技巧对于摘要任务如果原文很长不要一次性输入整个文章。可以先进行分句然后使用TextRank选出最重要的几个句子再将这些句子送给生成模型做摘要。这比直接处理长文本要快得多效果也更好。运行这个系统一年多它已经成了我工作中不可或缺的信息助手。最大的体会是自动化不是一劳永逸而是一个持续迭代和“调教”的过程。信息源会变AI领域的热点会转移模型也需要更新。我每周会花半小时快速浏览一下生成的日报手动修正一些明显的分类错误并把漏掉的重要新闻手动加进去。这些人工反馈又会成为优化分类模型和去重规则的数据。这个过程本身也让我对AI领域的动态有了更深、更结构化的理解。如果你也想尝试可以从最核心的三五个信息源开始用最简单的脚本跑起来再逐步丰富功能。最重要的不是一步到位做出完美的系统而是先让这个信息流为你自己转起来。