ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

个人新闻收集站搭建指南:基于Python与RSS的轻量资讯聚合模板

个人新闻收集站搭建指南:基于Python与RSS的轻量资讯聚合模板 简介这是一款面向个人用户、博客作者及资讯聚合站点搭建者的网页模板资源适合快速创建个人新闻收集类网站。模板采用响应式布局支持自定义配色、栏目版块与内容发布流程兼顾内容管理与SEO基础优化无需深厚编程基础即可上手。压缩包共13个文件约24KB主要包含HTML首页、jpg/gif图片素材、ini/db/txt配置说明及网址快捷方式等结构简洁清晰便于直接替换图片、填充文章内容。目前已有157人学习浏览。通过这套模板用户可拿到完整的主页代码、多张装饰图片和部署说明结合ReadMe中的指引修改index.htm即可搭建出具备基础展示功能的个人新闻聚合页面适合课程设计、个人练手或轻量级站点起步也能够为进一步二次开发提供简洁的静态页面框架。1. 为什么要自己做个人新闻收集站我做这件事的初衷特别简单每天打开手机各种资讯App给我推的东西越来越同质化算法永远把我限制在同一个信息茧房里。我真正想看的是几个固定科技博客、几个行业媒体、还有我关注的开源社区动态而不是被塞给我一堆广告和标题党。与其每天手动点开五六个网站刷一遍不如自己搭一个个人新闻收集网站模板把这些源全部聚合到一个页面上清爽、直接、没有广告。这个模板不是说给你一个成品站而是给你一套完整的思路和代码框架每天定时去抓取你指定的信息源提取标题、摘要、链接、时间存进数据库然后前端用一个干净的页面展示出来支持按来源筛选、按关键词搜索。你只需要改配置文件里的源地址就能拥有一个完全属于自己的资讯聚合页。这套方案适合谁如果你懂一点点Python和前端基础想做一个能长期稳定跑的个人资讯站或者你想给团队做一个内部信息汇总页面那这套模板可以直接拿去用。不需要复杂的架构一台普通服务器甚至树莓派都能流畅运行。2. 整体架构和数据流设计2.1 架构选型为什么是Python SQLite 轻量前端先说一下整体技术选型。采集端我选Python原因很简单生态成熟写爬虫和解析RSS的库非常多代码量能压缩到最小。数据存储选SQLite因为个人站的读写量根本到不了需要MySQL或PostgreSQL的地步SQLite单文件部署、零配置、备份直接拷贝文件就行对个人项目来说是最省心的方案。前端不做前后端分离的复杂工程直接用一个轻量服务把页面和接口一起提供出来减少部署成本。整个数据流是这样的定时任务从源地址抓取XML数据 → 解析出文章标题、链接、摘要、发布时间 → 去重后写入SQLite → 前端页面通过HTTP接口读取数据库 → 渲染成卡片列表。这个链路非常清晰中间任何一个环节出问题都容易定位。为了跑定时任务我用了Python标准库里的sched模块配合threading没有额外引入Celery这类重量级组件。考虑到实现简单、可读性高这一版模板把定时调度的核心逻辑控制在几十行内也方便你按自己的需求改成cron外部调度。2.2 数据模型设计一张表搞定数据库表设计得简单一点。我建了一张news_items表字段如下字段名类型说明idINTEGER PRIMARY KEY AUTOINCREMENT自增主键source_nameTEXT信息源名称titleTEXT文章标题urlTEXT UNIQUE原文链接作为唯一键去重summaryTEXT摘要或描述published_atTEXT发布时间ISO格式字符串created_atTEXT抓取入库时间url字段加上UNIQUE约束这是去重的核心手段。RSS源里同一篇文章可能被重复推送或者两个不同的源推送了同一篇转载用URL做唯一键是最简单可靠的方式。如果遇到同一篇文章URL不同但内容相同的情况那属于内容级去重复杂度会高很多个人站的场景下不需要过度设计。2.3 为什么用RSS而不是直接爬网页在动手之前我对比了两条路线直接写爬虫去解析网页HTML还是解析RSS/Atom订阅源。我个人的结论是能优先用RSS就用RSS。网页爬虫的坑太多了。目标网站改版导致CSS选择器失效、需要处理登录态、可能要应对JavaScript动态渲染、还得注意抓取频率不能给对方服务器造成压力。而RSS和Atom是内容发布方主动提供的结构化数据格式本身就是给程序消费的稳定、规范、对服务器友好。当然RSS也有局限有些网站不提供订阅源或者只提供部分内容的摘要。我的处理策略是主抓RSS对于少数没有RSS但确实需要的源单独写一个兼容入口用传统爬虫抓取后转成统一的字典格式再入库。模板里把这两种方式抽象成了统一的fetch_source接口后面可以按需扩展。3. 后端采集模块实现细节3.1 抓取RSS核心代码先看数据采集部分最核心的代码。我用feedparser这个库解析RSS和Atom格式它能把XML转换成Python字典省去了手写XML解析的麻烦。import feedparser import sqlite3 import datetime import sched import time import threading import requests SOURCES [ {name: 示例科技源A, url: https://example-a.com/rss, category: 科技}, {name: 示例科技源B, url: https://example-b.com/feed, category: 科技}, {name: 示例开源社区源C, url: https://example-c.org/rss/, category: 开源}, ] DB_PATH news.db def init_db(): conn sqlite3.connect(DB_PATH) conn.execute( CREATE TABLE IF NOT EXISTS news_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_name TEXT, title TEXT, url TEXT UNIQUE, summary TEXT, published_at TEXT, created_at TEXT ) ) conn.commit() conn.close() def fetch_and_store(source): try: resp requests.get(source[url], timeout10, headers{User-Agent: Mozilla/5.0}) resp.raise_for_status() parsed feedparser.parse(resp.content) except Exception as e: print(f[抓取失败] {source[name]}: {e}) return conn sqlite3.connect(DB_PATH) now datetime.datetime.now().isoformat() count 0 for entry in parsed.entries: title entry.get(title, ).strip() link entry.get(link, ).strip() summary entry.get(summary, entry.get(description, ))[:500] published entry.get(published, entry.get(updated, now)) try: conn.execute( INSERT OR IGNORE INTO news_items (source_name, title, url, summary, published_at, created_at) VALUES (?, ?, ?, ?, ?, ?), (source[name], title, link, summary, published, now) ) count 1 except sqlite3.Error as e: print(f[入库失败] {source[name]}: {e}) conn.commit() conn.close() print(f[完成] {source[name]} 新增 {count} 条)这里有几个细节值得说明。INSERT OR IGNORE是SQLite的语法配合url字段的唯一索引重复抓取时直接跳过不会报错也不会产生重复数据。我统计了一下实际运行效果同样的源连续抓三次数据库里文章数不会多一条。3.2 定时调度机制调度部分用了sched加threading每30分钟触发一次全量抓取。这个间隔是我多次测试后定下的太频繁容易给源站造成压力且没必要太长则新闻时效性差。一些更新很快的源我单独在配置里加了interval字段可以覆盖全局频率。scheduler sched.scheduler(time.time, time.sleep) def run_all_sources(): print(f[定时任务] {datetime.datetime.now()} 开始抓取) threads [] for source in SOURCES: t threading.Thread(targetfetch_and_store, args(source,)) t.start() threads.append(t) for t in threads: t.join() print(f[定时任务] {datetime.datetime.now()} 抓取完成) def schedule_loop(interval_seconds1800): run_all_sources() scheduler.enter(interval_seconds, 1, schedule_loop, (interval_seconds,)) def start_scheduler(): scheduler.enter(0, 1, schedule_loop, (1800,)) scheduler.run()多线程抓取是必须的。如果单线程逐个请求遇到某个源响应慢整个采集链路会被拖住。实测中我加了5个源单线程跑完需要40秒左右改成多线程后最快只要8秒。这8秒和40秒的差别对定时任务来说不算什么大问题但每次任务积压会导致时间越长越偏所以坚持用多线程值得的。3.3 抓取异常处理的经验我在测试阶段遇到的第一个坑就是请求被部分源拒绝。有些站点会检查User-Agent默认的Python UA会被拦截所以我在请求头里伪装成了浏览器UA。还有几个源启用了HTTPS证书校验requests默认开启验证如果你的源证书有问题会直接报错这时可以临时用verifyFalse但我建议不到万不得已别这样做有中间人攻击风险。另一个坑是RSS里的时间格式不统一。有的用RFC 822格式有的是ISO 8601还有一些源根本不写时间。feedparser已经做了大部分解析工作entry.get(published, entry.get(updated, now))这个回退链保证了至少会有一个值兜底。入库后如果要按时间排序建议在SQL查询时用datetime(published_at)做转换避免字符串排序出错。4. 前端页面和服务接口实现4.1 接口设计一个JSON接口撑起整个页面后端除了采集还需要给前端提供数据。我直接写了一个HTTP服务同时负责静态页面和JSON接口。这里用Python标准库的http.server实现不引入Flask等框架好处是部署时不需要装额外依赖但对后续功能扩展不太灵活。如果你后续想加用户系统、点赞评论之类的功能建议切换到Flask重写接口层。import json from http.server import HTTPServer, BaseHTTPRequestHandler class NewsHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path.startswith(/api/news): self.handle_api() elif self.path / or self.path.startswith(/index.html): self.serve_file(index.html, text/html; charsetutf-8) elif self.path.startswith(/static/): self.serve_static(self.path) else: self.send_error(404) def handle_api(self): from urllib.parse import urlparse, parse_qs query parse_qs(urlparse(self.path).query) source query.get(source, [None])[0] keyword query.get(keyword, [None])[0] limit int(query.get(limit, [50])[0]) sql SELECT source_name, title, url, summary, published_at FROM news_items WHERE 11 params [] if source and source ! 全部: sql AND source_name ? params.append(source) if keyword: sql AND (title LIKE ? OR summary LIKE ?) params.append(f%{keyword}%) params.append(f%{keyword}%) sql ORDER BY datetime(published_at) DESC LIMIT ? params.append(limit) conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row rows conn.execute(sql, params).fetchall() conn.close() data [dict(row) for row in rows] body json.dumps(data, ensure_asciiFalse).encode(utf-8) self.send_response(200) self.send_header(Content-Type, application/json; charsetutf-8) self.send_header(Cache-Control, no-store) self.send_header(Content-Length, str(len(body))) self.end_headers() self.wfile.write(body) def serve_file(self, filepath, content_type): try: with open(filepath, rb) as f: content f.read() self.send_response(200) self.send_header(Content-Type, content_type) self.send_header(Content-Length, str(len(content))) self.end_headers() self.wfile.write(content) except FileNotFoundError: self.send_error(404) def log_message(self, format, *args): print(f[访问日志] {self.address_string()} {format % args})接口里加上了两个查询参数source用来按源筛选keyword用来搜索关键词。limit控制返回条数避免前端一次性要太多数据页面加载变慢。4.2 页面设计卡片式布局的取舍前端页面我坚持用原生HTML CSS JavaScript没有引入Vue或React。理由很现实这个页面不涉及复杂状态管理原生JS完全够用。引入框架反而要处理打包构建问题增加学习成本和部署复杂度。页面的视觉风格我参照了主流资讯类产品的设计顶部一个标题栏下面一行筛选按钮再往下是卡片流。每张卡片展示信息来源、标题、摘要、发布时间标题点击后新标签页打开原文链接。卡片之间用grid布局自适应列数宽屏显示三列平板两列手机单列。整体风格偏简约背景用了浅灰色卡片白色带轻微阴影。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title我的新闻收集站/title style body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; background: #f5f6f7; margin: 0; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; } .site-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } .site-title { font-size: 1.6em; font-weight: 700; color: #1a1a1a; } .filter-bar { margin-bottom: 16px; } .filter-btn { padding: 6px 16px; margin-right: 8px; border: 1px solid #d0d0d0; border-radius: 20px; background: #fff; cursor: pointer; font-size: 0.9em; } .filter-btn.active { background: #1a1a1a; color: #fff; border-color: #1a1a1a; } .search-input { padding: 6px 12px; width: 220px; border: 1px solid #d0d0d0; border-radius: 20px; font-size: 0.9em; } .news-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; } .news-card { background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); transition: box-shadow 0.2s; } .news-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.12); } .source-label { font-size: 0.8em; color: #666; margin-bottom: 6px; } .news-title { font-size: 1.1em; font-weight: 600; margin-bottom: 8px; line-height: 1.4; } .news-title a { color: #1a1a1a; text-decoration: none; } .news-title a:hover { color: #0066cc; } .summary { font-size: 0.9em; color: #444; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 10px; } .time { font-size: 0.8em; color: #999; } .empty { text-align: center; color: #999; padding: 60px 0; } /style /head body div classcontainer div classsite-header div classsite-title我的新闻收集站/div input classsearch-input idsearchBox placeholder搜索标题或摘要... /div div classfilter-bar idfilterBar/div div classnews-grid idnewsGrid/div div classempty idemptyTip styledisplay:none;暂无匹配新闻/div /div script const API_URL /api/news; async function loadNews(source, keyword) { const params new URLSearchParams(); if (source source ! 全部) params.set(source, source); if (keyword) params.set(keyword, keyword); const resp await fetch(${API_URL}?${params.toString()}); const data await resp.json(); render(data); } function render(items) { const grid document.getElementById(newsGrid); const empty document.getElementById(emptyTip); grid.innerHTML ; if (!items || items.length 0) { empty.style.display block; return; } empty.style.display none; items.forEach(item { const card document.createElement(div); card.className news-card; const time new Date(item.published_at).toLocaleString(zh-CN, { hour12: false }); card.innerHTML div classsource-label${item.source_name}/div div classnews-titlea href${item.url} target_blank relnoopener noreferrer${item.title}/a/div div classsummary${item.summary || }/div div classtime${time}/div ; grid.appendChild(card); }); } async function initFilters() { const resp await fetch(/api/news?limit1); const data await resp.json(); // 从数据中提取来源列表实际场景建议额外提供 /api/sources 接口 } document.getElementById(searchBox).addEventListener(input, (e) { loadNews(currentSource, e.target.value); }); let currentSource 全部; loadNews(全部, ); /script /body /html4.3 来源筛选按钮的动态渲染上面代码里initFilters函数只留了注释实际做动态渲染按钮时我建议在后端单独加一个/api/sources接口因为从新闻列表里提取来源不够精确当某个源当天没有抓取到新内容时按钮可能就消失了。我在实际项目中是这样实现的elif self.path.startswith(/api/sources): conn sqlite3.connect(DB_PATH) rows conn.execute(SELECT DISTINCT source_name FROM news_items ORDER BY source_name).fetchall() conn.close() data [row[0] for row in rows] body json.dumps(data, ensure_asciiFalse).encode(utf-8) self.send_response(200) self.send_header(Content-Type, application/json; charsetutf-8) self.end_headers() self.wfile.write(body)前端拿到这个列表后先添加一个“全部”按钮再逐个追加来源按钮。点击按钮时切换active类并重新请求新闻列表。这里需要注意给按钮绑事件时不要直接用onclick函数名拼接的方式用闭包保存当前点击的值不然闭包陷阱会让你所有按钮拿到的都是最后一个值。5. 部署、性能优化和常见问题5.1 部署方式先用本地测试再上服务器模板开发阶段的运行方式很简单在项目目录下执行python main.py服务默认跑在8000端口。第一次跑起来后由于数据库是空的页面会显示空白等定时任务第一次抓取完成后刷新页面才会有数据。我建议你先把定时间隔设成60秒来做测试确认抓取正常再改回正式间隔。如果要在服务器上长期运行我推荐用systemd来做守护进程而不是用nohup加这种方式。systemd的配置很简单[Unit] DescriptionPersonal News Collector Afternetwork.target [Service] WorkingDirectory/opt/news-collector ExecStart/usr/bin/python3 main.py Restartalways RestartSec10 [Install] WantedBymulti-user.target配置完成后执行systemctl enable news-collector开机自启进程崩溃后10秒内自动拉起。这个模板在本地跑通后部署到服务器上基本不用改代码只要确认Python版本在3.8以上就行。5.2 性能优化为什么打开页面不会卡网页的响应速度主要取决于数据库查询。目前这张表的数据量在几千到几万条级别SELECT语句即使是全表扫描也就几十毫秒。为了展示流畅我在published_at和source_name字段上建了普通索引查询效率进一步提升。如果你后续要存几十万条数据建议把SQLite换成PostgreSQL或者加一层Redis缓存但个人站大概率用不到。另一个优化点是前端懒加载。目前接口默认返回50条页面滚动到接近底部时再加载下一页这种“无限滚动”的交互模式在资讯类网站里很常见。实现思路是监听window.scroll事件判断scrollTop clientHeight是否接近scrollHeight如果是就请求下一页数据并追加渲染。为了避免重复请求需要在加载过程中加一个isLoading标志。这里我踩过一个坑滚动事件触发频率很高如果不做节流一次滚动会发出好几个相同请求。最简单的节流方式是加一个定时器记录上次请求时间间隔小于500毫秒的直接忽略。5.3 常见问题速查我在整个开发过程中汇总了几个高频问题整理成表格方便你排查问题现象可能原因排查与解决方法页面一直空白没有卡片数据库还没有数据定时任务未触发或抓取失败先看控制台日志是否有[抓取失败]输出手动调用一次fetch_and_store函数测试同一个源抓了两次数据库里全是重复数据URL唯一索引未生效或者不同批次URL格式不一致检查url字段是否有特殊符号差异确认表里已经建了UNIQUE约束接口返回500错误数据库文件被占用或锁死检查是否有另一个进程同时写同一个数据库文件SQLite不支持高并发写入前端卡片摘要显示乱码内容编码格式不正确确认页面声明了UTF-8数据库连接也设置了UTF-8某个源一直抓取失败目标网站屏蔽了默认UA或不支持RSS先手动用浏览器访问源地址确认返回的是XML格式再调整请求头参数还有个容易被忽略的问题HTTP服务使用http.server模块是单线程的同时只处理一个请求。如果某个前端页面同时发出了多个请求后面的请求会排队等待。这在个人使用场景下感受不到但如果多人访问就会明显卡顿。解决方法是继承ThreadingHTTPServer或者参考官方文档用ThreadingMixIn让每个请求在独立线程中处理。5.4 长时间稳定运行的关键参数模板里定时任务默认30分钟抓取一次但我在实际使用中发现不是所有源都需要这么高的频率。日更的博客一天刷新一次就行科技媒体可以两小时一次。把每个源独立的抓取间隔做成字典写进配置区是让整个系统保持轻负载的关键。另外抓取失败时不要连续重试我设置了失败后跳过本轮等下一轮再尝试避免目标站短暂故障时我们这边疯狂请求。6. 这个模板还能怎么扩展6.1 给新闻加分类和标签目前模板只有来源分类你可以继续扩展为文章内容标签。解析完RSS后根据标题和摘要里的关键词做简单匹配打上“AI”“开源”“前端”等标签。最终在页面上做成标签筛选。这个功能可以简单用一组正则表达式实现几行代码就能有一个不错的效果。6.2 生成每日摘要邮件如果你不想打开网页可以加一个每天定时发送摘要邮件的功能。把当天新增的新闻标题和链接整理成HTML表格用邮件发送到指定邮箱。这个非常适合上班族早上打开邮箱就能看到昨晚发生的行业要闻。发送邮件用smtplib就行配置一下邮箱的SMTP地址和授权码注意不要把授权码写死在代码里用环境变量读取更安全。6.3 导出为JSON或Markdown配合其他工具使用数据库里的数据还可以自动导出成JSON文件这样即使不启动HTTP服务也能被其他脚本读取。我做了一个小脚本每天凌晨把前一天的新闻标题汇总成一个Markdown文件方便做周报和月报。这个思路特别适合需要定期整理资讯内容的人比如做行业研究、竞品分析。6.4 部署到低功耗设备上整套模板运行起来的内存占用大约是60MB左右CPU占用几乎可以忽略。我之前试过把它跑在一台树莓派上用USB移动硬盘存数据库连续运行一个月没有出现异常。如果放在家用NAS的Docker容器里也就是一个镜像的事情。整个模板没有需要编译的原生依赖feedparser和requests都是纯Python库所以跨平台部署非常丝滑。7. 一点使用心得建这个个人新闻收集网站模板折腾了大概三个晚上第一版用Flask写跑通后觉得太重换成了标准库版本。现在这套东西已经在我自己的服务器上稳定跑了大半年每天早上打开页面扫一眼就能把关注的领域动态全部过一遍。相比以前挨个网站刷节约的时间不是一点点。我最推荐的用法是前期先加5个以内你最常看的源跑上一周观察稳定性和抓取效果再慢慢扩充。不要一开始就加几十个源因为每个源的格式细节都不一样全堆在一起时排错和调优会消耗大量精力。先跑起来再慢慢迭代这才是个人项目最舒服的节奏。另外多花点时间在你自己的阅读效率上。内容聚合只是第一步怎么筛选、怎么沉淀、怎么把零散信息组织成自己的知识体系才是这个模板带给我们更深层的思考。后续如果你有什么好玩的扩展思路欢迎多交流。本文还有配套的精品资源点击获取
返回列表