Python爬虫零基础入门【第八章:项目实战演练·第1节】项目 1:RSS 聚合器(采集→去重→入库→查询)!

🔥本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!!

全文目录:

      • [🌟 开篇语](#🌟 开篇语)
      • [📚 上期回顾](#📚 上期回顾)
      • [🎯 本篇目标](#🎯 本篇目标)
      • [💡 什么是 RSS?为什么选它?](#💡 什么是 RSS?为什么选它?)
        • [RSS 简介](#RSS 简介)
        • [RSS 数据结构示例](#RSS 数据结构示例)
      • [🗄️ 数据库设计](#🗄️ 数据库设计)
      • [🛠️ 代码实战:完整实现](#🛠️ 代码实战:完整实现)
      • [📊 运行效果](#📊 运行效果)
      • [⚠️ 常见问题处理](#⚠️ 常见问题处理)
        • [问题 1:RSS 源解析失败](#问题 1:RSS 源解析失败)
        • [问题 2:时间格式不统一](#问题 2:时间格式不统一)
      • [🚀 扩展功能](#🚀 扩展功能)
        • [扩展 1:定时采集](#扩展 1:定时采集)
        • [扩展 2:导出 OPML(RSS 源分享格式)](#扩展 2:导出 OPML(RSS 源分享格式))
      • [📝 小结](#📝 小结)
      • [🎯 下期预告](#🎯 下期预告)
      • [🌟 文末](#🌟 文末)
        • [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
        • [✅ 互动征集](#✅ 互动征集)

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 👉 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏《Python爬虫实战》

订阅后更新会优先推送,按目录学习更高效~

📚 上期回顾

恭喜你完成前 7 章的学习!从最基础的 Requests 静态爬取,到解析清洗、数据入库、增量去重、断点续爬,再到 Playwright 动态页面和 API 逆向------你已经掌握了爬虫工程化的完整链路。

但学了这么多,总感觉缺点什么?缺少一个能拿得出手的完整项目

今天开始,我们进入实战环节。第一个项目很实用:RSS 聚合器------从多个新闻源采集内容,去重入库,提供简单查询。麻雀虽小,五脏俱全,而且能真正跑起来!

🎯 本篇目标

看完这篇,你能做到:

  1. 理解 RSS 协议(最简单的数据源)
  2. 设计数据库表结构(多源聚合场景)
  3. 实现完整采集流程(采集→解析→去重→入库)
  4. 提供简单查询接口(按时间、关键词搜索)

验收标准:从 3 个 RSS 源采集至少 100 条新闻,存入数据库,能按关键词搜索

💡 什么是 RSS?为什么选它?

RSS 简介

RSS(Really Simple Syndication)是一种内容订阅协议,很多网站提供 RSS Feed 让用户订阅更新。

特点:

  • 数据格式标准(XML),不需要写复杂解析
  • 合法获取(网站主动提供)
  • 稳定可靠(格式变化少)
  • 适合新手(零门槛入门项目)

典型 RSS 源:

json 复制代码
https://www.zhihu.com/rss
https://sspai.com/feed
https://blog.example.com/feed.xml
RSS 数据结构示例
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>某科技博客</title>
    <link>https://blog.example.com</link>
    <item>
      <title>Python爬虫入门教程</title>
      <link>https://blog.example.com/post/123</link>
      <pubDate>Fri, 23 Jan 2026 10:00:00 GMT</pubDate>
      <description>今天我们来学习爬虫...</description>
      <guid>https://blog.example.com/post/123</guid>
    </item>
    <item>
      <title>数据清洗技巧</title>
      <link>https://blog.example.com/post/124</link>
      <pubDate>Fri, 22 Jan 2026 15:30:00 GMT</pubDate>
    </item>
  </channel>
</rss>

核心字段:

  • title:标题
  • link:原文链接
  • pubDate:发布时间
  • description:摘要/正文
  • guid:唯一标识(去重用)

🗄️ 数据库设计

表结构设计
sql 复制代码
-- 创建文章表
CREATE TABLE articles (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    
    -- 去重键(核心字段)
    dedup_key TEXT UNIQUE NOT NULL,
    
    -- 来源信息
    source_name TEXT NOT NULL,        -- 来源名称(如"知乎")
    source_url TEXT NOT NULL,         -- RSS 源地址
    
    -- 文章信息
    title TEXT NOT NULL,
    link TEXT NOT NULL,
    description TEXT,                 -- 摘要
    content TEXT,                     -- 完整正文(可选)
    author TEXT,                      -- 作者
    
    -- 时间信息
    pub_date TEXT,                    -- 发布时间(原始格式)
    pub_timestamp INTEGER,            -- 发布时间戳(方便排序)
    crawled_at TEXT NOT NULL,         -- 采集时间
    
    -- 索引字段
    created_at TEXT DEFAULT (datetime('now')),
    updated_at TEXT DEFAULT (datetime('now'))
);

-- 创建索引(提升查询性能)
CREATE INDEX idx_source ON articles(source_name);
CREATE INDEX idx_pub_timestamp ON articles(pub_timestamp);
CREATE INDEX idx_crawled_at ON articles(crawled_at);
CREATE INDEX idx_title ON articles(title);  -- 支持关键词搜索

-- 创建 RSS 源管理表
CREATE TABLE rss_sources (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT UNIQUE NOT NULL,        -- 源名称
    url TEXT UNIQUE NOT NULL,         -- RSS 地址
    enabled BOOLEAN DEFAULT TRUE,     -- 是否启用
    last_crawl_time TEXT,            -- 上次采集时间
    article_count INTEGER DEFAULT 0,  -- 累计文章数
    created_at TEXT DEFAULT (datetime('now'))
);

🛠️ 代码实战:完整实现

第一步:安装依赖
bash 复制代码
pip install feedparser requests

feedparser 是专门解析 RSS/Atom 的库,非常好用!

第二步:RSS 源管理器
python 复制代码
import sqlite3
from datetime import datetime
from pathlib import Path

class RSSSourceManager:
    """RSS 源管理器"""
    
    def __init__(self, db_path="rss_aggregator.db"):
        self.db_path = db_path
        self.conn = sqlite3.connect(db_path, check_same_thread=False)
        self._init_db()
    
    def _init_db(self):
        """初始化数据库表"""
        # 创建文章表
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS articles (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                dedup_key TEXT UNIQUE NOT NULL,
                source_name TEXT NOT NULL,
                source_url TEXT NOT NULL,
                title TEXT NOT NULL,
                link TEXT NOT NULL,
                description TEXT,
                author TEXT,
                pub_date TEXT,
                pub_timestamp INTEGER,
                crawled_at TEXT NOT NULL,
                created_at TEXT DEFAULT (datetime('now')),
                updated_at TEXT DEFAULT (datetime('now'))
            )
        """)
        
        # 创建索引
        self.conn.execute("CREATE INDEX IF NOT EXISTS idx_source ON articles(source_name)")
        self.conn.execute("CREATE INDEX IF NOT EXISTS idx_pub_timestamp ON articles(pub_timestamp)")
        
        # 创建 RSS 源表
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS rss_sources (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT UNIQUE NOT NULL,
                url TEXT UNIQUE NOT NULL,
                enabled BOOLEAN DEFAULT TRUE,
                last_crawl_time TEXT,
                article_count INTEGER DEFAULT 0,
                created_at TEXT DEFAULT (datetime('now'))
            )
        """)
        
        self.conn.commit()
    
    def add_source(self, name, url):
        """添加 RSS 源"""
        try:
            self.conn.execute("""
                INSERT INTO rss_sources (name, url)
                VALUES (?, ?)
            """, (name, url))
            self.conn.commit()
            print(f"✅ 添加 RSS 源:{name}")
            return True
        except sqlite3.IntegrityError:
            print(f"⚠️ RSS 源已存在:{name}")
            return False
    
    def get_enabled_sources(self):
        """获取启用的 RSS 源"""
        cursor = self.conn.execute("""
            SELECT name, url FROM rss_sources WHERE enabled = 1
        """)
        return cursor.fetchall()
    
    def update_crawl_time(self, name):
        """更新采集时间"""
        self.conn.execute("""
            UPDATE rss_sources
            SET last_crawl_time = datetime('now')
            WHERE name = ?
        """, (name,))
        self.conn.commit()
第三步:RSS 采集器
python 复制代码
import feedparser
import requests
from hashlib import md5
from datetime import datetime
import time

class RSSCrawler:
    """RSS 采集器"""
    
    def __init__(self, db_manager):
        self.db = db_manager
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (compatible; RSSBot/1.0)'
        })
    
    def fetch_feed(self, source_name, feed_url):
        """
        采集单个 RSS 源
        
        Returns:
            int: 新增文章数
        """
        print(f"\n🔍 正在采集:{source_name}")
        print(f"   URL: {feed_url}")
        
        try:
            # 方式 1:直接用 feedparser(推荐)
            feed = feedparser.parse(feed_url)
            
            # 方式 2:先用 requests 下载再解析(更可控)
            # resp = self.session.get(feed_url, timeout=30)
            # feed = feedparser.parse(resp.content)
            
            if feed.bozo:  # 解析出错
                print(f"❌ RSS 解析失败:{feed.bozo_exception}")
                return 0
            
            entries = feed.entries
            print(f"   📄 发现 {len(entries)} 篇文章")
            
            new_count = 0
            for entry in entries:
                if self.save_article(source_name, feed_url, entry):
                    new_count += 1
            
            print(f"   ✅ 新增 {new_count} 篇文章")
            
            # 更新采集时间
            self.db.update_crawl_time(source_name)
            
            return new_count
            
        except Exception as e:
            print(f"❌ 采集失败:{e}")
            return 0
    
    def save_article(self, source_name, source_url, entry):
        """
        保存单篇文章(带去重)
        
        Returns:
            bool: 是否为新增
        """
        # 生成去重键(优先用 guid,否则用 link)
        unique_id = entry.get('id') or entry.get('link')
        dedup_key = md5(f"{source_name}_{unique_id}".encode()).hexdigest()
        
        # 提取字段
        title = entry.get('title', '').strip()
        link = entry.get('link', '').strip()
        description = entry.get('description') or entry.get('summary', '')
        author = entry.get('author', '')
        
        # 解析时间
        pub_date = entry.get('published') or entry.get('updated')
        pub_timestamp = None
        
        if pub_date:
            try:
                # feedparser 会自动解析时间
                time_struct = entry.get('published_parsed') or entry.get('updated_parsed')
                if time_struct:
                    pub_timestamp = int(time.mktime(time_struct))
            except:
                pass
        
        # 插入数据库(去重)
        try:
            self.db.conn.execute("""
                INSERT INTO articles (
                    dedup_key, source_name, source_url,
                    title, link, description, author,
                    pub_date, pub_timestamp, crawled_at
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
            """, (
                dedup_key, source_name, source_url,
                title, link, description, author,
                pub_date, pub_timestamp
            ))
            self.db.conn.commit()
            return True  # 新增成功
            
        except sqlite3.IntegrityError:
            # 重复数据,跳过
            return False
    
    def crawl_all_sources(self):
        """采集所有启用的 RSS 源"""
        sources = self.db.get_enabled_sources()
        
        if not sources:
            print("⚠️ 没有可用的 RSS 源")
            return
        
        print(f"📦 共 {len(sources)} 个 RSS 源待采集")
        
        total_new = 0
        for name, url in sources:
            new_count = self.fetch_feed(name, url)
            total_new += new_count
            time.sleep(1)  # 礼貌延迟
        
        print(f"\n✨ 采集完成!共新增 {total_new} 篇文章")
第四步:查询接口
python 复制代码
class RSSQuery:
    """RSS 查询接口"""
    
    def __init__(self, db_manager):
        self.db = db_manager
    
    def search_by_keyword(self, keyword, limit=20):
        """按关键词搜索"""
        cursor = self.db.conn.execute("""
            SELECT source_name, title, link, pub_date, description
            FROM articles
            WHERE title LIKE ? OR description LIKE ?
            ORDER BY pub_timestamp DESC
            LIMIT ?
        """, (f'%{keyword}%', f'%{keyword}%', limit))
        
        return cursor.fetchall()
    
    def get_latest(self, source=None, limit=20):
        """获取最新文章"""
        if source:
            cursor = self.db.conn.execute("""
                SELECT source_name, title, link, pub_date
                FROM articles
                WHERE source_name = ?
                ORDER BY pub_timestamp DESC
                LIMIT ?
            """, (source, limit))
        else:
            cursor = self.db.conn.execute("""
                SELECT source_name, title, link, pub_date
                FROM articles
                ORDER BY pub_timestamp DESC
                LIMIT ?
            """, (limit,))
        
        return cursor.fetchall()
    
    def get_stats(self):
        """获取统计信息"""
        cursor = self.db.conn.execute("""
            SELECT 
                source_name,
                COUNT(*) as count,
                MAX(pub_date) as latest_pub_date
            FROM articles
            GROUP BY source_name
            ORDER BY count DESC
        """)
        
        return cursor.fetchall()
第五步:主程序
python 复制代码
# main.py
from rss_aggregator import RSSSourceManager, RSSCrawler, RSSQuery

def init_sources(manager):
    """初始化 RSS 源(首次运行时调用)"""
    sources = [
        ('少数派', 'https://sspai.com/feed'),
        ('阮一峰博客', 'https://www.ruanyifeng.com/blog/atom.xml'),
        ('Python官方博客', 'https://blog.python.org/feeds/posts/default')
    ]
    
    for name, url in sources:
        manager.add_source(name, url)

def main():
    # 初始化
    db_manager = RSSSourceManager()
    crawler = RSSCrawler(db_manager)
    query = RSSQuery(db_manager)
    
    # 首次运行:添加 RSS 源
    # init_sources(db_manager)
    
    # 采集所有源
    print("="*50)
    print("🚀 开始采集 RSS...")
    print("="*50)
    crawler.crawl_all_sources()
    
    # 查询统计
    print("\n" + "="*50)
    print("📊 采集统计")
    print("="*50)
    stats = query.get_stats()
    for source, count, latest in stats:
        print(f"  {source}: {count} 篇(最新:{latest})")
    
    # 查询最新文章
    print("\n" + "="*50)
    print("📰 最新 10 篇文章")
    print("="*50)
    latest = query.get_latest(limit=10)
    for i, (source, title, link, pub_date) in enumerate(latest, 1):
        print(f"{i}. [{source}] {title}")
        print(f"   {link}")
        print()
    
    # 关键词搜索
    keyword = "Python"
    print(f"\n{'='*50}")
    print(f"🔍 搜索关键词:{keyword}")
    print("="*50)
    results = query.search_by_keyword(keyword, limit=5)
    for source, title, link, pub_date, desc in results:
        print(f"[{source}] {title}")
        print(f"   {link}")
        print()

if __name__ == '__main__':
    main()

📊 运行效果

json 复制代码
==================================================
🚀 开始采集 RSS...
==================================================
📦 共 3 个 RSS 源待采集

🔍 正在采集:少数派
   URL: https://sspai.com/feed
   📄 发现 20 篇文章
   ✅ 新增 20 篇文章

🔍 正在采集:阮一峰博客
   URL: https://www.ruanyifeng.com/blog/atom.xml
   📄 发现 15 篇文章
   ✅ 新增 15 篇文章

🔍 正在采集:Python官方博客
   URL: https://blog.python.org/feeds/posts/default
   📄 发现 10 篇文章
   ✅ 新增 10 篇文章

✨ 采集完成!共新增 45 篇文章

==================================================
📊 采集统计
==================================================
  少数派: 20 篇(最新:2026-01-23)
  阮一峰博客: 15 篇(最新:2026-01-22)
  Python官方博客: 10 篇(最新:2026-01-21)

==================================================
📰 最新 10 篇文章
==================================================
1. [少数派] 2026年值得入手的生产力工具
   https://sspai.com/post/123456

2. [阮一峰博客] 科技爱好者周刊(第 288 期)
   https://www.ruanyifeng.com/blog/2026/01/weekly-issue-288.html
...

⚠️ 常见问题处理

问题 1:RSS 源解析失败

现象:feed.bozo = True

原因:XML 格式不规范、编码问题、网络超时

解决:

python 复制代码
# 容错处理
if feed.bozo:
    print(f"⚠️ 解析有问题,但尝试继续:{feed.bozo_exception}")
    # 有些源虽然 bozo=True 但 entries 还是能用的
    if not feed.entries:
        return 0
问题 2:时间格式不统一

解决:

python 复制代码
from dateutil import parser

def parse_date_safe(date_str):
    """容错时间解析"""
    try:
        return parser.parse(date_str)
    except:
        return None

🚀 扩展功能

扩展 1:定时采集
python 复制代码
import schedule

def job():
    print(f"\n⏰ {datetime.now()} 开始定时采集")
    crawler.crawl_all_sources()

# 每小时采集一次
schedule.every(1).hours.do(job)

while True:
    schedule.run_pending()
    time.sleep(60)
扩展 2:导出 OPML(RSS 源分享格式)
python 复制代码
def export_opml(db_manager):
    """导出 RSS 源列表为 OPML 格式"""
    sources = db_manager.get_enabled_sources()
    
    opml = '<?xml version="1.0" encoding="UTF-8"?>\n'
    opml += '<opml version="2.0">\n'
    opml += '  <body>\n'
    
    for name, url in sources:
        opml += f'    <outline text="{name}" xmlUrl="{url}"/>\n'
    
    opml += '  </body>\n'
    opml += '</opml>'
    
    with open('subscriptions.opml', 'w', encoding='utf-8') as f:
        f.write(opml)
    
    print("✅ OPML 已导出:subscriptions.opml")

📝 小结

今天我们完成了第一个完整项目------RSS 聚合器,涵盖了:

  1. 数据库设计(去重键、索引、多源管理)
  2. RSS 解析(feedparser 使用)
  3. 采集流程(循环采集、去重入库)
  4. 查询接口(关键词搜索、最新文章)

这个项目麻雀虽小,但包含了爬虫工程化的核心要素。你可以把它部署到服务器上,每小时定时采集,打造属于自己的信息聚合平台!

🎯 下期预告

RSS 聚合器相对简单,下一篇我们要挑战更硬的骨头------信息聚合站 Demo

《项目 2:信息聚合站 Demo(列表+详情+增量+质量报告)》将实现:

  • 列表页→详情页的二段式采集
  • 增量采集策略
  • 数据质量报告
  • 断点续爬机制

这是一个真正的"作品级"项目,可以写进简历!

验收作业:部署你的 RSS 聚合器,从 3 个源采集 100+ 篇文章,截图给我看看!加油!

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。

相关推荐
沛沛老爹2 小时前
从Web到AI:多模态Agent Skills开发实战——JavaScript+Python全栈赋能视觉/语音能力
java·开发语言·javascript·人工智能·python·安全架构
storyseek2 小时前
RAG的四种的检索方式
python
一只大侠的侠2 小时前
用PyTorch Lightning快速搭建可复现实验 pipeline
人工智能·pytorch·python
偷星星的贼112 小时前
Python虚拟环境(venv)完全指南:隔离项目依赖
jvm·数据库·python
一株月见草哇2 小时前
[python/uv]现代化python工具[先占坑]
python·uv
Leinwin2 小时前
Azure 存储重磅发布系列创新 以 AI 与云原生能力解锁数据未来
后端·python·flask
无心水2 小时前
4、Go语言程序实体详解:变量声明与常量应用【初学者指南】
java·服务器·开发语言·人工智能·python·golang·go
充值修改昵称2 小时前
数据结构基础:B*树B+树的极致优化
数据结构·b树·python·算法
one____dream2 小时前
【算法】相同的树与对称二叉树
b树·python·算法·递归