Python爬虫零基础入门【第八章:项目实战演练·第2节】项目 2:信息聚合站 Demo(列表+详情+增量+质量报告)!

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

全文目录:

      • [🌟 开篇语](#🌟 开篇语)
      • [📚 上期回顾](#📚 上期回顾)
      • [🎯 本篇目标](#🎯 本篇目标)
      • [💡 项目架构设计](#💡 项目架构设计)
      • [🗄️ 数据库设计](#🗄️ 数据库设计)
      • [🛠️ 核心模块实现](#🛠️ 核心模块实现)
        • [模块 1:列表页采集器](#模块 1:列表页采集器)
        • [模块 2:详情页采集器](#模块 2:详情页采集器)
        • [模块 3:数据管理器](#模块 3:数据管理器)
        • [模块 4:质量检查器](#模块 4:质量检查器)
        • [模块 5:主控制器](#模块 5:主控制器)
      • [📊 运行效果](#📊 运行效果)
      • [🚀 进阶功能](#🚀 进阶功能)
        • [功能 1:增量采集](#功能 1:增量采集)
        • [功能 2:导出数据](#功能 2:导出数据)
      • [📝 小结](#📝 小结)
      • [🎯 下期预告](#🎯 下期预告)
      • [🌟 文末](#🌟 文末)
        • [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
        • [✅ 互动征集](#✅ 互动征集)

🌟 开篇语

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

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

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

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

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

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

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

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

📚 上期回顾

上一篇《Python爬虫零基础入门【第八章:项目实战演练·第1节】项目 1:RSS 聚合器(采集→去重→入库→查询)!》我们完成了 RSS 聚合器,体验了从零到一搭建完整项目的成就感。但说实话,RSS 采集相对简单------数据格式标准、不需要复杂解析、也不涉及动态渲染。

真实的爬虫项目往往更复杂:你要从列表页找到详情页链接,再逐个采集详情;你要处理增量更新,避免重复劳动;你还要监控数据质量,确保采集结果靠谱。

今天,我们就来搞定一个真正能写进简历的作品级项目------信息聚合站!

🎯 本篇目标

看完这篇,你能做到:

  1. 实现二段式采集(列表页→详情页)
  2. 集成增量策略(只抓新增/更新内容)
  3. 设计质量报告(缺失率、重复率、异常值)
  4. 支持断点续爬(中断后无缝继续)

验收标准:从一个新闻站采集 200+ 篇文章,生成质量报告,支持断点续爬

💡 项目架构设计

核心流程
json 复制代码
1. 采集列表页 → 提取详情链接(去重)
2. 采集详情页 → 提取完整字段
3. 数据清洗 → 标准化处理
4. 入库存储 → 幂等写入
5. 质量检查 → 生成报告
技术选型
模块 技术选择 理由
列表采集 Requests/Playwright 优先 API,实在不行上浏览器
详情采集 Requests + BeautifulSoup 多数详情页是静态的
增量控制 时间戳 + 去重键 双重保险
数据存储 SQLite 轻量、零配置
任务管理 任务状态表 支持断点续爬

🗄️ 数据库设计

python 复制代码
# db_schema.py
import sqlite3

def init_database(db_path="aggregator.db"):
    """初始化数据库"""
    conn = sqlite3.connect(db_path)
    
    # 1. 文章表(主表)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS articles (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            
            -- 去重键
            dedup_key TEXT UNIQUE NOT NULL,
            
            -- 来源信息
            source TEXT NOT NULL,
            category TEXT,
            
            -- 基础信息(列表页能拿到的)
            title TEXT NOT NULL,
            list_url TEXT,
            detail_url TEXT NOT NULL,
            
            -- 详细信息(详情页才有的)
            author TEXT,
            publish_time TEXT,
            publish_timestamp INTEGER,
            summary TEXT,
            content TEXT,
            tags TEXT,
            view_count INTEGER,
            
            -- 元数据
            crawl_status TEXT DEFAULT 'PENDING',  -- PENDING/SUCCESS/FAILED
            error_msg TEXT,
            retry_count INTEGER DEFAULT 0,
            
            -- 时间戳
            created_at TEXT DEFAULT (datetime('now')),
            updated_at TEXT DEFAULT (datetime('now')),
            crawled_at TEXT
        )
    """)
    
    # 2. 创建索引
    conn.execute("CREATE INDEX IF NOT EXISTS idx_dedup_key ON articles(dedup_key)")
    conn.execute("CREATE INDEX IF NOT EXISTS idx_source ON articles(source)")
    conn.execute("CREATE INDEX IF NOT EXISTS idx_status ON articles(crawl_status)")
    conn.execute("CREATE INDEX IF NOT EXISTS idx_pub_time ON articles(publish_timestamp)")
    
    # 3. 采集状态表
    conn.execute("""
        CREATE TABLE IF NOT EXISTS crawl_state (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            source TEXT UNIQUE NOT NULL,
            last_crawl_time TEXT,
            last_detail_url TEXT,
            last_publish_time TEXT,
            total_crawled INTEGER DEFAULT 0,
            created_at TEXT DEFAULT (datetime('now'))
        )
    """)
    
    # 4. 质量报告表
    conn.execute("""
        CREATE TABLE IF NOT EXISTS quality_reports (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            batch_id TEXT NOT NULL,
            total_count INTEGER,
            success_count INTEGER,
            failed_count INTEGER,
            duplicate_count INTEGER,
            missing_fields TEXT,  -- JSON 格式
            created_at TEXT DEFAULT (datetime('now'))
        )
    """)
    
    conn.commit()
    conn.close()
    print("✅ 数据库初始化完成")

🛠️ 核心模块实现

模块 1:列表页采集器
python 复制代码
# list_crawler.py
import requests
from bs4 import BeautifulSoup
from hashlib import md5
import time

class ListCrawler:
    """列表页采集器"""
    
    def __init__(self, source_name):
        self.source = source_name
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })
    
    def crawl_list_page(self, url, page_num=1):
        """
        采集单个列表页
        
        Returns:
            list: [{title, detail_url, publish_time}, ...]
        """
        print(f"\n📄 采集列表页 {page_num}:{url}")
        
        try:
            resp = self.session.get(url, timeout=15)
            resp.raise_for_status()
            
            soup = BeautifulSoup(resp.text, 'html.parser')
            items = []
            
            # 根据实际页面调整选择器
            article_items = soup.select('.article-item')  # 示例选择器
            
            for item in article_items:
                try:
                    # 提取基础信息
                    title_elem = item.select_one('.title')
                    link_elem = item.select_one('a')
                    time_elem = item.select_one('.publish-time')
                    
                    if not title_elem or not link_elem:
                        continue
                    
                    title = title_elem.text.strip()
                    detail_url = link_elem.get('href')
                    
                    # 处理相对路径
                    if detail_url and not detail_url.startswith('http'):
                        from urllib.parse import urljoin
                        detail_url = urljoin(url, detail_url)
                    
                    publish_time = time_elem.text.strip() if time_elem else None
                    
                    # 生成去重键
                    dedup_key = md5(f"{self.source}_{detail_url}".encode()).hexdigest()
                    
                    items.append({
                        'source': self.source,
                        'title': title,
                        'detail_url': detail_url,
                        'publish_time': publish_time,
                        'dedup_key': dedup_key,
                        'list_url': url
                    })
                    
                except Exception as e:
                    print(f"   ⚠️ 提取单条失败:{e}")
                    continue
            
            print(f"   ✅ 提取到 {len(items)} 条链接")
            return items
            
        except Exception as e:
            print(f"   ❌ 列表页采集失败:{e}")
            return []
    
    def crawl_multiple_pages(self, base_url, max_pages=5):
        """
        采集多个列表页
        
        Args:
            base_url: 基础URL(包含 {page} 占位符)
                     如:https://news.example.com/list?page={page}
        """
        all_items = []
        
        for page in range(1, max_pages + 1):
            url = base_url.format(page=page)
            items = self.crawl_list_page(url, page)
            
            if not items:
                print(f"🛑 第 {page} 页无数据,停止采集")
                break
            
            all_items.extend(items)
            time.sleep(2)  # 礼貌延迟
        
        print(f"\n📊 列表页采集完成,共 {len(all_items)} 条链接")
        return all_items
模块 2:详情页采集器
python 复制代码
# detail_crawler.py
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import re

class DetailCrawler:
    """详情页采集器"""
    
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })
    
    def crawl_detail(self, url):
        """
        采集详情页
        
        Returns:
            dict: {author, content, tags, ...}
        """
        print(f"🔍 采集详情:{url}")
        
        try:
            resp = self.session.get(url, timeout=15)
            resp.raise_for_status()
            
            soup = BeautifulSoup(resp.text, 'html.parser')
            
            # 提取各字段(根据实际页面调整)
            data = {
                'author': self._extract_author(soup),
                'content': self._extract_content(soup),
                'tags': self._extract_tags(soup),
                'view_count': self._extract_view_count(soup),
                'summary': self._extract_summary(soup)
            }
            
            print(f"   ✅ 详情采集成功")
            return data
            ❌ 详情采集失败:{e}")
            return None
    
    def _extract_author(self, soup):
        """提取作者"""
        author_elem = soup.select_one('.author') or soup.select_one('[class*="author"]')
        return author_elem.text.strip() if author_elem else None
    
    def _extract_content(self, soup):
        """提取正文"""
        content_elem = soup.select_one('.content') or soup.select_one('article')
        if not content_elem:
            return None
        
        # 去除脚本和样式
        for tag in content_elem(['script', 'style']):
            tag.decompose()
        
        # 提取纯文本
        content = content_elem.get_text(separator='\n', strip=True)
        
        # 清洗空行
        content = re.sub(r'\n{3,}', '\n\n', content)
        
        return content
    
    def _extract_tags(self, soup):
        """提取标签"""
        tag_elems = soup.select('.tag') or soup.select('[class*="tag"]')
        if tag_elems:
            tags = [tag.text.strip() for tag in tag_elems]
            return ','.join(tags)
        return None
    
    def _extract_view_count(self, soup):
        """提取阅读量"""
        view_elem = soup.select_one('.view-count')
        if view_elem:
            text = view_elem.text
            # 提取数字(如 "阅读 1234" → 1234)
            match = re.search(r'(\d+)', text)
            if match:
                return int(match.group(1))
        return None
    
    def _extract_summary(self, soup):
        """提取摘要"""
        # 优先从 meta 标签提取
        meta_desc = soup.select_one('meta[name="description"]')
        if meta_desc:
            return meta_desc.get('content', '').strip()
        
        # 或从正文前 200 字
        content = self._extract_content(soup)
        if content:
            return content[:200] + '...'
        
        return None
模块 3:数据管理器
python 复制代码
# data_manager.py
import sqlite3
from datetime import datetime

class DataManager:
    """数据管理器"""
    
    def __init__(self, db_path="aggregator.db"):
        self.conn = sqlite3.connect(db_path, check_same_thread=False)
    
    def save_list_items(self, items):
        """
        批量保存列表页数据
        
        Returns:
            tuple: (inserted_count, skipped_count)
        """
        inserted = 0
        skipped = 0
        
        for item in items:
            try:
                self.conn.execute("""
                    INSERT INTO articles (
                        dedup_key, source, title, detail_url, 
                        list_url, publish_time, crawl_status
                    )
                    VALUES (?, ?, ?, ?, ?, ?, 'PENDING')
                """, (
                    item['dedup_key'],
                    item['source'],
                    item['title'],
                    item['detail_url'],
                    item.get('list_url'),
                    item.get('publish_time')
                ))
                inserted += 1
            except sqlite3.IntegrityError:
                # 重复,跳过
                skipped += 1
        
        self.conn.commit()
        return inserted, skipped
    
    def get_pending_details(self, limit=50):
        """获取待采集的详情链接"""
        cursor = self.conn.execute("""
            SELECT id, detail_url, dedup_key
            FROM articles
            WHERE crawl_status = 'PENDING'
              AND retry_count < 3
            ORDER BY created_at ASC
            LIMIT ?
        """, (limit,))
        
        return cursor.fetchall()
    
    def update_detail(self, dedup_key, detail_data):
        """更新详情数据"""
        self.conn.execute("""
            UPDATE articles
            SET author = ?,
                content = ?,
                tags = ?,
                view_count = ?,
                summary = ?,
                crawl_status = 'SUCCESS',
                crawled_at = datetime('now'),
                updated_at = datetime('now')
            WHERE dedup_key = ?
        """, (
            detail_data.get('author'),
            detail_data.get('content'),
            detail_data.get('tags'),
            detail_data.get('view_count'),
            detail_data.get('summary'),
            dedup_key
        ))
        self.conn.commit()
    
    def mark_failed(self, dedup_key, error_msg):
        """标记采集失败"""
        self.conn.execute("""
            UPDATE articles
            SET crawl_status = 'FAILED',
                error_msg = ?,
                retry_count = retry_count + 1,
                updated_at = datetime('now')
            WHERE dedup_key = ?
        """, (error_msg, dedup_key))
        self.conn.commit()
    
    def get_stats(self):
        """获取统计信息"""
        cursor = self.conn.execute("""
            SELECT 
                crawl_status,
                COUNT(*) as count
            FROM articles
            GROUP BY crawl_status
        """)
        
        stats = {row[0]: row[1] for row in cursor.fetchall()}
        return stats
模块 4:质量检查器
python 复制代码
# quality_checker.py
import json
from datetime import datetime

class QualityChecker:
    """数据质量检查器"""
    
    def __init__(self, db_manager):
        self.db = db_manager
    
    def generate_report(self, batch_id=None):
        """
        生成质量报告
        
        Returns:
            dict: 质量报告
        """
        if not batch_id:
            batch_id = datetime.now().strftime('%Y%m%d_%H%M%S')
        
        print("\n" + "="*50)
        print("📊 生成数据质量报告")
        print("="*50)
        
        report = {
            'batch_id': batch_id,
            'timestamp': datetime.now().isoformat(),
            'total_stats': self._check_total_stats(),
            'field_missing': self._check_field_missing(),
            'duplicate_check': self._check_duplicates(),
            'content_quality': self._check_content_quality()
        }
        
        # 打印报告
        self._print_report(report)
        
        # 保存到数据库
        self._save_report(report)
        
        return report
    
    def _check_total_stats(self):
        """统计总览"""
        stats = self.db.get_stats()
        return {
            'total': sum(stats.values()),
            'success': stats.get('SUCCESS', 0),
            'pending': stats.get('PENDING', 0),
            'failed': stats.get('FAILED', 0)
        }
    
    def _check_field_missing(self):
        """字段缺失率检查"""
        cursor = self.db.conn.execute("""
            SELECT 
                COUNT(*) as total,
                SUM(CASE WHEN author IS NULL THEN 1 ELSE 0 END) as missing_author,
                SUM(CASE WHEN content IS NULL OR content = '' THEN 1 ELSE 0 END) as missing_content,
                SUM(CASE WHEN publish_time IS NULL THEN 1 ELSE 0 END) as missing_time,
                SUM(CASE WHEN tags IS NULL THEN 1 ELSE 0 END) as missing_tags
            FROM articles
            WHERE crawl_status = 'SUCCESS'
        """)
        
        row = cursor.fetchone()
        total = row[0]
        
        if total == 0:
            return {}
        
        return {
            'author_missing_rate': round(row[1] / total * 100, 2),
            'content_missing_rate': round(row[2] / total * 100, 2),
            'time_missing_rate': round(row[3] / total * 100, 2),
            'tags_missing_rate': round(row[4] / total * 100, 2)
        }
    
    def _check_duplicates(self):
        """重复检查"""
        cursor = self.db.conn.execute("""
            SELECT COUNT(*) as dup_count
            FROM (
                SELECT title, COUNT(*) as cnt
                FROM articles
                GROUP BY title
                HAVING cnt > 1
            )
        """)
        
        dup_count = cursor.fetchone()[0]
        
        return {
            'duplicate_titles': dup_count
        }
    
    def _check_content_quality(self):
        """内容质量检查"""
        cursor = self.db.conn.execute("""
            SELECT 
                AVG(LENGTH(content)) as avg_content_length,
                MIN(LENGTH(content)) as min_content_length,
                MAX(LENGTH(content)) as max_content_length
            FROM articles
            WHERE content IS NOT NULL
              AND crawl_status = 'SUCCESS'
        """)
        
        row = cursor.fetchone()
        
        return {
            'avg_content_length': int(row[0]) if row[0] else 0,
            'min_content_length': int(row[1]) if row[1] else 0,
            'max_content_length': int(row[2]) if row[2] else 0
        }
    
    def _print_report(self, report):
        """打印报告"""
        print(f"\n📈 统计总览:")
        stats = report['total_stats']
        print(f"   总计:{stats['total']} 条")
        print(f"   成功:{stats['success']} 条")
        print(f"   待处理:{stats['pending']} 条")
        print(f"   失败:{stats['failed']} 条")
        
        print(f"\n⚠️ 字段缺失率:")
        missing = report['field_missing']
        for field, rate in missing.items():
            print(f"   {field}: {rate}%")
        
        print(f"\n🔍 重复检查:")
        print(f"   标题重复:{report['duplicate_check']['duplicate_titles']} 组")
        
        print(f"\n📝 内容质量:")
        quality = report['content_quality']
        print(f"   平均长度:{quality['avg_content_length']} 字符")
        print(f"   最短:{quality['min_content_length']} 字符")
        print(f"   最长:{quality['max_content_length']} 字符")
    
    def _save_report(self, report):
        """保存报告到数据库"""
        stats = report['total_stats']
        missing = report['field_missing']
        
        self.db.conn.execute("""
            INSERT INTO quality_reports (
                batch_id, total_count, success_count, 
                failed_count, duplicate_count, missing_fields
            )
            VALUES (?, ?, ?, ?, ?, ?)
        """, (
            report['batch_id'],
            stats['total'],
            stats['success'],
            stats['failed'],
            report['duplicate_check']['duplicate_titles'],
            json.dumps(missing, ensure_ascii=False)
        ))
        
        self.db.conn.commit()
模块 5:主控制器
python 复制代码
# main.py
from db_schema import init_database
from list_crawler import ListCrawler
from detail_crawler import DetailCrawler
from data_manager import DataManager
from quality_checker import QualityChecker
import time

class AggregatorSpider:
    """信息聚合爬虫主控制器"""
    
    def __init__(self, source_name):
        self.source = source_name
        self.list_crawler = ListCrawler(source_name)
        self.detail_crawler = DetailCrawler()
        self.db = DataManager()
        self.quality = QualityChecker(self.db)
    
    def run_full_crawl(self, list_url_template, max_pages=5):
        """完整采集流程"""
        print("="*50)
        print(f"🚀 开始采集:{self.source}")
        print("="*50)
        
        # 步骤 1:采集列表页
        print("\n【步骤 1/3】采集列表页...")
        list_items = self.list_crawler.crawl_multiple_pages(
            list_url_template, 
            max_pages=max_pages
        )
        
        if not list_items:
            print("❌ 列表页采集失败,终止")
            return
        
        # 步骤 2:保存列表数据
        print("\n【步骤 2/3】保存列表数据...")
        inserted, skipped = self.db.save_list_items(list_items)
        print(f"   ✅ 新增 {inserted} 条,跳过 {skipped} 条重复")
        
        # 步骤 3:采集详情页
        print("\n【步骤 3/3】采集详情页...")
        self._crawl_details()
        
        # 步骤 4:生成质量报告
        print("\n【步骤 4/4】生成质量报告...")
        self.quality.generate_report()
        
        print("\n" + "="*50)
        print("✨ 采集完成!")
        print("="*50)
    
    def _crawl_details(self, batch_size=10):
        """批量采集详情"""
        while True:
            # 获取待采集的详情
            pending = self.db.get_pending_details(limit=batch_size)
            
            if not pending:
                print("   ✅ 所有详情采集完成")
                break
            
            print(f"\n   📦 本批次待处理:{len(pending)} 条")
            
            for article_id, detail_url, dedup_key in pending:
                # 采集详情
                detail_data = self.detail_crawler.crawl_detail(detail_url)
                
                if detail_data:
                    # 更新数据库
                    self.db.update_detail(dedup_key, detail_data)
                else:
                    # 标记失败
                    self.db.mark_failed(dedup_key, "详情采集失败")
                
                time.sleep(1)  # 礼貌延迟
            
            # 显示进度
            stats = self.db.get_stats()
            print(f"   进度:成功 {stats.get('SUCCESS', 0)},"
                  f"待处理 {stats.get('PENDING', 0)},"
                  f"失败 {stats.get('FAILED', 0)}")

# 使用示例
if __name__ == '__main__':
    # 初始化数据库
    init_database()
    
    # 创建爬虫实例
    spider = AggregatorSpider(source_name='示例新闻网')
    
    # 运行完整采集
    spider.run_full_crawl(
        list_url_template='https://news.example.com/list?page={page}',
        max_pages=10
    )

📊 运行效果

json 复制代码
==
🚀 开始采集:示例新闻网
==================================================

【步骤 1/3】采集列表页...

📄 采集列表页 1:https://news.example.com/list?page=1
   ✅ 提取到 20 条链接

📄 采集列表页 2:https://news.example.com/list?page=2
   ✅ 提取到 20 条链接
...

📊 列表页采集完成,共 200 条链接

【步骤 2/3】保存列表数据...
   ✅ 新增 200 条,跳过 0 条重复

【步骤 3/3】采集详情页...

   📦 本批次待处理:10 条

🔍 采集详情:https://news.example.com/article/1
   ✅ 详情采集成功

...

   进度:成功 200,待处理 0,失败 0

【步骤 4/4】生成质量报告...

==================================================
📊 生成数据质量报告
==================================================

📈 统计总览:
   总计:200 条
   成功:200 条
   待处理:0 条
   失败:0 条

⚠️ 字段缺失率:
   author_missing_rate: 5.0%
   content_missing_rate: 0.0%
   time_missing_rate: 2.5%
   tags_missing_rate: 15.0%

🔍 重复检查:
   标题重复:0 组

📝 内容质量:
   平均长度:3500 字符
   最短:800 字符
   最长:15000 字符

==================================================
✨ 采集完成!
==================================================

🚀 进阶功能

功能 1:增量采集
python 复制代码
def run_incremental_crawl(self):
    """增量采集(只抓新增内容)"""
    # 获取上次采集的最后一条
    cursor = self.db.conn.execute("""
        SELECT detail_url, publish_time
        FROM articles
        WHERE source = ?
        ORDER BY created_at DESC
        LIMIT 1
    """, (self.source,))
    
    last_item = cursor.fetchone()
    
    if last_item:
        print(f"📌 上次采集到:{last_item[0]}")
        # 采集时遇到这条就停止
功能 2:导出数据
python 复制代码
def export_to_json(self, output_file='articles.json'):
    """导出为 JSON"""
    import json
    
    cursor = self.db.conn.execute("""
        SELECT source, title, detail_url, author, 
               publish_time, content, tags
        FROM articles
        WHERE crawl_status = 'SUCCESS'
        ORDER BY created_at DESC
    """)
    
    articles = []
    for row in cursor.fetchall():
        articles.append({
            'source': row[0],
            'title': row[1],
            'url': row[2],
            'author': row[3],
            'time': row[4],
            'content': row[5],
            'tags': row[6]
        })
    
    with open(output_file, 'w', encoding='utf-8') as f:
        json.dump(articles, f, ensure_ascii=False, indent=2)
    
    print(f"✅ 已导出 {len(articles)} 篇文章到 {output_file}")

📝 小结

今天我们完成了信息聚合站 Demo,这是一个真正的作品级项目,包含:

  1. 二段式采集(列表→详情)
  2. 增量策略(避免重复劳动)
  3. 质量报告(缺失率、重复率、内容质量)
  4. 断点续爬(任务状态管理)

这个项目可以直接写进简历,展示你的爬虫工程化能力!

🎯 下期预告

项目搭好了,但怎么让它自动运行?怎么定时采集?出错了怎么告警?

下一篇《上线与运维入门:定时运行、日志轮转、失败告警(轻量版)》,我们会学习如何部署到服务器、配置定时任务、设置监控告警------让你的爬虫真正"上线"!

验收作业:完成信息聚合站项目,从一个网站采集 200+ 篇文章,生成质量报告。顺便截图给我看看!加油!

🌟 文末

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

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

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

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

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

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

✅ 互动征集

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

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


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

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

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


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

相关推荐
翱翔的苍鹰2 小时前
多Agent智能体系统设计思路
java·python·深度学习·神经网络·机器学习·tensorflow
小北方城市网2 小时前
Spring Cloud Gateway 全链路监控与故障自愈实战
spring boot·python·rabbitmq·java-rabbitmq·数据库架构
weixin_440730502 小时前
04python编程笔记-04函数+05面向对象
笔记·python
weixin_462446232 小时前
用 Python 自动生成双面打印英语单词闪卡(Flashcards)PDF
python·pdf·记忆卡
wqwqweee2 小时前
Flutter for OpenHarmony 看书管理记录App实战:关于我们实现
android·javascript·python·flutter·harmonyos
东边的小山2 小时前
python 图形界面多个WORD按名字排序合并成一个WORD
python·c#·word
我的xiaodoujiao2 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 44--Pytest框架钩子函数
python·学习·测试工具·pytest
喵手2 小时前
Python爬虫零基础入门【第九章:实战项目教学·第5节】SQLite 入库实战:唯一键 + Upsert(幂等写入)!
爬虫·python·sqlite·爬虫实战·python爬虫工程化实战·零基础python爬虫教学·sqlite入库实战
DN20202 小时前
好用的机器人销售供应商
python