Python爬虫实战:7个真实案例教你从贴吧到豆瓣(附完整源码)

Python爬虫实战:从入门到精通的七个真实场景演练

如果你刚开始接触Python爬虫,可能会觉得这个概念既神秘又复杂。我刚开始学爬虫的时候,总以为需要掌握什么高深的黑科技,后来才发现,爬虫本质上就是模拟人类浏览网页的行为,只不过用代码来实现而已。真正让我开窍的,不是那些抽象的理论,而是动手去爬几个真实的网站,遇到问题、解决问题,这个过程比看十本教材都管用。

今天我想分享的,不是教科书式的知识点罗列,而是七个我亲自实践过的真实案例。这些案例覆盖了从最简单的静态页面到复杂的动态加载,从单线程到多线程优化,从数据抓取到基础分析的全流程。每个案例我都会详细拆解思路,提供完整的代码,更重要的是,我会告诉你我在实际操作中踩过的坑和总结的经验。无论你是想爬取论坛讨论、收集商品评价,还是分析电影数据,这里都有可以直接上手的方案。

1. 静态页面抓取:从论坛帖子开始

很多人学爬虫的第一个障碍是不知道从哪里下手。我的建议是,从结构相对简单、数据完全在HTML源码中的静态页面开始。论坛帖子就是个绝佳的起点——页面结构清晰,数据量大,而且通常没有复杂的反爬机制。

1.1 理解网页结构与数据定位

打开任何一个论坛帖子页面,按F12进入开发者工具,你会看到密密麻麻的HTML代码。新手看到这个可能会头晕,但其实只需要关注几个关键点:

  • 用户信息:通常包含在特定的classid属性中
  • 评论内容:一般包裹在<div><span>标签里
  • 发布时间:可能以时间戳或格式化字符串的形式存在

以某篮球论坛的热门讨论帖为例,我们需要爬取前5页的所有回复。首先观察URL规律:

https://tieba.baidu.com/p/7882177660?pn=1
https://tieba.baidu.com/p/7882177660?pn=2

很明显,pn参数控制页码。这种规律化的URL让批量爬取变得简单。

1.2 正则表达式的精准匹配

虽然现在更推荐使用XPath或BeautifulSoup,但正则表达式在某些场景下依然高效。特别是当页面结构不太规整,或者你需要提取特定模式的数据时。

import re
import requests
import csv
import time

def extract_page_data(html_content):
    """从HTML中提取用户、时间、评论三要素"""
    
    # 匹配用户名的模式
    user_pattern = r'class="p_author_name[^>]*>(.*?)</a>'
    
    # 匹配评论时间的模式  
    time_pattern = r'楼</span><span[^>]*>(.*?)</span><div'
    
    # 匹配评论内容的模式
    content_pattern = r'style="display:;"[^>]*>(.*?)</div>'
    
    users = re.findall(user_pattern, html_content)
    times = re.findall(time_pattern, html_content)
    contents = re.findall(content_pattern, html_content)
    
    return list(zip(users, times, contents))

这里有个细节需要注意:正则表达式中的.*?使用非贪婪匹配,确保我们只获取最小匹配的内容,避免跨标签抓取。

1.3 数据清洗与存储优化

爬下来的原始数据往往包含HTML标签、空白字符等噪音。我通常会建立一个清洗管道:

def clean_comment_data(raw_data):
    """清洗单条评论数据"""
    cleaned = []
    
    for user, time_str, content in raw_data:
        # 过滤广告或异常数据
        if len(user) > 50 or 'img' in content.lower():
            continue
            
        # 移除HTML标签
        content = re.sub(r'<[^>]+>', '', content)
        
        # 去除多余空白
        content = ' '.join(content.split())
        user = user.strip()
        time_str = time_str.strip()
        
        cleaned.append((user, time_str, content))
    
    return cleaned

存储时,CSV是最方便的选择,但要注意编码问题:

def save_to_csv(data, filename='forum_comments.csv'):
    """将数据保存为CSV文件"""
    with open(filename, 'a', encoding='utf-8-sig', newline='') as f:
        writer = csv.writer(f)
        
        # 如果是新文件,写入表头
        if f.tell() == 0:
            writer.writerow(['用户名', '发布时间', '评论内容'])
        
        writer.writerows(data)
    
    print(f"已保存{len(data)}条数据到{filename}")

注意:使用utf-8-sig编码可以确保Excel等软件正确显示中文,避免乱码问题。

1.4 完整的爬取流程控制

把上面的模块组合起来,加上适当的延迟和错误处理:

def crawl_forum_topic(base_url, total_pages=5):
    """爬取指定主题的多页内容"""
    
    all_comments = []
    
    for page in range(1, total_pages + 1):
        try:
            # 构造当前页URL
            current_url = f"{base_url}?pn={page}"
            
            # 设置合理的请求头
            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            }
            
            # 发送请求
            response = requests.get(current_url, headers=headers, timeout=10)
            response.raise_for_status()  # 检查HTTP错误
            
            # 提取数据
            raw_data = extract_page_data(response.text)
            cleaned_data = clean_comment_data(raw_data)
            all_comments.extend(cleaned_data)
            
            print(f"第{page}页完成,获取{len(cleaned_data)}条评论")
            
            # 礼貌性延迟,避免给服务器造成压力
            time.sleep(2 + random.random())
            
        except requests.RequestException as e:
            print(f"第{page}页请求失败: {e}")
            continue
        except Exception as e:
            print(f"第{page}页处理异常: {e}")
            continue
    
    # 保存所有数据
    if all_comments:
        save_to_csv(all_comments)
        print(f"爬取完成!总计{len(all_comments)}条评论")
    
    return all_comments

这个案例虽然简单,但包含了爬虫的核心要素:请求发送、数据解析、清洗存储。掌握了这个基础,你就能处理大多数静态页面了。

2. 多线程优化:高效爬取小说章节

当需要爬取大量页面时,比如一本小说的所有章节,单线程的效率就显得捉襟见肘了。我曾经用单线程爬一本300章的小说,花了将近一个小时。改用多线程后,时间缩短到了10分钟以内。

2.1 分析小说网站的结构

小说网站通常有清晰的目录结构。以某小说网为例,目录页列出了所有章节的链接和标题:

小说主页: https://www.example.com/book/12345/
章节链接: /book/12345/1.html
          /book/12345/2.html
          /book/12345/3.html

我们需要先获取所有章节的链接,然后并发地爬取每个章节的内容。

2.2 使用XPath提取结构化数据

XPath比正则表达式更适合处理结构化的HTML。安装lxml库后,可以这样提取章节信息:

from lxml import etree
import requests

def get_chapter_links(book_url):
    """获取小说所有章节的链接和标题"""
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }
    
    try:
        response = requests.get(book_url, headers=headers, timeout=15)
        response.encoding = 'utf-8'  # 根据实际情况调整编码
        
        # 解析HTML
        tree = etree.HTML(response.text)
        
        # 提取小说标题
        novel_title = tree.xpath('//div[@id="info"]/h1/text()')[0].strip()
        
        # 提取章节链接和标题
        chapters = []
        chapter_elements = tree.xpath('//div[@class="listmain"]//dd/a')
        
        for element in chapter_elements[:20]:  # 先测试前20章
            chapter_title = element.xpath('./text()')[0]
            chapter_url = element.xpath('./@href')[0]
            
            # 处理相对URL
            if chapter_url.startswith('/'):
                chapter_url = 'https://www.example.com/' + chapter_url
            elif not chapter_url.startswith('http'):
                chapter_url = book_url.rstrip('/') + '/' + chapter_url
            
            chapters.append({
                'title': chapter_title,
                'url': chapter_url
            })
        
        return novel_title, chapters
        
    except Exception as e:
        print(f"获取目录失败: {e}")
        return None, []

2.3 数据库存储设计

对于小说这种结构化数据,使用数据库比CSV更合适。MySQL是个不错的选择:

import pymysql
from contextlib import contextmanager

@contextmanager
def get_db_connection():
    """数据库连接上下文管理器"""
    conn = pymysql.connect(
        host='localhost',
        user='your_username',
        password='your_password',
        database='novel_db',
        charset='utf8mb4',  # 支持更全的字符集
        cursorclass=pymysql.cursors.DictCursor
    )
    
    try:
        yield conn
    finally:
        conn.close()

def init_database():
    """初始化数据库表"""
    with get_db_connection() as conn:
        with conn.cursor() as cursor:
            # 创建小说表
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS novels (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    title VARCHAR(200) NOT NULL,
                    author VARCHAR(100),
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            ''')
            
            # 创建章节表
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS chapters (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    novel_id INT,
                    chapter_title VARCHAR(200) NOT NULL,
                    content TEXT,
                    chapter_order INT,
                    FOREIGN KEY (novel_id) REFERENCES novels(id),
                    INDEX idx_novel_order (novel_id, chapter_order)
                )
            ''')
            
        conn.commit()

2.4 实现多线程爬虫

Python的concurrent.futures模块让多线程编程变得简单:

from concurrent.futures import ThreadPoolExecutor, as_completed
import threading

class NovelCrawler:
    def __init__(self, max_workers=5):
        self.max_workers = max_workers
        self.lock = threading.Lock()  # 线程锁,防止数据竞争
        
    def fetch_chapter_content(self, chapter_info, novel_id):
        """爬取单个章节内容"""
        try:
            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
            }
            
            response = requests.get(
                chapter_info['url'], 
                headers=headers, 
                timeout=10
            )
            response.encoding = 'utf-8'
            
            # 解析章节内容
            tree = etree.HTML(response.text)
            content_elements = tree.xpath('//div[@id="content"]//text()')
            
            if not content_elements:
                # 尝试其他选择器
                content_elements = tree.xpath('//div[contains(@class, "content")]//text()')
            
            # 清理内容
            content = '\n'.join([
                line.strip() for line in content_elements 
                if line.strip() and len(line.strip()) > 1
            ])
            
            # 移除广告文本
            ad_keywords = ['笔趣阁', '最快更新', '最新章节']
            for keyword in ad_keywords:
                content = content.replace(keyword, '')
            
            # 保存到数据库
            with self.lock:
                with get_db_connection() as conn:
                    with conn.cursor() as cursor:
                        cursor.execute('''
                            INSERT INTO chapters 
                            (novel_id, chapter_title, content, chapter_order) 
                            VALUES (%s, %s, %s, %s)
                        ''', (novel_id, chapter_info['title'], content, chapter_info['order']))
                        conn.commit()
            
            print(f"已保存章节: {chapter_info['title']}")
            return True
            
        except Exception as e:
            print(f"爬取章节失败 {chapter_info['title']}: {e}")
            return False
    
    def crawl_novel(self, book_url):
        """主爬取函数"""
        print("开始获取小说目录...")
        novel_title, chapters = get_chapter_links(book_url)
        
        if not chapters:
            print("未找到章节信息")
            return
        
        print(f"找到小说: {novel_title}, 共{len(chapters)}章")
        
        # 保存小说基本信息
        with get_db_connection() as conn:
            with conn.cursor() as cursor:
                cursor.execute(
                    'INSERT INTO novels (title) VALUES (%s)',
                    (novel_title,)
                )
                novel_id = cursor.lastrowid
                conn.commit()
        
        # 为每个章节添加顺序编号
        for i, chapter in enumerate(chapters, 1):
            chapter['order'] = i
        
        # 使用线程池并发爬取
        print("开始并发爬取章节内容...")
        success_count = 0
        
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            # 提交所有任务
            future_to_chapter = {
                executor.submit(
                    self.fetch_chapter_content, 
                    chapter, 
                    novel_id
                ): chapter for chapter in chapters
            }
            
            # 处理完成的任务
            for future in as_completed(future_to_chapter):
                chapter = future_to_chapter[future]
                try:
                    if future.result():
                        success_count += 1
                except Exception as e:
                    print(f"章节 {chapter['title']} 处理异常: {e}")
        
        print(f"爬取完成!成功{success_count}章,失败{len(chapters)-success_count}章")

多线程爬虫的关键是控制并发数,我一般设置为3-5个线程,既能提高效率,又不会对目标网站造成太大压力。

3. 数据解析对比:XPath与BeautifulSoup实战

解析HTML是爬虫的核心技能。XPath和BeautifulSoup是两种最常用的工具,它们各有优劣。我个人的经验是:XPath适合精确提取,BeautifulSoup适合灵活处理。

3.1 电影数据爬取场景分析

以豆瓣电影Top250为例,这个页面有几个特点:

  • 数据完全静态,无需处理JavaScript
  • 结构规整,适合学习数据提取
  • 有分页,适合练习批量爬取
  • 数据字段丰富,包含文本、数字、链接等

我们需要爬取的信息包括:

  • 电影名称
  • 导演信息
  • 电影类型
  • 上映年份
  • 评分
  • 评价人数
  • 电影简介

3.2 XPath版本实现

XPath的语法相对简洁,适合快速定位元素:

import requests
from lxml import etree
import csv
import time
import random

class DoubanMovieCrawler:
    def __init__(self):
        self.base_url = "https://movie.douban.com/top250"
        self.headers = {
            'User-Agent'
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值