基于边缘计算的新闻聚合器:Cloudflare Workers与GitHub Actions实践

基于边缘计算的新闻聚合器:Cloudflare Workers与GitHub Actions实践 1. 项目概述边缘新闻聚合器的诞生最近在折腾一个挺有意思的小项目我把它叫做“News — At The Edge”。这个名字听起来有点玄乎其实核心想法很简单我想做一个能自动抓取、聚合并整理当天重要新闻的工具但它的“大脑”不是放在某个固定的服务器上而是运行在“边缘”——也就是更靠近用户、更轻量、更分散的计算节点上。这个想法的起因是我自己每天被各种新闻App推送轰炸信息过载严重但又不想错过真正重要的行业动态。市面上的聚合器要么太重功能繁杂要么太“傻”推荐算法让人看不懂要么就是隐私问题让人担忧。于是我就琢磨着能不能自己搞一个它不需要全天候运行一个庞大的后台服务而是像一只勤劳的“数字蜜蜂”每天在特定的时间点比如早上8点18分这也是项目标题里“8/18”的灵感来源之一既指日期也暗合一个理想的信息获取时刻出动一次飞到几个我信赖的信源比如几家主流科技媒体的RSS、几个特定主题的Subreddit、或者Hacker News的特定板块把精华“采”回来然后经过简单的清洗、去重和优先级排序生成一份干净、简洁的每日简报通过最轻量的方式比如邮件或Telegram Bot推送到我面前。这个项目的核心价值对我而言是“可控的自动化”和“隐私友好的聚合”。我不需要把我的阅读习惯交给某个中心化的平台去分析所有的抓取、处理逻辑我都一清二楚甚至可以根据心情随时调整信源和过滤规则。它非常适合像我这样的开发者、内容创作者或者任何对特定领域信息有持续需求但又希望保持信息获取主动权和个人数据边界的朋友。整个系统设计追求极简和高效目标是能用最低的资源消耗甚至利用免费的边缘计算服务完成从信息采集到分发的闭环。2. 核心架构与设计思路拆解2.1 为什么选择“边缘”架构传统的数据聚合或爬虫项目通常会部署在云服务器VPS或容器服务上7x24小时运行。这当然稳定但对我这个个人需求来说有点“杀鸡用牛刀”且会产生持续的成本和运维负担。“边缘计算”在这里不是一个噱头而是一种务实的选择。我指的“边缘”是那些能够按需执行、触发式运行的计算环境比如云函数/Serverless服务例如AWS Lambda、Google Cloud Functions、Vercel Edge Functions或Cloudflare Workers。它们只在被调用时比如定时触发器触发才运行执行完就停止计费完美契合“每日一次”的采集任务。尤其是Cloudflare Workers它在全球边缘节点运行网络延迟低抓取速度快并且有慷慨的免费额度。GitHub Actions对开发者极其友好。可以配置一个定时任务Cron Job每天在特定时间启动一个工作流。这个工作流运行在一个临时的虚拟环境中执行你的抓取和推送脚本。完全免费在额度内并且与代码仓库天然集成版本管理方便。本地设备结合自动化工具如果你有一台常年开机的NAS或树莓派配合crontab定时任务这也是一个非常可靠的“边缘”节点。我的选择是Cloudflare WorkersGitHub Actions的组合。原因如下Cloudflare Workers负责最核心的、对网络延迟敏感的数据抓取和初步处理工作因为它遍布全球访问目标新闻站点的速度很快。然后将处理后的结构化数据暂存到一处比如Cloudflare KV存储或简单的Git仓库。接着由GitHub Actions的定时任务触发执行数据聚合、去重、格式化生成简报和最终推送的任务。这样分工既利用了边缘网络的优势又利用了GitHub Actions在复杂工作流和免费额度上的便利性。注意选择边缘方案必须考虑运行时长和内存限制。例如Cloudflare Workers免费计划有10ms的CPU时间限制和128MB内存限制。这意味着你的抓取逻辑必须高效不能抓取过于复杂的页面或进行重型计算。设计之初就要把“轻量”刻在脑子里。2.2 信源选择与数据获取策略新闻聚合信源的质量和稳定性是第一位的。我的原则是优先官方RSS其次考虑稳定性高的API万不得已再动用轻量级爬虫。RSS/Atom源这是最理想、最规范的方式。很多新闻网站和博客仍提供RSS。使用一个健壮的RSS解析库比如Python的feedparser就能轻松获取标题、链接、摘要和发布时间。例如TechCrunch、BBC News的特定板块等。公开API像Hacker News、Reddit都提供了优秀的官方API。以Hacker News为例通过其公开的Firebase API或Algolia API可以轻松获取首页或特定时间段内的热门故事。这比爬虫更可靠、更受网站欢迎。轻量级爬虫谨慎使用对于没有提供友好接口的网站如果必须抓取务必遵守robots.txt并采用极其节制的策略。我的做法是只抓取列表页避免进入大量详情页。使用HEAD请求检查更新通过对比Last-Modified或ETag头判断页面是否真有更新避免不必要的完整页面抓取。设置友好的请求头和合理的延迟模拟真实浏览器并在请求间添加随机延时如1-3秒。优先使用CSS选择器而非XPath现代解析库如parsel或BeautifulSoup配合CSS选择器通常更简洁稳定。在我的项目中我混合使用了这三种方式。核心科技新闻来自几个固定RSS行业趣闻来自Hacker News的“show”板块通过API而某个特定论坛的每日讨论精选则通过一个非常克制的爬虫来获取列表页标题和链接。2.3 数据处理流水线设计抓取到的原始数据是杂乱无章的需要一条清晰的流水线来处理。我的流水线分为四步全部在边缘环境中完成清洗与标准化不同来源的数据格式不一。这一步统一所有条目的字段title标题、link链接、source来源、timestamp时间戳统一为ISO 8601格式、summary摘要可能为空。清除标题中的多余空格、换行符。去重这是关键一步。不同信源可能报道同一事件。我的去重逻辑基于“标题相似度”和“链接唯一性”双重判断。链接去重直接对比link字段相同的直接丢弃。标题相似度去重计算标题的文本相似度例如使用Python的difflib.SequenceMatcher或更高效的jellyfish库。当相似度超过一个阈值如0.85且发布时间接近则视为重复只保留来源权重最高或发布时间最早的一条。实现细节我会为每条新闻生成一个“指纹”可以是link的MD5哈希也可以是标题经过清洗去除停用词、标点后的词序列的哈希。将指纹存储在KV中每次处理前先比对实现高效去重。优先级排序并非所有新闻都同等重要。我设计了一个简单的评分算法为每条新闻打分用于最终排序来源权重我预先给每个信源一个权重系数如权威媒体1.2个人博客0.8。时间衰减新闻越新分数越高。采用指数衰减因子。关键词匹配我维护一个个人关注的关键词列表如“AI”、“Rust”、“Edge Computing”。标题或摘要中出现关键词则获得加分。最终分数 来源权重 * 时间衰减因子 * (1 关键词匹配加分)。按分数降序排列。内容摘要生成可选进阶功能如果想让简报更精炼可以引入文本摘要。但边缘环境资源有限不能使用大型模型。我的方案是如果原文提供了良好的summary字段RSS中常有直接使用。如果没有则尝试调用轻量级的摘要API例如有些云服务提供按次计费的摘要功能或者使用一个非常简单的提取式摘要方法——抽取文章开头几句和包含关键词的句子。鉴于边缘函数的内存和时间限制这一步我目前没有加入核心流程而是作为一个手动触发的“增强”选项。3. 核心模块实现与实操要点3.1 基于Cloudflare Workers的抓取器实现Cloudflare Workers使用JavaScript/TypeScript或WebAssembly。我选择TypeScript因为它能提供更好的类型安全。下面是一个抓取RSS源的Worker简化示例// src/fetchers/techCrunchFetcher.ts import { parseFeed } from rss-parser; // 需要使用Bundler将npm包打包进Worker export interface NewsItem { title: string; link: string; source: string; timestamp: string; summary?: string; } export async function fetchTechCrunch(): PromiseNewsItem[] { const RSS_URL https://techcrunch.com/feed/; const response await fetch(RSS_URL); const xml await response.text(); // 使用rss-parser库解析 const parser new RSSParser(); const feed await parser.parseString(xml); return feed.items.map(item ({ title: item.title?.trim() || No Title, link: item.link || #, source: TechCrunch, timestamp: item.isoDate || new Date().toISOString(), // 优先使用ISO日期 summary: item.contentSnippet?.substring(0, 200) // 截取摘要 })); } // src/index.ts (Worker入口点) import { fetchTechCrunch } from ./fetchers/techCrunchFetcher; import { storeItems } from ./storage; // 假设的存储模块 export default { async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) { // 这个函数会在预设的Cron时间触发例如“18 8 * * *”表示每天8:18 console.log(Cron trigger fired at ${event.scheduledTime}); try { const techCrunchNews await fetchTechCrunch(); // 可以并行抓取其他源 // const hnNews await fetchHackerNews(); // 合并、清洗数据这里简化 const allItems [...techCrunchNews]; // 存储到Cloudflare KV中供后续步骤使用 await storeItems(env.NEWS_KV, allItems); console.log(Fetched and stored ${allItems.length} items.); } catch (error) { console.error(Fetch failed:, error); // 可以在这里集成错误通知比如发送到Discord或Telegram } }, async fetch(request: Request, env: Env, ctx: ExecutionContext): Response { // 这里可以提供一个手动触发的HTTP端点用于测试 return new Response(News Fetcher Worker is running.); } };实操要点与避坑指南依赖管理Worker环境并非完整的Node.js不能直接require。需要使用wranglerCF官方CLI工具进行构建和打包或者使用ES模块格式直接导入。对于rss-parser这类库需要确认其是否兼容Worker环境通常要求是纯JS或无Node特定API。错误处理与重试网络请求可能失败。务必用try-catch包裹并为关键请求添加简单的重试逻辑例如最多重试2次指数退避。Worker的失败不会导致整个任务崩溃但记录日志至关重要。内存与CPU时间抓取多个源时避免使用Promise.all进行无限制的并行抓取这可能导致内存激增或超时。可以控制并发数或用Promise.allSettled收集所有结果包括失败的。秘钥管理如果需要访问有API密钥的源绝对不要将密钥硬编码在代码中。使用Cloudflare Workers的环境变量env.API_KEY来安全存储。3.2 基于GitHub Actions的聚合与推送引擎Worker负责抓取和暂存GitHub Actions则扮演“主编”角色每天定时执行进行深度处理并发出简报。# .github/workflows/daily-news-digest.yml name: Daily News Digest on: schedule: # 每天UTC时间0:18运行假设你所在时区是UTC8那这就是早上8:18 - cron: 18 0 * * * workflow_dispatch: # 允许手动触发 jobs: aggregate-and-send: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Install dependencies run: | pip install -r requirements.txt # 可能需要的包requests, feedparser, python-dotenv, jinja2 (用于邮件模板), smtplib - name: Run aggregation script env: CF_KV_NAMESPACE_ID: ${{ secrets.CF_KV_NAMESPACE_ID }} CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} run: python scripts/aggregator.py - name: Commit and push if content changed run: | git config --local user.email actiongithub.com git config --local user.name GitHub Action git add -A git diff --staged --quiet || git commit -m Update daily news digest for $(date %Y-%m-%d) git push核心脚本scripts/aggregator.py的关键步骤从KV读取数据使用Cloudflare API从Worker存储的KV中读取所有抓取到的新闻项。执行去重与排序运行前面提到的去重和评分算法。生成简报内容使用Jinja2模板引擎将排名前15-20条的新闻渲染成美观的HTML或Markdown格式。模板中可以包含日期、分类、摘要等信息。选择推送渠道电子邮件最通用。使用smtplib和Gmail的App Password需在Gmail账户中开启两步验证后生成或专业的邮件发送服务如SendGrid、Mailgun的API。Telegram Bot体验极佳。在Telegram上找BotFather创建一个Bot获取Token。然后通过简单的HTTP POST请求到https://api.telegram.org/botYOUR_TOKEN/sendMessage就能发送消息。支持Markdown格式阅读起来很舒服。Discord Webhook类似Telegram适合团队共享。生成静态页面将简报生成一个HTML文件提交到GitHub仓库通过GitHub Pages自动发布。这样你就得到了一个可公开访问的每日新闻存档页面。实操心得秘钥安全所有API Token、密码都必须存储在GitHub仓库的Settings Secrets and variables Actions中然后在工作流文件中通过${{ secrets.XXX }}引用切勿写在代码里。时区问题GitHub Actions的Cron调度基于UTC时间。务必根据你的本地时区进行换算。例如北京东八区UTC8早上8点18分对应的UTC时间就是凌晨0点18分Cron表达式为18 0 * * *。处理失败通知可以在工作流中添加一个失败通知的步骤使用if: failure()条件通过邮件或Webhook告知你任务执行失败方便及时排查。3.3 数据存储的轻量级选择在边缘架构中存储也需要“轻量”。我评估了以下几种方案Cloudflare KV与Workers天生一对是键值存储。适合存储临时抓取的数据读写速度快免费额度足够个人使用。缺点不是数据库查询能力弱适合按已知键存取。Git仓库本身将处理后的结构化数据如JSON文件直接提交回Git仓库。这是最“朴素”但极其有效的版本化存储方案。GitHub Actions可以自然读写工作区。优点历史记录完整无需额外服务。缺点频繁读写大文件可能不适合。SQLite数据库文件可以将SQLite数据库文件.db存储在仓库或KV中。Python原生支持轻量且功能强大。在Actions中运行时可以将其作为文件操作。注意如果多进程并发写需要小心锁的问题但在我们每日一次的脚本中这不是问题。我的选择是混合策略Worker抓取的原始数据暂存到Cloudflare KV因为Worker写KV最方便。然后GitHub Actions的脚本从KV中读取数据处理完成后将最终的精简列表一个JSON数组和一个渲染好的HTML简报文件一并提交回Git仓库。这样KV作为临时缓冲区Git仓库作为最终归档和发布源。4. 部署、监控与优化实践4.1 部署流程与配置整个项目的部署分为两部分Cloudflare Worker部署安装wranglerCLInpm install -g wrangler登录并配置wrangler login在wrangler.toml配置文件中定义KV命名空间绑定和Cron触发器。name news-at-edge-fetcher compatibility_date 2024-08-18 workers_dev true [kv_namespaces] binding NEWS_KV id your_kv_namespace_id # 通过wrangler kv:namespace create创建后获得 [triggers] crons [18 8 * * *] # 每天UTC时间8:18运行请根据你的时区调整发布wrangler publishGitHub Actions工作流部署将.github/workflows/daily-news-digest.yml文件推送到仓库的默认分支如main。在仓库Settings中配置好所需的SecretsCF_KV_NAMESPACE_ID,CF_API_TOKEN,SMTP_PASSWORD等。第一次可以手动触发workflow_dispatch测试流程是否通畅。4.2 监控与日志查看一个自动化系统没有监控就等于“黑盒”。Cloudflare Worker日志在Cloudflare Dashboard的Workers Pages部分选择你的Worker进入“日志”标签页。你可以看到每次调用的console.log输出和错误信息。务必在代码的关键节点添加日志如“开始抓取[源]”、“成功抓取X条”、“存储完成”、“错误XXX”。GitHub Actions运行历史在仓库的“Actions”标签页可以清晰看到每次工作流的运行状态成功/失败、每个步骤的详细日志。这是排查脚本错误的主要阵地。推送结果监控最直接的监控就是你是否按时收到了简报。可以设置一个简单的“心跳”监控让Actions脚本在成功发送后向一个单独的监控频道或日志文件发送一条成功消息。如果连续几天没收到就该检查了。4.3 性能优化与成本控制对于个人项目成本几乎为零但优化习惯很重要Worker优化减少不必要的依赖保持Worker bundle体积小。定期检查package.json。使用缓存对于不常变化的源如某些网站的静态列表页可以在Worker中合理使用Cache API将响应缓存一段时间如1小时减少对目标网站的请求。并行与串行的权衡如前所述控制并发。对于多个独立源适度的并行如同时抓取2-3个可以缩短总时间但要注意资源上限。Actions优化依赖缓存在工作流中设置对Python依赖包pip或Node模块npm的缓存可以大幅缩短工作流启动时间。- name: Cache pip packages uses: actions/cachev3 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles(requirements.txt) }} restore-keys: | ${{ runner.os }}-pip-矩阵策略如果你的聚合脚本需要处理多种输出格式如同时生成邮件和Telegram消息可以考虑使用策略矩阵来并行执行不相互依赖的任务。5. 常见问题与排查技巧实录在开发和运行这个项目的过程中我踩过不少坑这里总结一下最常见的问题和解决方法问题现象可能原因排查步骤与解决方案Worker执行超时失败1. 抓取的网站响应慢或阻塞。2. 处理逻辑太复杂超过CPU时间限制。3. 网络延迟高。1. 检查目标网站状态为fetch请求设置合理的timeout如10秒。2. 优化代码减少不必要的循环和计算。将复杂的去重/排序逻辑移到下游的GitHub Actions中。3. 考虑更换Worker的部署区域CF Workers默认在全局边缘通常很快。GitHub Actions工作流失败1. Python脚本语法错误或运行时异常。2. 环境变量Secrets未正确设置或读取。3. 依赖安装失败。4. Git推送权限问题。1. 查看Actions日志中具体的错误堆栈。先在本地环境运行脚本测试。2. 确认Secrets的名称与脚本中os.environ.get(XXX)引用的名称完全一致包括大小写。3. 检查requirements.txt文件格式是否正确网络是否通畅。可使用国内镜像源。4. 使用actions/checkout时默认使用GITHUB_TOKEN有推送权限。如果失败检查仓库设置。收不到推送的邮件/消息1. 邮件被标记为垃圾邮件。2. SMTP配置错误端口、加密方式。3. Telegram Bot Token错误或聊天ID不对。4. 推送脚本逻辑错误未执行到发送步骤。1. 检查垃圾邮件箱。优化邮件标题和内容避免 spam 关键词。2. 使用telnet或在线工具测试SMTP连接。Gmail需使用App Password而非原密码。3. 确认Bot Token正确且已通过/start命令与Bot发起过对话以获取chat_id。4. 在发送函数前后添加详细日志确认程序流执行到了发送环节。抓取到的数据为空或格式错误1. 目标网站改版CSS选择器或API接口失效。2. RSS源地址失效或返回非XML内容。3. 网站有反爬机制如Cloudflare盾。1. 定期手动测试你的抓取器。编写一个简单的测试脚本定期运行并报警。2. 检查RSS源URL用浏览器或curl查看返回内容。增加更健壮的解析错误处理。3. 对于简单反爬尝试设置更真实的User-Agent和Referer头。如果遇到复杂反爬请尊重网站规则考虑放弃该源或寻找官方API。去重效果不佳1. 相似度阈值设置不合理。2. 标题清洗不彻底残留标点、停用词影响相似度计算。3. 不同来源的时间戳格式不一致导致时间接近判断出错。1. 调整相似度阈值并通过一批人工标记的重复/非重复数据来测试效果。2. 强化清洗函数统一转小写、移除所有标点符号、移除常见停用词a, the, and等。3. 在标准化步骤中将所有时间戳统一转换为UTC时间戳整数再进行对比。一个独家避坑技巧处理动态加载的内容有些现代网站新闻列表是JavaScript动态渲染的直接fetch拿到的是空壳HTML。最初我考虑用Puppeteer之类的无头浏览器但这在资源受限的边缘环境几乎不可能。我的解决方案是寻找替代数据源。很多时候这些网站会有对应的、内容相同的移动版页面结构更简单或者有未被前端混淆的API接口通过浏览器开发者工具的“网络”标签页查找XHR/Fetch请求。如果实在找不到我会评估这个源是否不可或缺如果不是宁愿舍弃它也不引入重型且不稳定的解决方案。保持边缘的“轻”是项目稳定的基石。这个“News — At The Edge”项目运行几个月下来已经成为我每日信息早餐的固定组成部分。它完全按照我的意愿运行安静、高效、透明。最大的成就感来自于“驯服”了信息流而不是被它淹没。如果你也有类似的需求不妨从一个小信源、一个最简单的推送方式比如先推到Telegram开始尝试。边缘计算的模式让这种个人自动化项目的启动成本和维护负担变得极低一旦跑通它带来的效率提升和心智平静感是非常值得的。