自建新闻聚合网站模板:RSS采集、去重、分类与展示全解析

发布时间:2026/10/17 20:28:55
自建新闻聚合网站模板:RSS采集、去重、分类与展示全解析
简介这是一款面向个人博客与新闻聚合场景的网页模板适合想快速搭建个人主页或资讯展示页的非专业设计师使用。模板预设了清晰布局、配色方案和基础交互元素用户无需深入学习编程即可按需添加或删除版块、更换主题色或定制CSS样式。压缩包共13个文件整体仅24KB包含HTML主页、JPG/GIF图片素材、INI配置、文本说明等图片承担视觉展示HTML负责页面结构说明文档则提供部署指引文件组织轻量直观。目前已有157人浏览学习适合作为入门级前端模板参考。利用模板自带的响应式设计、SEO基础优化与多媒体支持用户只需替换主页栏目内容和图片素材即可生成个人专属的新闻收集网站同时也能在实操中理解网页文件组织与部署的基本方法。1. 个人新闻收集网站模板把散落信息源收进一个可检索的页面很多人第一反应是把它当成那种个人主页用的静态网页模板实际上这个资源解决的是另一类问题每天要盯七八个不同站点的更新挨个刷太浪费时间邮件订阅要么漏要么堆成山。这套模板是一套能合并 RSS、网页正文、行业站点信息流的聚合框架核心逻辑是把“采集 → 去重 → 分类 → 展示”四件事全拆好你只需要填订阅源地址、配好分类关键词跑一遍采集就能得到一个按时间和主题组织的新闻列表。适合高频接收信息的从业者——产品、运营、研究员以及想给自己搭信息看板但不想从零写爬虫和后端的开发者。2. 模板架构拆解四层流水线每一层的参数和边界在哪这套模板本质上是一条处理管道先抓取再判重接着分类入库最后渲染到页面上。单独看每一层都不难难的是层与层之间的参数怎么配合。比如采集太快容易被封、判重太宽松会让内容重复出现、分类规则写得太死会把该进“科技”的稿子送进“其他”。下面把这四层逐个拆开讲清楚每层的选型理由、配置项边界和翻车点。2.1 采集层RSS 优先、页面解析兜底以及源配置格式先明确一个原则能用 RSS 就不要去爬页面。RSS 是结构化 XML自带标题、链接、发布时间和摘要解析稳定、对服务器压力小也不会触发反爬。但现实里很多站点要么不提供 RSS要么只给摘要不给正文这时候才轮到页面解析上场。常见做法是给每个订阅源配一个类型字段模板根据类型走不同采集器。看一段配置# 订阅源的最小配置示例sources.json { source_type: rss, name: 科技快讯, url: https://news.example.com/feed.xml, category: tech, update_every_minutes: 30 }这里source_type二选一rss表示走标准 feedparser 解析page_parser表示走页面抓取。url指向源地址category决定入库后归到哪个栏目update_every_minutes是后续定时任务用的刷新间隔。如果是页面解析源配置多一个selector字段用来定位列表项模板内部再用抓下来的链接去取正文{ source_type: page_parser, name: 某行业站, url: https://industry.example.com/news, selector: ul.news-list li a, category: industry, update_every_minutes: 60 }用页面解析要有一个心理准备网页结构变化是常态今天能抓到明天可能就抓不到所以这类源要单独盯日志。模板里对页面解析源做了超时控制和 User-Agent 伪装但反爬强一点的站仍然可能返回 403这时候除了降频没有太多办法——别把采集间隔设得太短这是血泪经验。2.2 去重与分类标题指纹、相似度阈值和关键词规则采集只是第一步真正的坑在去重。一个稿子被多个站转载是常态如果只按 URL 去重同一篇内容换个链接就会重复出现如果按标题精确去重版权方稍微改个标点都能绕过。所以模板里做两层判定。第一层是标题指纹把标题归一化后取哈希import re import hashlib def normalize_title(title: str) - str: # 归一化转小写、压缩空白、把全角标点统一成半角 title title.lower().strip() title re.sub(r[\s|【】\[\]_\-—。:], , title) return title def make_fingerprint(title: str) - str: # 取title归一化后的SHA-1前16位作为指纹 # 16位够用且比完整哈希更省索引空间 return hashlib.sha1(normalize_title(title).encode(utf-8)).hexdigest()[:16]第二层是相似度阈值判定。指纹完全相同的直接扔掉但指纹不同、却几乎相同的内容会进相似度比较用字符 n-gram 的 Jaccard 系数打分超过 0.85 视为重复。模板里的参数DEDUP_THRESHOLD默认就是这个值实际使用中越低越严格调到 0.9 会漏调到 0.8 会误杀。注意页面解析源抓回来的标题经常带站点前缀比如“某行业站 | 某某新闻”所以归一化函数里把竖线和方括号都处理掉了。分类层用的是关键词表匹配每个栏目列一组关键词和权重CATEGORY_RULES { tech: [(软件, 3), (代码, 2), (开源, 2), (数据, 1)], industry: [(产能, 3), (供应链, 2), (订单, 2), (价格, 1)], } def classify(title: str, content: str) - str: # 标题命中关键词权重翻倍正文命中权重按1倍计 # 每个栏目累计分数取最高分栏目 best_cat, best_score other, 0 for cat, rules in CATEGORY_RULES.items(): score 0 for keyword, weight in rules: if keyword in title: score weight * 2 elif keyword in content: score weight if score best_score: best_cat, best_score cat, score return best_cat if best_score 3 else other关键词规则里的权重是模板里最需要你经常调的地方权重设太高会导致标题一沾词就归错类设太低又什么都进“other”。我一般会先跑一周样本看误判条目再反向调整权重。2.3 存储与数据模型三张表和一个唯一索引存储层选 SQLite理由很实际这套模板跑在单机或一台小服务器上数据量一天几千条SQLite 完全扛得住零运维备份就是把那个文件拷走比 MySQL 省心得多。模板里建了三张表sources记录订阅源信息items记录抓回来的条目categories做栏目字典CREATE TABLE IF NOT EXISTS sources ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, url TEXT NOT NULL UNIQUE, source_type TEXT NOT NULL DEFAULT rss, category TEXT, enabled INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_id INTEGER NOT NULL REFERENCES sources(id), title TEXT NOT NULL, link TEXT NOT NULL, summary TEXT, content TEXT, category TEXT, published_at TEXT, fingerprint TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime(now)), UNIQUE (link) ); CREATE INDEX IF NOT EXISTS idx_items_fingerprint ON items(fingerprint); CREATE INDEX IF NOT EXISTS idx_items_published_at ON items(published_at);关键在于items表里最后加的唯一约束和两个索引。UNIQUE (link)堵住同一 URL 重复入库fingerprint索引负责加速相似度判重查询。字段类型上所有时间都存文本格式的 UTC 时间戳这是为了避免后面展示层时区转换出幺蛾子。summary和content分开存列表页只读 summary详情页才拉 content省内存。2.4 展示层列表、详情、搜索和首页摘要卡片展示层是最容易让人误解的部分很多人把模板的目录结构看一遍觉得是个普通网站模板其实它内建的页面系统是围绕采集数据设置的。后台框架用的 Flask前端是 Jinja2 模板入口路由只有三个# app.py 核心路由 from flask import Flask, render_template, request, jsonify app Flask(__name__) app.route(/) def index(): # 默认按发布时间倒序分页每页 20 条 page request.args.get(page, 1, typeint) items fetch_items(categoryNone, pagepage, page_size20) return render_template(index.html, itemsitems, pagepage) app.route(/item/int:item_id) def detail(item_id): item fetch_item_by_id(item_id) return render_template(detail.html, itemitem) app.route(/search) def search(): q request.args.get(q, ) items search_items(q) return render_template(search.html, itemsitems, qq)首页模板用一个摘要卡片列表展示条目标题、来源名、发布时间和摘要三行点标题进详情页。搜索走 LIKE 查询数据量大了之后建议换全文索引这个放到最后一章说。整个展示层如果拿来当普通个人网页模板用会觉得页面太素但它的设计本来就不是为了好看而是为了信息密度——同一个页面能快速扫过几十条新闻并随时点进去看全文。3. 动手复现本地把模板跑起来从空目录到第一篇新闻上墙这一章是完整落地路径。我按自己复现时的顺序来写先搭环境、再改配置、然后启动采集验证链路。整个过程约十五分钟能通坑都标在参数旁边。3.1 环境搭建与模板目录结构先确认环境Python 3.9 及以上版本Linux、macOS 或 Windows 均可。模板对系统依赖很少主要依赖三个 Python 包feedparser负责 RSS 解析flask负责 Web 展示requests负责页面抓取。安装命令cd news-collector python3 -m venv venv source venv/bin/activate pip install -r requirements.txt模板根目录下的requirements.txt是给虚拟环境用的依赖清单。目录结构如下路径/文件说明app.pyFlask 入口负责页面路由与搜索collector.py采集脚本支持单次运行与常驻模式sources.json订阅源配置文件核心可改文件templates/首页、详情页、搜索页的 Jinja2 模板static/CSS 与少量前端脚本news.dbSQLite 数据库首次运行自动创建logs/collector.log采集日志问题排查第一现场先不用改任何代码直接跑一次空库初始化确认模板能自动建表python collector.py --init这条命令只建表不入数据。成功时会打印database initialized如果报错多半是 SQLite 版本过旧升级系统的 sqlite3 即可。3.2 配置订阅源三种不同源的写法改sources.json这是整个模板的灵魂文件。下面是一个能直接用的三源配置示例[ { source_type: rss, name: 科技快讯, url: https://news.example.com/feed.xml, category: tech, update_every_minutes: 30 }, { source_type: rss, name: 行业观察, url: https://industry.example.com/feed, category: industry, update_every_minutes: 60 }, { source_type: page_parser, name: 某垂直媒体, url: https://vert.example.com/list, selector: div.post-list h2 a, category: industry, update_every_minutes: 90 } ]RSS 源的核心字段是url页面解析源多一个selector。selector 写的是 CSS 选择器模板内部用它对页面做列表提取。这里有个容易犯的低级错误update_every_minutes不会因为值不同就导致模板同时跑多套时钟它只在常驻模式下被消费单次采集时这个字段会被忽略。也就是说手动跑采集时不要指望这个字段生效要验证定时刷新就得让常驻进程跑起来。改完配置后建议先做一次配置校验避免 JSON 语法错误把采集器直接带崩python collector.py --check-config校验命令会逐个源做网络连通性测试但不入库。这样能用最少的时间确认url是否可达、selector是否能命中不用等完整采集跑完再回头排查。3.3 启动采集与本地预览跑通链路只需要两条命令。第一条手动采集一轮python collector.py --once--once表示只执行一次抓取循环遍历sources.json里的所有源抓完即退出适合手动验证。如果配置了五个源命令会依次输出“抓取科技快讯 → 新增 12 条去重 3 条”。注意这里的“去重”既包括 link 撞库也包括指纹相似度被阈值拦下。第二条命令启动 Web 服务python app.py --port 8000服务启动后浏览http://127.0.0.1:8000看到的第一屏是最新的 20 条条目。如果页面是空的按照下面小节验证链路。3.4 验证链路日志、数据库检查与前台展示第一次跑完采集后页面没有内容先别怀疑模板坏了按顺序做三个检查。先看采集日志有没有报错tail -30 logs/collector.log常见的输出有两种fetched 10 items说明抓到了error: unable to fetch feed说明 URL 或网络有问题。网络问题在本地开发时更多是 DNS 或代理干扰抓不到的源单独用浏览器打开确认一下链接本身是否正常。再看数据库里到底进了多少条sqlite3 news.db SELECT category, COUNT(*) FROM items GROUP BY category;正常情况会按栏目返回一个计数列表。如果查出来是空表说明采集阶段就在源解析处挂掉了回到日志里看是哪一行报错。最后再看看 HTTP 服务本身通不通curl -s http://127.0.0.1:8000/ | head -20返回 HTML 片段就说明路由正常。这一步排查完整条采集到展示的链路就是通的。此时你会发现这套模板的日常使用节奏变成了跑采集 → 刷首页 → 看日志三个动作循环。4. 避坑笔记采集、编码、时区与去重失效的五个真实现场自己在本地和服务器上折腾这套模板前后遇到过不少怪问题挑五个最有代表性的写出来。每一条按现象、原因、解决三步记录你可以直接对照排查。4.1 RSS 正文被截断详情页只有一段摘要现象列表页正常点进详情页发现正文只有短短两行明显是从 RSS 摘要字段读过来的不是全文。原因很多 RSS 源只输出摘要content:encoded字段缺位。模板默认优先取全文字段取不到就退回摘要所以你看到的是摘要不是正文。解决给这类源打开fetch_full_article开关模板会用抓到的链接再走一次页面解析拿正文淘替换摘要。改法是在sources.json的对应源上加一个字段{ source_type: rss, name: 科技快讯, url: https://news.example.com/feed.xml, category: tech, fetch_full_article: true }打开后采集耗时明显变长因为每条 RSS 条目都要额外抓一次页面。建议只给重读价值的源开不要全局开启。4.2 同一篇转载稿反复进入列表去重形同虚设现象A 站发了一篇稿B 站和 C 站半小时内转载标题各删了几个字或加了站点前缀三条全进来了。原因每个站对标题的处理不同归一化函数处理不了所有变体指纹自然对不上。这是标题指纹法天然的死角。解决把相似度判定重新启用。模板里默认DEDUP_THRESHOLD 0.85同时将指纹比较后的相似条目做存活检查——如果待入库条目的标题与库内某条标题的相似度超过阈值就不入库直接记录一条 duck 日志。这里的代价是每次入库都要扫描同时间窗口内的文件名指纹记录源多了之后要加个索引。从那以后任何转载站只要标题变动幅度小于 15%都会被拦截。4.3 中文标题变乱码页面显示一堆问号现象数据库里存的内容是ç§æ€页面渲染成乱码。原因页面解析时源站的编码声明与实际内容不一致模板在requests.get阶段没有正确解码。尤其老一些的站点会按 GBK 输出但 HTTP 头写作 UTF-8导致文本被按错误编码解析。解决模板里加一次编码修正在文本落库前强制做检测。你可以在采集器里加一段这样的兜底逻辑def safe_decode(raw: bytes, declared_charset: str) - str: # 先显式按声明解码失败后回退到 utf-8再失败才用 uni for charset in [declared_charset, utf-8, gb18030]: try: return raw.decode(charset) except (UnicodeDecodeError, LookupError): continue return raw.decode(utf-8, errorsreplace)gb18030是兜底编码兼容 GBK 和 GB2312覆盖绝大多数中文老站。编码这块没有银弹新站基本都 UTF-8老站才需要兜底参数。4.4 时区差 8 小时“今日新闻”分区里全是昨天的现象首页按日期分组早上起来发现最新的内容被归到前一天发布时间慢了一圈。原因数据库存的是本地时间但展示层做了datetime.now()再格式化一旦服务器时区不是浏览器时区日期分组就全错位。解决模板里时间字段约定统一存 UTC展示时再做本地时区转换。这个原则很好记但很容易被忽略入库时一律datetime.now(timezone.utc)取出来渲染时再转本地时区。在配置里加一个display_timezone参数默认Asia/Shanghai{ display_timezone: Asia/Shanghai }这套下每个新增条目入库用 UTC 时间戳详情页格式化时才转到展示时区不会再出现跨日错乱。4.5 改了 sources.json 再跑一遍发现新配置根本没生效现象明明往sources.json里加了新源甚至删了旧源但采集结果还是按旧的源列表在跑。原因模板常驻模式把sources结构缓存在内存里改文件后进程没有重载采集器用的还是启动时的旧对象。解决改配置后先重启常驻进程。如果是手动单次运行则无需担心此问题因为--once每次启动都读新文件。如果你是常驻模式跑着改了配置一定要走一遍重载流程才可以。我一般把这个重载动作做成一个脚本先杀进程再重新拉起常驻采集顺带做一次--check-config确保配置没写错避免改了配置文件直接让进程崩掉。5. 进阶把这个模板改造成自己的私人信息看板模板基本跑通后剩下的玩法全在数据上做文章。它已经在仓库里存了标题、摘要、正文、分类、来源和时间这些字段足够支撑更实用的功能。我做过两个升级你可以直接照着改。第一个是星标收藏机制。给items表加一个starred整数列默认 0在详情页模板里加一个收藏按钮点击后通过一个/star/item_id路由翻转值首页加一个“只看星标”的 tab 和?starred1查询参数。这样每天的信息流里重要条目就不是看一遍就丢掉而是攒在数据库里形成自己的选题库。SQLite 加列很简单一条ALTER TABLE就行不需要数据迁移。第二个是本地全文搜索。数据量上了几万条后LIKE %关键词%会开始明显变慢这时给 SQLite 启用 FTS5 虚拟表CREATE VIRTUAL TABLE IF NOT EXISTS items_fts USING ft5(title, content, contentitems, content_rowidid);之后查询改用 MATCH 语法搜索速度和准确率都会上一个台阶代价是多占一点磁盘空间对于单机个人用完全可以忽略。第三个玩法是定时摘要推送。模板的采集器支持常驻模式配合update_every_minutes做定时抓取你可以再挂一个每日摘要脚本每天早上把前一天入库条目里分类为tech的标题和链接整理成一条 Markdown 消息发给自己的邮箱。常见做法是写一个独立脚本调 Telegram 的 Bot API 接口或者用任何支持 HTTP 发消息的协作工具把这个看板从“被动刷新”变成“主动推送”不用每天打开页面只需要等消息进来。从那以后我每次改完这套模板都强制走一遍验证链路先跑--check-config确认配置没写错再跑--once确认采集链路没断最后查一眼时区转换之后的日期分组确认跨日不会错乱。这套顺序帮我避开了很多次本地运行正常、部署到服务器就翻车的尴尬。希望帮到你。本文还有配套的精品资源点击获取