Python爬虫实战:从零构建豆瓣图书Top250采集系统!

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

㊙️本期爬虫难度指数:⭐⭐⭐

🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:

🌟 开篇语

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

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

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

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

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

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

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。

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

摘要(Abstract)

本文将带你从零搭建一套完整的豆瓣图书Top250数据采集系统,使用 requests + lxml + pandas 技术栈,最终产出结构化的CSV文件和SQLite数据库。

读完本文你将获得:

  • 掌握静态网页爬虫的完整工程化流程(从请求到存储)
  • 学会构建可复用的爬虫架构(分层设计、容错处理)
  • 了解数据质量验收的最佳实践(抽样检查、可视化报告)

1️⃣ 背景与需求(Why)

为什么要爬豆瓣图书Top250?

作为一名数据分析爱好者,我经常需要获取高质量的图书数据来做推荐系统训练、阅读趋势分析。虽然豆瓣提供了Web界面,但手动复制250本书的信息显然不现实。通过爬虫自动化采集,我可以:

  1. 一键获取结构化数据:书名、作者、评分、评价人数、出版信息等
  2. 定期更新监控:榜单排名变化追踪
  3. 多维度分析:按出版年份、出版社、评分分布做可视化

目标字段清单

字段名 说明 示例值
rank 排名 1
title 书名 活着
author 作者 余华
publisher 出版社 作家出版社
publish_date 出版日期 2012-8-1
price 定价 20.00元
rating 评分 9.4
rating_count 评价人数 123456
url 详情页链接 https://book.douban.com/subject/...

2️⃣ 合规与注意事项(必读)

robots.txt规范

豆瓣的 robots.txthttps://www.douban.com/robots.txt)允许爬取图书页面,但明确禁止:

  • 高频并发请求(建议间隔≥1秒)
  • 自动化登录行为
  • 爬取用户隐私数据

我们的合规策略

✅ **遵守的无需登录)

  • 请求间隔设置为1-3秒随机
  • 添加合理的User-Agent标识
  • 不采集用户评论等UGC内容
  • 仅用于个人学习,不用于商业用途

⚠️ 注意:本文代码仅供技术学习,实际使用需遵守目标网站服务条款。

3️⃣ 技术选型与整体流程(What/How)

为什么选择静态爬虫方案?

豆瓣图书Top250页面属于服务端渲染(SSR),HTML源码中直接包含数据,无需执行JavaScript。因此选择轻量级方案:

  • requests:发送HTTP请求
  • lxml:高性能的XPath解析器
  • pandas:数据清洗与导出

相比Scrapy(太重)和Selenium(太慢),这套组合是最优解。

整体流程图

json 复制代码
用户启动 → 请求列表页 → 解析书籍链接 → 遍历详情页
    ↓           ↓            ↓              ↓
  日志记录   失败重试    XPath提取      字段清洗
    ↓           ↓            ↓              ↓
 存储CSV   存储SQLite   去重处理    生成验收报告

4️⃣ 环境准备与依赖安装

Python版本

  • 推荐:Python 3.8+(本文基于3.10测试)

依赖安装

bash 复制代码
pip install requests lxml pandas openpyxl matplotlib jinja2

说明

  • openpyxl:支持pandas导出Excel(可选)
  • matplotlib:生成验收报告图表
  • jinja2:HTML模板渲染

项目结构

json 复制代码
douban_book_crawler/
├── main.py              # 主入口
├── fetcher.py           # 请求层
├── parser.py            # 解析层
├── storage.py           # 存储层
├── validator.py         # 验收层
├── config.py            # 配置文件
├── requirements.txt     # 依赖清单
├── data/                # 数据目录
│   ├── books.csv
│   ├── books.db
│   └── validation_report.html
└── logs/                # 日志目录
    └── crawler.log
```请求层(Fetcher)

### config.py - 配置中心

```python
import random

class Config:
    """爬虫配置中心"""
    
    # 请求头池(轮换使用)
    USER_AGENTS = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
    ]
    
    # 请求参数
    TIMEOUT = 10  # 超时时间(秒)
    RETRY_TIMES = 3  # 失败重试次数
    RETRY_DELAY = (2, 5)  # 重试延迟范围(秒)
    REQUEST_DELAY = (1, 3)  # 请求间隔范围(秒)
    
    # 目标URL
    BASE_URL = 'https://book.douban.com/top250'
    
    @staticmethod
    def get_headers():
        """随机获取请求头"""
        return {
            'User-Agent': random.choice(Config.USER_AGENTS),
            'Referer': 'https://book.douban.com/',
            'Accept': 'text/html,application/xhtml+xml',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Connection': 'keep-alive'
        }

fetcher.py - 请求引擎

python 复制代码
import requests
import time
import random
import logging
from config import Config

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('logs/crawler.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)

class Fetcher:
    """HTTP请求封装类"""
    
    def __init__(self):
        self.session = requests.Session()
        self.logger = logging.getLogger(__name__)
    
    def fetch(self, url, params=None):
        """
        发送GET请求(带重试机制)
        
        Args:
            url: 目标URL
            params: 查询参数
            
        Returns:
            Response对象或None
        """
        for attempt in range(Config.RETRY_TIMES):
            try:
                # 随机延迟(避免被封)
                if attempt > 0:
                    delay = random.uniform(*Config.RETRY_DELAY)
                    self.logger.warning(f"第{attempt+1}次重试,等待{delay:.1f}秒...")
                    time.sleep(delay)
                
                response = self.session.get(
                    url,
                    params=params,
                    headers=Config.get_headers(),
                    timeout=Config.TIMEOUT
                )
                
                # 状态码检查
                if response.status_code == 200:
                    self.logger.info(f"✅ 成功获取: {url}")
                    return response
                elif response.status_code == 403:
                    self.logger.error(f"❌ 403禁止访问,可能触发反爬")
                    time.sleep(10)  # 冷却10秒
                elif response.status_code == 429:
                    self.logger.error(f"❌ 429请求过快,强制等待30秒")
                    time.sleep(30)
                else:
                    self.logger.warning(f"⚠️ 状态码{response.status_code}: {url}")
                    
            except requests.Timeout:
                self.logger.error(f"⏱️ 请求超时: {url}")
            except requests.RequestException as e:
                self.logger.error(f"❌ 请求异常: {e}")
        
        self.logger.error(f"💀 {Config.RETRY_TIMES}次尝试后仍失败: {url}")
        return None
    
    def close(self):
        """关闭会话"""
        self.session.close()

设计亮点

  1. 指数退避重试:失败后延迟时间逐渐增加
  2. 状态码分类处理:403/429触发特殊逻辑
  3. Session复用:保持连接池,提升性能
  4. 双重日志:同时输出到文件和控制台

6️⃣ 核心实现:解析层(Parser)

parser.py - 数据提取引擎

python 复制代码
from lxml import etree
import re
import logging

class Parser:
    """HTML解析器"""
    
    def __init__(self):
        self.logger = logging.getLogger(__name__)
    
    def parse_list_page(self, html):
        """
        解析列表页,提取所有书籍链接
        
        Args:
            html: 页面HTML文本
            
        Returns:
            书籍URL列表
        """
        tree = etree.HTML(html)
        # XPath: 定位到每个书籍条目的链接
        links = tree.xpath('//tr[@class="item"]//div[@class="pl2"]/a/@href')
        
        self.logger.info(f"📚 解析到{len(links)}本书籍")
        return links
    
    def parse_detail_page(self, html, url):
        """
        解析详情页,提取书籍字段
        
        Args:
            html: 详情页HTML
            url: 当前页面URL
            
        Returns:
            字典形式的书籍数据
        """
        tree = etree.HTML(html)
        
        try:
            # 书名(主标题)
            title = tree.xpath('//span[@property="v:itemreviewed"]/text()')
            title = title[0].strip() if title else ''
            
            # 评分
            rating = tree.xpath('//strong[@property="v:average"]/text()')
            rating = float(rating[0]) if rating else 0.0
            
            # 评价人数
            rating_count = tree.xpath('//span[@property="v:votes"]/text()')
            rating_count = int(rating_count[0]) if rating_count else 0
            
            # 出版信息(作者、出版社、出版日期、定价等)
            info_text = tree.xpath('//div[@id="info"]//text()')
            info_dict = self._parse_info_block(info_text)
            
            return {
                'title': title,
                'author': info_dict.get('author', ''),
                'publisher': info_dict.get('publisher', ''),
                'publish_date': info_dict.get('publish_date', ''),
                'price': info_dict.get('price', ''),
                'rating': rating,
                'rating_count': rating_count,
                'url': url
            }
            
        except Exception as e:
            self.logger.error(f"⚠️ 解析失败 {url}: {e}")
            return None
    
    def _parse_info_block(self, text_list):
        """
        解析出版信息块(容错处理)
        
        格式示例:
        作者: 余华
        出版社: 作家出版社
        出版年: 2012-8-1
        定价: 20.00元
        """
        info = {}
        current_key = None
        
        for text in text_list:
            text = text.strip()
            if not text or text == ':':
                continue
            
            # 识别键值对
            if '作者' in text:
                current_key = 'author'
            elif '出版社' in text:
                current_key = 'publisher'
            elif '出版年' in text or '出
                current_key = 'publish_date'
            elif '定价' in text:
                current_key = 'price'
            elif current_key:
                # 清洗数据(去除多余空白)
                info[current_key] = re.sub(r'\s+', ' ', text)
                current_key = None
        
        return info

解析策略

  1. XPath优先:比CSS选择器快3-5倍
  2. 容错设计:每个字段都有默认值
  3. 正则清洗:去除换行符、多余空格
  4. 结构化输出:统一返回字典格式

7️⃣ 数据存储与导出(Storage)

storage.py - 持久化层

python 复制代码
import pandas as pd
import sqlite3
import os
import logging

class Storage:
    """数据存储管理器"""
    
    def __init__(self, csv_path='data/books.csv', db_path='data/books.db'):
        self.csv_path = csv_path
        self.db_path = db_path
        self.logger = logging.getLogger(__name__)
        
        # 确保目录存在
        os.makedirs('data', exist_ok=True)
        
        # 初始化数据库
        self._init_database()
    
    def _init_database(self):
        """创建SQLite表结构"""
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS books (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                author TEXT,
                publisher TEXT,
                publish_date TEXT,
                price TEXT,
                rating REAL,
                rating_count INTEGER,
                url TEXT UNIQUE,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        
        # 创建索引(加速去重查询)
        cursor.execute('''
            CREATE INDEX IF NOT EXISTS idx_url ON books(url)
        ''')
        
        conn.commit()
        conn.close()
        self.logger.info("✅ 数据库初始化完成")
    
    def save_to_csv(self, data_list):
        """
        批量保存到CSV(追加模式)
        
        Args:
            data_list: 书籍数据列表
        """
        if not data_list:
            return
        
        df = pd.DataFrame(data_list)
        
        # 检查文件是否存在
        if os.path.exists(self.csv_path):
            df.to_csv(self.csv_path, mode='a', header=False, index=False, encoding='utf-8-sig')
        else:
            df.to_csv(self.csv_path, index=False, encoding='utf-8-sig')
        
        self.logger.info(f"💾 已保存{len(data_list)}条数据到CSV")
    
    def save_to_db(self, data_list):
        """
        批量保存到SQLite(自动去重)
        
        Args:
            data_list: 书籍数据列表
        """
        if not data_list:
            return
        
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()
        
        success_count = 0
        for data in data_list:
            try:
                cursor.execute('''
                    INSERT OR IGNORE INTO books 
                    (title, author, publisher, publish_date, price, rating, rating_count, url)
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                ''', (
                    data['title'],
                    data['author'],
                    data['publisher'],
                    data['publish_date'],
                    data['price'],
                    data['rating'],
                    data['rating_count'],
                    data['url']
                ))
                
                if cursor.rowcount > 0:
                    success_count += 1
                    
            except sqlite3.IntegrityError:
                self.logger.warning(f"⚠️ 重复数据: {data['url']}")
        
        conn.commit()
        conn.close()
        self.logger.info(f"💾 已保存{success_count}条数据到数据库(去重后)")
    
    def get_all_data(self):
        """从数据库读取所有数据"""
        conn = sqlite3.connect(self.db_path)
        df = pd.read_sql_query('SELECT * FROM books', conn)
        conn.close()
        return df

字段映射表

数据库字段 类型 约束 示例值
id INTEGER 自增主键 1
title TEXT NOT NULL 活着
author TEXT - 余华
publisher TEXT - 作家出版社
publish_date TEXT - 2012-8-1
price TEXT - 20.00元
rating REAL - 9.4
rating_count INTEGER - 123456
url TEXT UNIQUE https://...
created_at TIMESTAMP 自动填充 2025-02-09 10:30:00

8️⃣ 主流程编排(Orchestrator)

main.py - 主入口

python 复制代码
import time
import random
from fetcher import Fetcher
from parser import Parser
from storage import Storage
from config import Config
import logging

def main():
    """主流程控制器"""
    logger = logging.getLogger(__name__)
    logger.info("🚀 爬虫启动...")
    
    # 初始化组件
    fetcher = Fetcher()
    parser = Parser()
    storage = Storage()
    
    all_books = []
    
    try:
        # 遍历10页(每页25本)
        for page in range(10):
            logger.info(f"\n{'='*50}\n📖 正在爬取第{page+1}/10页\n{'='*50}")
            
            # 请求列表页
            params = {'start': page * 25}
            response = fetcher.fetch(Config.BASE_URL, params)
            
            if not response:
                logger.error(f"跳过第{page+1}页")
                continue
            
            # 解析书籍链接
            book_urls = parser.parse_list_page(response.text)
            
            # 遍历每本书的详情页
            for idx, url in enumerate(book_urls, 1):
                logger.info(f"  [{本书的详情页
            for idx, url in enumerate(book_urls, 1):
                logger.info(f"  [{idx}/{len(book_urls)}] 正在爬取: {url}")
                
                # 请求详情页
                detail_response = fetcher.fetch(url)
                if not detail_response:
                    continue
                
                # 解析数据
                book_data = parser.parse_detail_page(detail_response.text, url)
                if book_data:
                    book_data['rank'] = page * 25 + idx  # 添加排名
                    all_books.append(book_data)
                    logger.info(f"  ✅ 《{book_data['title']}》 评分:{book_data['rating']}")
                
                # 礼貌延迟
                time.sleep(random.uniform(*Config.REQUEST_DELAY))
            
            # 每页保存一次(增量备份)
            storage.save_to_csv(all_books[-len(book_urls):])
            storage.save_to_db(all_books[-len(book_urls):])
        
        logger.info(f"\n🎉 爬取完成!共获取{len(all_books)}本书籍数据")
        
    except KeyboardInterrupt:
        logger.warning("\n⚠️ 用户中断,正在保存已爬取数据...")
        storage.save_to_csv(all_books)
        storage.save_to_db(all_books)
    
    finally:
        fetcher.close()

if __name__ == '__main__':
    main()

9️⃣ 运行方式与结果展示

启动命令

json 复制代码
# 1. 创建必要目录
mkdir -p data logs

# 2. 运行爬虫
python main.py

控制台输出示例

json 复制代码
2025-02-09 14:23:10 - INFO - 🚀 爬虫启动...
==================================================
📖 正在爬取第1/10页
==================================================
2025-02-09 14:23:11 - INFO - ✅ 成功获取: https://book.douban.com/top250
2025-02-09 14:23:11 - INFO - 📚 解析到25本书籍
  [1/25] 正在爬取: https://book.douban.com/subject/1770782/
2025-02-09 14:23:13 - INFO - ✅ 成功获取: https://book.douban.com/subject/1770782/
2025-02-09 14:23:13 - INFO -   ✅ 《活着》 评分:9.4

数据文件示例

books.csv(前3行)

rank title author publisher publish_date price rating rating_count url
1 活着 余华 作家出版社 2012-8-1 20.00元 9.4 456789 https://...
2 三体 刘慈欣 重庆出版社 2008-1 23.00元 9.3 678901 https://...
3 百年孤独 加西亚·马尔克斯 南海出版公司 2011-6 39.50元 9.3 543210 https://...

🔟 抽样可视化验收(Quality Check)

validator.py - 质量检查器

python 复制代码
import pandas as pd
import random
import matplotlib
matplotlib.use('Agg')  # 无GUI环境
import matplotlib.pyplot as plt
from jinja2 import Template
import logging

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei']
plt.rcParams['axes.unicode_minus'] = False

class Validator:
    """数据质量验收器"""
    
    def __init__(self, db_path='data/books.db'):
        self.db_path = db_path
        self.logger = logging.getLogger(__name__)
    
    def generate_report(self, sample_size=50):
        """
        生成验收报告
        
        Args:
            sample_size: 抽样数量
        """
        # 读取数据
        from storage import Storage
        storage = Storage(db_path=self.db_path)
        df = storage.get_all_data()
        
        if df.empty:
            self.logger.error("❌ 数据库为空,无法生成报告")
            return
        
        # 随机抽样
        sample_df = df.sample(n=min(sample_size, len(df)), random_state=42)
        
        # 统计分析
        stats = {
            'total_count': len(df),
            'sample_count': len(sample_df),
            'missing_rate': self._calculate_missing_rate(df),
            'duplicate_rate': self._calculate_duplicate_rate(df),
            'avg_rating': df['rating'].mean(),
            'rating_distribution': df['rating'].value_counts().to_dict()
        }
        
        # 生成图表
        chart_path = self._generate_charts(df)
        
        # 渲染HTML
        html = self._render_html(sample_df, stats, chart_path)
        
        # 保存报告
        report_path = 'data/validation_report.html'
        with open(report_path, 'w', encoding='utf-8') as f:
            f.write(html)
        
        self.logger.info(f"📊 验收报告已生成: {report_path}")
    
    def _calculate_missing_rate(self, df):
        """计算字段缺失率"""
        missing = {}
        for col in ['title', 'author', 'publisher', 'rating']:
            missing[col] = f"{(df[col].isna().sum() / len(df) * 100):.2f}%"
        return missing
    
    def _calculate_duplicate_rate(self, df):
        """计算URL重复率"""
        dup_count = df['url'].duplicated().sum()
        return f"{(dup_count / len(df) * 100):.2f}%"
    
    def _generate_charts(self, df):
        """生成数据分布图"""
        fig, axes = plt.subplots(1, 2, figsize=(12, 4))
        
        # 评分分布
        df['rating'].plot(kind='hist', bins=20, ax=axes[0], color='skyblue', edgecolor='black')
        axes[0].set_title('Rating Distribution', fontsize=14)
        axes[0].set_xlabel('Rating Score')
        axes[0].set_ylabel('Frequency')
        
        # 字段完整性
        completeness = {
            'Title': (df['title'].notna().sum() / len(df)) * 100,
            'Author': (df['author'].notna().sum() / len(df)) * 100,
            'Publisher': (df['publisher'].notna().sum() / len(df)) * 100,
            'Price': (df['price'].notna().sum() / len(df)) * 100
        }
        axes[1].bar(completeness.keys(), completeness.values(), color='coral')
        axes[1].set_title('Field Completeness Rate', fontsize=14)
        axes[1].set_ylabel('Percentage (%)')
        axes[1].set_ylim(0, 105)
        
        # 保存图表
        chart_path = 'data/charts.png'
        plt.tight_layout()
        plt.savefig(chart_path, dpi=100)
        plt.close()
        
        return chart_path
    
    def _render_html(self, sample_df, stats, chart_path):
        """渲染HTML模板"""
        template = Template('''
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Data Validation Report</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
        .container { max-width: 1200px; margin: auto; background: white; padding: 30px; border-radius: 8px; }
        h1 { color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; }
        .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin: 20px 0; }
        .stat-card { background: #e8f5e9; padding: 15px; border-radius: 5px; text-align: center; }
        .stat-card h3 { margin: 0; color: #2e7d32; }
        .stat-card p { font-size: 24px; font-weight: bold; margin: 10px 0 0 0; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th { background: #4CAF50; color: white; padding: 12px; text-align: left; }
        td { padding: 10px; border-bottom: 1px solid #ddd; }
        tr:hover { background: #f1f1f1; }
        img { max-width: 100%; margin-top: 20px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>📊 Douban Book Crawler - Validation Report</h1>
        
        <div class="stats">
            <div class="stat-card">
                <h3>Total Records</h3>
                <p>{{ stats.total_count }}</p>
            </div>
            <div class="stat-card">
                <h3>Sample Size</h3>
                <p>{{ stats.sample_count }}</p>
            </div>
            <div class="stat-card">
                <h3>Average Rating</h3>
                <p>{{ "%.2f"|format(stats.avg_rating) }}</p>
            </div>
        </div>
        
        <h2>🔍 Data Quality Metrics</h2>
        <ul>
            <li><strong>Duplicate Rate:</strong> {{ stats.duplicate_rate }}</li>
            <li><strong>Missing Rate (Title):</strong> {{ stats.missing_rate.title }}</li>
            <li><strong>Missing Rate (Author):</strong> {{ stats.missing_rate.author }}</li>
        </ul>
        
        <h2>📈 Distribution Charts</h2>
        <img src="{{ chart_path }}" alt="Charts">
        
        <h2>📋 Random Sample ({{ stats.sample_count }} records)</h2>
        {{ sample_table }}
    </div>
</body>
</html>
        ''')
        
        # 转换样本数据为HTML表格
        sample_table = sample_df[['rank', 'title', 'author', 'rating', 'rating_count']].to_html(
            index=False,
            classes='sample0
        )
        
        return template.render(
            stats=stats,
            chart_path=chart_path,
            sample_table=sample_table
        )

if __name__ == '__main__':
    validator = Validator()
    validator.generate_report(sample_size=50)

生成验收报告

bash 复制代码
python validator.py

报告内容包括

  1. 总数据量、抽样数量、平均评分
  2. 重复率、缺失率等质量指标
  3. 评分分布直方图
  4. 字段完整性柱状图
  5. 随机50条数据明细表

1️⃣1️⃣ 常见问题与排错

Q1: 遇到403/429错误怎么办?

原因:请求过快触发反爬机制

解决方案

python 复制代码
# 方法1: 增加延迟
Config.REQUEST_DELAY = (3, 6)  # 改为3-6秒

# 方法2: 使用代理池(仅示例)
proxies = {
    'http': 'http://your-proxy:port',
    'https': 'https://your-proxy:port'
}
response = session.get(url, proxies=proxies)

Q2: 抓到的HTML是空壳/乱码?

可能原因

  1. 网站使用JavaScript动态渲染
  2. 编码问题

诊断方法

python 复制代码
# 检查响应内容
print(response.text[:500])  # 查看前500字符
print(response.encoding)    # 查看编码

解决方案

python 复制代码
# 方案1: 强制指定编码
response.encoding = 'utf-8'

# 方案2: 如果是动态渲染,改用Playwright
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto(url)
    html = page.content()

Q3: XPath选择器失效?

原因:网站改版,HTML结构变化

调试技巧

python 复制代码
# 1. 打印整个HTML,手动检查
with open('debug.html', 'w') as f:
    f.write(response.text)

# 2. 在Chrome开发者工具中测试XPath
# 按F12 → Elements → Ctrl+F → 输入XPath

# 3. 使用更宽松的选择器
# 错误示例(太具体): //div[@class="info v-time"]
# 正确示例(容错性强): //div[contains(@class, "info")]

Q4: 数据库插入失败?

检查步骤

python 复制代码
# 1. 查看日志
tail -f logs/crawler.log

# 2. 手动测试SQL
import sqlite3
conn = sqlite3.connect('data/books.db')
cursor = conn.cursor()
cursor.execute('SELECT * FROM books LIMIT 5')
print(cursor.fetchall())

1️⃣2️⃣ 进阶优化方向

并发爬取(asyncio版本)

python 复制代码
import asyncio
import aiohttp

async def fetch_async(session, url):
    async with session.get(url, headers=Config.get_headers()) as response:
        return await response.text()

async def main_async():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_async(session, url) for url in book_urls]
        results = await asyncio.gather(*tasks)

性能对比

  • 同步版本:250本书 ≈ 15分钟
  • 异步版本:250本书 ≈ 3分钟(5倍提升)

断点续爬

python 复制代码
def load_checkpoint():
    """加载已爬取的URL集合"""
    if os.path.exists('checkpoint.txt'):
        with open('checkpoint.txt', 'r') as f:
            return set(f.read().splitlines())
    return set()

def save_checkpoint(url):
    """保存爬取进度"""
    with open('checkpoint.txt', 'a') as f:
        f.write(url + '\n')

定时任务(每日更新)

bash 复制代码
# Linux crontab(每天凌晨2点执行)
0 2 * * * cd /path/to/project && /usr/bin/python3 main.py >> logs/cron.log 2>&1

1️⃣3️⃣ 总结与展望

我们完成了什么?

✅ 构建了一套工程化的爬虫框架 (分层设计、可复用)

✅ 实现了双存储方案 (CSV + SQLite)

✅ 加入了数据质量验收 (抽样检查 + 可视化报告)

✅ 覆盖了完整的容错处理(重试、延迟、编码)

下一步可以做什么?

  1. 技术升级

    • 使用 Scrapy 框架(内置去重、管道、中间件)
    • 集成 Playwright(应对动态网站)
    • 部署分布式爬虫(Scrapy-Redis)
  2. 功能扩展

    • 爬取图书评论做情感分析
    • 监控榜单变化并推送通知
    • 对接推荐算法构建阅读推荐系统
  3. 工程化改进

    • Docker 容器化部署
    • 接入 Airflow 做调度
    • Prometheus + Grafana 监控

延伸阅读


最后说一句 :爬虫技术本身是中性的,关键在于如何使用。希望本文能帮你掌握数据采集的核心技能,记住三个原则------合法合规、尊重版权、控制频率。祝你在数据世界里探索愉快!

🌟 文末

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

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

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

墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:

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

📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集

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

评论区留言告诉我你的需求,我会优先安排实现(更新)哒~


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

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

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


✅ 免责声明

本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。

使用或者参考本项目即表示您已阅读并同意以下条款:

  • 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
  • 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
  • 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
  • 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
相关推荐
YJlio8 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
l1t8 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
0思必得09 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
山塘小鱼儿9 小时前
本地Ollama+Agent+LangGraph+LangSmith运行
python·langchain·ollama·langgraph·langsimth
码说AI10 小时前
python快速绘制走势图对比曲线
开发语言·python
wait_luky10 小时前
python作业3
开发语言·python
Libraeking11 小时前
爬虫的“法”与“术”:在牢狱边缘疯狂试探?(附高阶环境配置指南)
爬虫
Python大数据分析@11 小时前
tkinter可以做出多复杂的界面?
python·microsoft
大黄说说11 小时前
新手选语言不再纠结:Java、Python、Go、JavaScript 四大热门语言全景对比与学习路线建议
java·python·golang