Python爬虫零基础入门【第三章:Requests 静态爬取入门·第5节】限速与礼貌爬取:并发、延迟、频率控制!

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

全文目录:

      • [🌟 开篇语](#🌟 开篇语)
      • [📌 上期回顾](#📌 上期回顾)
      • [🎯 本节目标](#🎯 本节目标)
      • 一、三种存储方案对比
        • [1.1 快速对比表](#1.1 快速对比表)
        • [1.2 选择决策树](#1.2 选择决策树)
      • [二、CSV 存储:简单高效](#二、CSV 存储:简单高效)
        • [2.1 基础写入](#2.1 基础写入)
        • [2.2 使用 pandas(推荐)](#2.2 使用 pandas(推荐))
        • [2.3 增量追加](#2.3 增量追加)
        • [2.4 处理复杂字段](#2.4 处理复杂字段)
      • [三、JSON 存储:灵活通用](#三、JSON 存储:灵活通用)
        • [3.1 标准 JSON 格式](#3.1 标准 JSON 格式)
        • [3.2 JSON Lines 格式(推荐用于大数据)](#3.2 JSON Lines 格式(推荐用于大数据))
        • [3.3 压缩存储(节省空间)](#3.3 压缩存储(节省空间))
      • [四、SQLite 存储:结构化查询](#四、SQLite 存储:结构化查询)
        • [4.1 为什么选择 SQLite?](#4.1 为什么选择 SQLite?)
        • [4.2 创建数据库和表](#4.2 创建数据库和表)
        • [4.3 使用 pandas 操作 SQLite](#4.3 使用 pandas 操作 SQLite)
      • 五、存储方案选择指南
        • [5.1 根据数据量选择](#5.1 根据数据量选择)
        • [5.2 多格式保存(保险策略)](#5.2 多格式保存(保险策略))
      • 六、性能对比测试
      • 七、最佳实践建议
        • [7.1 存储策略组合](#7.1 存储策略组合)
        • [7.2 数据备份策略](#7.2 数据备份策略)
      • 八、本节小结
      • 九、课后作业(必做,验收进入第四章)
      • [🔮 下期预告](#🔮 下期预告)
      • [🌟 文末](#🌟 文末)
        • [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
        • [✅ 互动征集](#✅ 互动征集)

🌟 开篇语

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

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

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

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

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

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

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

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

📌 上期回顾

在上一节《列表页→详情页:两段式采集(90%项目都这样)》中,我们完成了两段式采集的完整流程:从列表页提取 URL,再逐个采集详情页。你已经能够获取结构化的数据了。

但数据采集完成后,如何高效地存储和管理?这是爬虫工程化的最后一公里!📦

这一节,我们将学习三种最常用的数据存储方案:CSV(轻量简单)、JSON(灵活通用)、SQLite(结构化查询)。选对存储方案,能让后续的数据分析事半功倍!

🎯 本节目标

通过本节学习,你将能够:

  1. 理解三种存储方案的优缺点和适用场景
  2. 使用 pandas 高效读写 CSV 文件
  3. 掌握 JSON Lines 格式(适合增量追加)
  4. 使用 SQLite 存储和查询数据
  5. 设计存储方案的决策树
  6. 交付验收:为采集器添加多种存储方式,并对比性能

一、三种存储方案对比

1.1 快速对比表
特性 CSV JSON SQLite
学习曲线 ⭐ 简单 ⭐⭐ 简单 ⭐⭐⭐ 中等
数据结构 表格(二维) 树形(嵌套) 关系型(多表)
可读性 ✅ Excel打开 ✅ 文本编辑器 ❌ 需工具查看
查询能力 ❌ 需加载全部 ❌ 需加载全部 ✅ SQL查询
增量追加 ⚠️ 需重写 ✅ 逐行追加 ✅ INSERT
大数据 ⚠️ 内存限制 ⚠️ 内存限制 ✅ 磁盘操作
字段类型 ❌ 都是文本 ✅ 支持类型 ✅ 强类型
适用场景 表格数据分析 配置、日志 数据库应用
1.2 选择决策树
json 复制代码
你的数据特点是什么?
│
├─ 简单表格(每行结构相同)
│  └─ 需要用Excel打开?
│     ├─ 是 → CSV
│     └─ 否 → 需要查询?
│        ├─ 是 → SQLite
│        └─ 否 → CSV
│
├─ 嵌套结构(数组、对象)
│  └─ 数据量大(>10万条)?
│     ├─ 是 → SQLite(JSON字段)
│     └─ 否 → JSON
│
└─ 需要关联查询(多表JOIN)
└─ SQLite

二、CSV 存储:简单高效

2.1 基础写入
python 复制代码
import csv
from datetime import datetime

def save_to_csv(data_list, filename):
    """
    保存数据到 CSV
    
    Args:
        data_list: 字典列表
        filename: 文件名
    """
    if not data_list:
        print("跳过保存")
        return
    
    # 提取字段名(从第一条数据)
    fieldnames = data_list[0].keys()
    
    with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        
        # 写入表头
        writer.writeheader()
        
        # 写入数据
        writer.writerows(data_list)
    
    print(f"✅ CSV 已保存: {filename}")
    print(f"📊 共 {len(data_list)} 条记录")

# 使用示例
data = [
    {'title': '新闻A', 'author': '张三', 'views': 1000},
    {'title': '新闻B', 'author': '李四', 'views': 2000},
]

save_to_csv(data, 'news.csv')
2.2 使用 pandas(推荐)
python 复制代码
import pandas as pd

def save_to_csv_pandas(data_list, filename):
    """
    使用 pandas 保存 CSV
    
    优势:
    1. 自动处理缺失字段
    2. 支持数据类型推断
    3. 性能更好
    """
    if not data_list:
        print("⚠️  数据为空")
        return
    
    # 转为 DataFrame
    df = pd.DataFrame(data_list)
    
    # 保存(utf-8-sig 让 Excel 不乱码)
    df.to_csv(filename, index=False, encoding='utf-8-sig')
    
    print(f"✅ CSV 已保存: {filename}")
    print(f"📊 形状: {df.shape} (行×列)")
    print(f"\n字段: {', '.join(df.columns)}")

# 使用
save_to_csv_pandas(data, 'news_pandas.csv')
2.3 增量追加
python 复制代码
def append_to_csv(data_list, filename):
    """
    追加数据到已存在的 CSV
    
    注意:确保字段顺序一致
    """
    import os
    
    # 检查文件是否存在
    file_exists = os.path.exists(filename)
    
    if not data_list:
        return
    
    fieldnames = data_list[0].keys()
    
    with open(filename, 'a', newline='', encoding='utf-8-sig') as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        
        # 如果是新文件,写入表头
        if not file_exists:
            writer.writeheader()
        
        # 追加数据
        writer.writerows(data_list)
    
    print(f"✅ 已追加 {len(data_list)} 条记录到 {filename}")

# pandas 方式
def append_to_csv_pandas(data_list, filename):
    """pandas 追加(更可靠)"""
    new_df = pd.DataFrame(data_list)
    
    if os.path.exists(filename):
        # 读取现有数据
        existing_df = pd.read_csv(filename, encoding='utf-8-sig')
        # 合并
        combined_df = pd.concat([existing_df, new_df], ignore_index=True)
    else:
        combined_df = new_df
    
    # 保存
    combined_df.to_csv(filename, index=False, encoding='utf-8-sig')
    print(f"✅ 已保存 {len(combined_df)} 条记录")
2.4 处理复杂字段
python 复制代码
def prepare_for_csv(data):
    """
    预处理数据以适配 CSV
    
    处理:
    1. 数组 → 逗号分隔字符串
    2. 嵌套对象 → JSON 字符串
    3. None → 空字符串
    """
    import json
    
    processed = {}
    
    for key, value in data.items():
        if value is None:
            processed[key] = ''
        elif isinstance(value, list):
            # 数组转字符串
            if all(isinstance(x, str) for x in value):
                processed[key] = ', '.join(value)
            else:
                processed[key] = json.dumps(value, ensure_ascii=False)
        elif isinstance(value, dict):
            # 字典转 JSON
            processed[key] = json.dumps(value, ensure_ascii=False)
        else:
            processed[key] = value
    
    return processed

# 使用
data = {
    'title': '新闻标题',
    'tags': ['科技', '互联网'],  # 数组
    'meta': {'author': '张三', 'dept': '编辑部'},  # 嵌套对象
}

csv_ready = prepare_for_csv(data)
print(csv_ready)
# {'title': '新闻标题', 'tags': '科技, 互联网', 'meta': '{"author": "张三", ...}'}

三、JSON 存储:灵活通用

3.1 标准 JSON 格式
python 复制代码
import json

def save_to_json(data_list, filename, indent=2):
    """
    保存为标准 JSON 文件
    
    Args:
        data_list: 数据列表
        filename: 文件名
        indent: 缩进空格数(None=压缩,2=易读)
    """
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data_list, f, ensure_ascii=False, indent=indent)
    
    print(f"✅ JSON 已保存: {filename}")
    print(f"📊 共 {len(data_list)} 条记录")

# 使用
save_to_json(data, 'news.json', indent=2)

生成的文件格式

json 复制代码
[
  {
    "title": "新闻A",
    "author": "张三",
    "tags": ["科技", "互联网"]
  },
  {
    "title": "新闻B",
    "author": "李四",
    "tags": ["财经"]
  }
]
3.2 JSON Lines 格式(推荐用于大数据)

什么是 JSON Lines?

每行一个 JSON 对象,适合:

  • ✅ 流式处理(逐行读取)
  • ✅ 增量追加(直接写入)
  • ✅ 大文件处理(不需要全部加载)
python 复制代码
def save_to_jsonl(data_list, filename):
    """
    保存为 JSON Lines 格式
    
    格式:每行一个 JSON 对象
    """
    with open(filename, 'w', encoding='utf-8') as f:
        for item in data_list:
            json_line = json.dumps(item, ensure_ascii=False)
            f.write(json_line + '\n')
    
    print(f"✅ JSONL 已保存: {filename}")

def append_to_jsonl(data_list, filename):
    """追加到 JSON Lines 文件"""
    with open(filename, 'a', encoding='utf-8') as f:
        for item in data_list:
            json_line = json.dumps(item, ensure_ascii=False)
            f.write(json_line + '\n')
    
    print(f"✅ 已追加 {len(data_list)} 条记录")

def read_jsonl(filename):
    """读取 JSON Lines 文件(逐行)"""
    data = []
    with open(filename, 'r', encoding='utf-8') as f:
        for line in f:
            data.append(json.loads(line))
    return data

# 使用
save_to_jsonl(data, 'news.jsonl')

生成的文件格式

json 复制代码
{"title": "新闻A", "author": "张三", "tags": ["科技", "互联网"]}
{"title": "新闻B", "author": "李四", "tags": ["财经"]}
3.3 压缩存储(节省空间)
python 复制代码
import gzip

def save_to_json_gz(data_list, filename):
    """保存为压缩的 JSON(节省 70-90% 空间)"""
    json_str = json.dumps(data_list, ensure_ascii=False)
    
    with gzip.open(filename, 'wt', encoding='utf-8') as f:
        f.write(json_str)
    
    # 对比文件大小
    import os
    normal_size = len(json_str.encode())
    compressed_size = os.path.getsize(filename)
    ratio = (1 - compressed_size / normal_size) * 100
    
    print(f"✅ 压缩 JSON 已保存: {filename}")
    print(f"📊 压缩率: {ratio:.1f}% ({normal_size} → {compressed_size} 字节)")

def read_json_gz(filename):
    """读取压缩的 JSON"""
    with gzip.open(filename, 'rt', encoding='utf-8') as f:
        return json.load(f)

# 使用
save_to_json_gz(data, 'news.json.gz')

四、SQLite 存储:结构化查询

4.1 为什么选择 SQLite?

优势

  • ✅ 零配置(Python 内置)
  • ✅ 支持 SQL 查询(WHERE、JOIN、GROUP BY)
  • ✅ 事务支持(ACID)
  • ✅ 适合中小型数据(<100GB)

适用场景

  • 需要按条件查询数据
  • 需要更新/删除记录
  • 多表关联查询
  • 数据分析和统计
4.2 创建数据库和表
python 复制代码
import sqlite3
from datetime import datetime

class NewsDatabase:
    """新闻数据库管理器"""
    
    def __init__(self, db_file='news.db'):
        """初始化数据库连接"""
        self.db_file = db_file
        self.conn = sqlite3.connect(db_file)
        self.cursor = self.conn.cursor()
        self._create_tables()
    
    def _create_tables(self):
        """创建表结构"""
        # 新闻表
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS news (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                url TEXT UNIQUE NOT NULL,
                title TEXT NOT NULL,
                author TEXT,
                publish_time TEXT,
                content TEXT,
                views INTEGER DEFAULT 0,
                crawl_time TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        
        # 标签表(多对多关系示例)
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS tags (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT UNIQUE NOT NULL
            )
        ''')
        
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS news_tags (
                news_id INTEGER,
                tag_id INTEGER,
                FOREIGN KEY (news_id) REFERENCES news(id),
                FOREIGN KEY (tag_id) REFERENCES tags(id),
                PRIMARY KEY (news_id, tag_id)
            )
        ''')
        
        # 创建索引(提升查询性能)
        self.cursor.execute('''
            CREATE INDEX IF NOT EXISTS idx_news_url 
            ON news(url)
        ''')
        
        self.cursor.execute('''
            CREATE INDEX IF NOT EXISTS idx_news_publish_time 
            ON news(publish_time)
        ''')
        
        self.conn.commit()
        print(f"✅ 数据库已初始化: {self.db_file}")
    
    def insert_news(self, news_data):
        """
        插入新闻数据
        
        Args:
            news_data: 新闻字典
            
        Returns:
            int: 插入的记录 ID,失败返回 None
        """
        try:
            self.cursor.execute('''
                INSERT INTO news (url, title, author, publish_time, 
                                 content, views, crawl_time)
                VALUES (?, ?, ?, ?, ?, ?, ?)
            ''', (
                news_data.get('url'),
                news_data.get('title'),
                news_data.get('author'),
                news_data.get('publish_time'),
                news_data.get('content'),
                news_data.get('views', 0),
                news_data.get('crawl_time', datetime.now().isoformat())
            ))
            
            self.conn.commit()
            return self.cursor.lastrowid
            
        except sqlite3.IntegrityError as e:
            print(f"⚠️  重复 URL,跳过: {news_data.get('url')}")
            return None
        except Exception as e:
            print(f"❌ 插入失败: {e}")
            return None
    
    def batch_insert(self, news_list):
        """
        批量插入(性能更好)
        
        Args:
            news_list: 新闻列表
            
        Returns:
            dict: 统计信息
        """
        success = 0
        failed = 0
        duplicates = 0
        
        for news in news_list:
            news_id = self.insert_news(news)
            if news_id:
                success += 1
                
                # 处理标签
                if 'tags' in news and news['tags']:
                    self._insert_tags(news_id, news['tags'])
            elif news_id is None:
                duplicates += 1
            else:
                failed += 1
        
        return {
            'success': success,
            'duplicates': duplicates,
            'failed': failed
        }
    
    def _insert_tags(self, news_id, tags):
        """插入标签(多对多关系)"""
        for tag_name in tags:
            # 插入标签(如果不存在)
            self.cursor.execute('''
                INSERT OR IGNORE INTO tags (name) VALUES (?)
            ''', (tag_name,))
            
            # 获取标签 ID
            self.cursor.execute('''
                SELECT id FROM tags WHERE name = ?
            ''', (tag_name,))
            tag_id = self.cursor.fetchone()[0]
            
            # 建立关联
            self.cursor.execute('''
                INSERT OR IGNORE INTO news_tags (news_id, tag_id)
                VALUES (?, ?)
            ''', (news_id, tag_id))
        
        self.conn.commit()
    
    def query_news(self, limit=10, offset=0):
        """查询新闻列表"""
        self.cursor.execute('''
            SELECT id, url, title, author, publish_time, views
            FROM news
            ORDER BY id DESC
            LIMIT ? OFFSET ?
        ''', (limit, offset))
        
        columns = [desc[0] for desc in self.cursor.description]
        rows = self.cursor.fetchall()
        
        return [dict(zip(columns, row)) for row in rows]
    
    def search_by_keyword(self, keyword):
        """按关键词搜索"""
        self.cursor.execute('''
            SELECT id, url, title, author, publish_time
            FROM news
            WHERE title LIKE ? OR content LIKE ?
            ORDER BY publish_time DESC
        ''', (f'%{keyword}%', f'%{keyword}%'))
        
        columns = [desc[0] for desc in self.cursor.description]
        return [dict(zip(columns, row)) for row in self.cursor.fetchall()]
    
    def get_stats(self):
        """获取统计信息"""
        # 总记录数
        self.cursor.execute('SELECT COUNT(*) FROM news')
        total = self.cursor.fetchone()[0]日新增
        self.cursor.execute('''
            SELECT COUNT(*) FROM news 
            WHERE DATE(created_at) = DATE('now')
        ''')
        today = self.cursor.fetchone()[0]
        
        # 热门标签
        self.cursor.execute('''
            SELECT t.name, COUNT(*) as count
            FROM tags t
            JOIN news_tags nt ON t.id = nt.tag_id
            GROUP BY t.name
            ORDER BY count DESC
            LIMIT 5
        ''')
        top_tags = self.cursor.fetchall()
        
        return {
            'total_news': total,
            'today_news': today,
            'top_tags': top_tags
        }
    
    def close(self):
        """关闭数据库连接"""
        self.conn.close()
        print("🔒 数据库已关闭")


# ========== 使用示例 ==========

if __name__ == "__main__":
    # 创建数据库
    db = NewsDatabase('news.db')
    
    # 插入数据
    news_data = [
        {
            'url': 'https://example.com/news/1',
            'title': '重大科技突破',
            'author': '张三',
            'publish_time': '2025-01-21 10:00:00',
            'content': '正文内容...',
            'views': 1000,
            'tags': ['科技', '创新']
        },
        {
            'url': 'https://example.com/news/2',
            'title': '经济政策解读',
            'author': '李四',
            'publish_time': '2025-01-21 09:00:00',
            'content': '正文内容...',
            'views': 800,
            'tags': ['财经', '政策']
        }
    ]
    
    # 批量插入
    stats = db.batch_insert(news_data)
    print(f"\n📊 插入统计: {stats}")
    
    # 查询数据
    print("\n📋 最新新闻:")
    news_list = db.query_news(limit=5)
    for news in news_list:
        print(f"  {news['title']} - {news['author']}")
    
    # 关键词搜索
    print("\n🔍 搜索'科技':")
    results = db.search_by_keyword('科技')
    for item in results:
        print(f"  {item['title']}")
    
    # 统计信息
    print("\n📊 数据库统计:")
    stats = db.get_stats()
    print(f"  总新闻数: {stats['total_news']}")
    print(f"  今日新增: {stats['today_news']}")
    print(f"  热门标签: {stats['top_tags']}")
    
    # 关闭
    db.close()
4.3 使用 pandas 操作 SQLite
python 复制代码
import pandas as pd
import sqlite3

def save_to_sqlite_pandas(data_list, db_file, table_name='news'):
    """
    使用 pandas 保存到 SQLite(更简单)
    
    Args:
        data_list: 数据列表
        db_file: 数据库文件
        table_name: 表名
    """
    df = pd.DataFrame(data_list)
    
    conn = sqlite3.connect(db_file)
    
    # if_exists='append': 追加数据
    # if_exists='replace': 替换表
    df.to_sql(table_name, conn, if_exists='append', index=False)
    
    conn.close()
    print(f"✅ 已保存 {len(df)} 条记录到 {db_file}")

def read_from_sqlite_pandas(db_file, query):
    """从 SQLite 读取数据到 DataFrame"""
    conn = sqlite3.connect(db_file)
    df = pd.read_sql_query(query, conn)
    conn.close()
    return df

# 使用
save_to_sqlite_pandas(data, 'news_pandas.db')

# 查询
df = read_from_sqlite_pandas('news_pandas.db', 'SELECT * FROM news LIMIT 10')
print(df.head())

五、存储方案选择指南

5.1 根据数据量选择
python 复制代码
def recommend_storage(record_count, has_nested=False, need_query=False):
    """
    推荐存储方案
    
    Args:
        record_count: 预计记录数
        has_nested: 是否有嵌套结构
        need_query: 是否需要查询功能
        
    Returns:
        str: 推荐方案
    """
    if record_count < 1000:
        if has_nested:
            return "JSON(易读,便于检查)"
        else:
            return "CSV(可用 Excel 打开)"
    
    elif record_count < 100000:
        if need_query:
            return "SQLite(支持查询)"
        elif has_nested:
            return "JSON Lines(增量友好)"
        else:
            return "CSV(pandas 处理快)"
    
    else:  # > 100000
        if need_query:
            return "SQLite(必须用数据库)"
        else:
            return "JSON Lines + 压缩(节省空间)"

# 测试
print(recommend_storage(500, has_nested=True, need_query=False))
# "JSON(易读,便于检查)"

print(recommend_storage(50000, has_nested=False, need_query=True))
# "SQLite(支持查询)"
5.2 多格式保存(保险策略)
python 复制代码
class MultiFormatSaver:
    """多格式保存器(同时保存多种格式)"""
    
    def __init__(self, base_name='data'):
        """
        初始化
        
        Args:
            base_name: 文件名前缀
        """
        self.base_name = base_name
        self.timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    
    def save_all(self, data_list):
        """保存为所有格式"""
        if not data_list:
            print("⚠️  数据为空")
            return
        
        filename_base = f"{self.base_name}_{self.timestamp}"
        
        # 1. CSV(用于 Excel 分析)
        csv_file = f"{filename_base}.csv"
        df = pd.DataFrame(data_list)
        df.to_csv(csv_file, index=False, encoding='utf-8-sig')
        print(f"✅ CSV: {csv_file}")
        
        # 2. JSON(完整格式,易读)
        json_file = f"{filename_base}.json"
        with open(json_file, 'w', encoding='utf-8') as f:
            json.dump(data_list, f, ensure_ascii=False, indent=2)
        print(f"✅ JSON: {json_file}")
        
        # 3. JSON Lines(增量友好)
        jsonl_file = f"{filename_base}.jsonl"
        with open(jsonl_file, 'w', encoding='utf-8') as f:
            for item in data_list:
                f.write(json.dumps(item, ensure_ascii=False) + '\n')
        print(f"✅ JSONL: {jsonl_file}")
        
        # 4. SQLite(可查询)
        db_file = f"{self.base_name}.db"
        conn = sqlite3.connect(db_file)
        df.to_sql('news', conn, if_exists='append', index=False)
        conn.close()
        print(f"✅ SQLite: {db_file}")
        
        print(f"\n📦 共保存 {len(data_list)} 条记录(4 base_name='news')
saver.save_all(data)

六、性能对比测试

python 复制代码
import time

def benchmark_storage(data_list, iterations=3):
    """
    对比不同存储方案的性能
    
    Args:
        data_list: 测试数据
        iterations: 测试次数
    """
    results = {}
    
    # 测试 CSV
    times = []
    for _ in range(iterations):
        start = time.time()
        df = pd.DataFrame(data_list)
        df.to_csv('benchmark.csv', index=False)
        times.append(time.time() - start)
    results['CSV (pandas)'] = sum(times) / iterations
    
    # 测试 JSON
    times = []
    for _ in range(iterations):
        start = time.time()
        with open('benchmark.json', 'w', encoding='utf-8') as f:
            json.dump(data_list, f)
        times.append(time.time() - start)
    results['JSON'] = sum(times) / iterations
    
    # 测试 JSON Lines
    times = []
    for _ in range(iterations):
        start = time.time()
        with open('benchmark.jsonl', 'w', encoding='utf-8') as f:
            for item in data_list:
                f.write(json.dumps(item) + '\n')
        times.append(time.time() - start)
    results['JSON Lines'] = sum(times) / iterations
    
    # 测试 SQLite
    times = []
    for _ in range(iterations):
        start = time.time()
        conn = sqlite3.connect(':memory:')  # 内存数据库
        df.to_sql('test', conn, if_exists='replace', index=False)
        conn.close()
        times.append(time.time() - start)
    results['SQLite'] = sum(times) / iterations
    
    # 打印结果
    print(f"\n⏱️  性能测试({len(data_list)} 条记录,平均 {iterations} 次)")
    print("=" * 50)
    
    sorted_results = sorted(results.items(), key=lambda x: x[1])
    for method, avg_time in sorted_results:
        print(f"{method:20s}: {avg_time:.4f}s")
    
    # 清理测试文件
    import os
    for f in ['benchmark.csv', 'benchmark.json', 'benchmark.jsonl']:
        if os.path.exists(f):
            os.remove(f)

# 生成测试数据
test_data = [
    {
        'id': i,
        'title': f'新闻标题{i}',
        'content': '这是正文内容...' * 10,
        'tags': ['tag1', 'tag2']
    }
    for i in range(1000)
]

benchmark_storage(test_data)

七、最佳实践建议

7.1 存储策略组合
python 复制代码
class SmartStorage:
    """智能存储策略"""
    
    def __init__(self, project_name='spider'):
        self.project_name = project_name
        
        # 原始数据(JSON Lines,便于追加)
        self.raw_file = f"{project_name}_raw.jsonl"
        
        # 清洗后数据(SQLite,便于查询)
        self.db = NewsDatabase(f"{project_name}_clean.db")
        
        # 导出数据(CSV,便于分析)
        self.export_dir = f"export/{project_name}"
        os.makedirs(self.export_dir, exist_ok=True)
    
    def save_raw(self, data_list):
        """保存原始数据(不经处理)"""
        with open(self.raw_file, 'a', encoding='utf-8') as f:
            for item in data_list:
                f.write(json.dumps(item, ensure_ascii=False) + '\n')
        print(f"✅ 原始数据已保存: {self.raw_file}")
    
    def save_clean(self, data_list):
        """保存清洗后的数据"""
        stats = self.db.batch_insert(data_list)
        print(f"✅ 清洗数据已入库: {stats}")
    
    def export_csv(self, query=None):
        """导出 CSV 用于分析"""
        if query is None:
            query = "SELECT * FROM news"
        
        conn = sqlite3.connect(self.db.db_file)
        df = pd.read_sql_query(query, conn)
        conn.close()
        
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        csv_file = f"{self.export_dir}/export_{timestamp}.csv"
        df.to_csv(csv_file, index=False, encoding='utf-8-sig')
        
        print(f"✅ 已导出 CSV: {csv_file}")
        return csv_file
7.2 数据备份策略
python 复制代码
import shutil

def backup_data(source_dir, backup_dir):
    """
    备份数据文件
    
    建议:每天定时备份
    """
    timestamp = datetime.now().strftime('%Y%m%d')
    backup_path = f"{backup_dir}/backup_{timestamp}"
    
    if not os.path.exists(backup_path):
        shutil.copytree(source_dir, backup_path)
        print(f"✅ 已备份到: {backup_path}")
    else:
        print(f"⏭️  今日已备份")

八、本节小结

本节我们学习了三种数据存储方案:

CSV 存储 :简单直观,适合表格数据和 Excel 分析

JSON 存储 :灵活通用,支持嵌套结构和增量追加

SQLite 存储 :结构化查询,适合中大型数据和复杂查询

性能对比 :了解不同方案的速度差异

组合策略:原始+清洗+导出的三层存储架构

核心原则

  • 原始数据保留:用 JSON Lines 保存未处理的原始数据
  • 清洗数据入库:用 SQLite 存储结构化的干净数据
  • 按需导出:用 CSV 导出用于分析的数据子集
  • 定期备份:重要数据要有备份机制

九、课后作业(必做,验收进入第四章)

任务1:对比三种存储方式

使用你之前采集的新闻数据(至少 50 条),完成:

  1. 分别保存为 CSV、JSON、SQLite 三种格式
  2. 对比文件大小
  3. 测试读取速度
  4. 生成对比报告
任务2:设计数据库表结构

为一个电商商品采集项目设计 SQLite 表结构:

  1. 商品表(id、名称、价格、库存...)
  2. 分类表(一对多关系)
  3. 评论表(一对多关系)
  4. 编写建表 SQL 和插入示例
任务3:实现智能存储

为你的采集器添加 SmartStorage 类:

  1. 原始数据保存为 JSON Lines
  2. 清洗后数据存入 SQLite
  3. 提供 CSV 导出功能
  4. 实现数据统计和查询接口

验收方式:在留言区提交:式的对比报告(含文件大小、速度截图)

  • 电商数据库设计文档(ER 图或建表 SQL)
  • SmartStorage 实现代码和测试结果
  • 学习心得

🔮 下期预告

恭喜完成第三章!🎉 下一章《动态网站爬取:Selenium 基础》,我们将进入新的领域:

  • 什么是动态网站?为什么 requests 抓不到数据?
  • Selenium 的安装和基本使用
  • 定位元素的八种方法
  • 等待策略:隐式等待 vs 显式等待
  • 执行 JavaScript 脚本

预习建议

安装 Selenium 和 Chrome 浏览器驱动(ChromeDriver)。了解什么是 JavaScript 渲染的网页。


💬 选对存储方案,事半功倍!数据是爬虫的成果! 📦✨

记住:原始数据是最宝贵的资产。即使清洗逻辑改变,有了原始数据就能重新处理。工程师要有"数据第一"的意识!

🌟 文末

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

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

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

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

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

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

✅ 互动征集

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

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


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

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

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


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

相关推荐
归去来?3 小时前
记录一次从https接口提取25G大文件csv并落表的经历
大数据·数据仓库·hive·python·网络协议·5g·https
喵手3 小时前
Python爬虫零基础入门【第三章:Requests 静态爬取入门·第1节】你的第一个爬虫:抓取页面并保存 HTML!
爬虫·python·爬虫实战·python爬虫工程化实战·requests静态爬取·抓取网页并保存html·零基础入门python爬虫
喵手3 小时前
Python爬虫零基础入门【第三章:Requests 静态爬取入门·第2节】伪装与会话:Headers、Session、Cookie(合规使用)!
爬虫·python·python爬虫实战·python爬虫工程化实战·requests静态爬取·伪装与会话·零基础python爬虫入门
小白学大数据3 小时前
绕过拼多多 App 反抓包机制的综合逆向解决方案
开发语言·爬虫·python·自动化
使者大牙3 小时前
【单点知识】 Python装饰器介绍
开发语言·数据库·python
Jackson@ML3 小时前
2026最新版Sublime Text 4安装使用指南
java·python·编辑器·sublime text
TonyLee0173 小时前
半监督学习介绍
人工智能·python·深度学习·机器学习
kong79069283 小时前
Python核心语法-Python自定义模块、Python包
开发语言·python·python核心语法
OLOLOadsd1234 小时前
基于Mask-RCNN和RegNetX的茎蛀虫检测识别系统详解
python