Python爬虫实战:从入门到精通的七个真实场景演练
如果你刚开始接触Python爬虫,可能会觉得这个概念既神秘又复杂。我刚开始学爬虫的时候,总以为需要掌握什么高深的黑科技,后来才发现,爬虫本质上就是模拟人类浏览网页的行为,只不过用代码来实现而已。真正让我开窍的,不是那些抽象的理论,而是动手去爬几个真实的网站,遇到问题、解决问题,这个过程比看十本教材都管用。
今天我想分享的,不是教科书式的知识点罗列,而是七个我亲自实践过的真实案例。这些案例覆盖了从最简单的静态页面到复杂的动态加载,从单线程到多线程优化,从数据抓取到基础分析的全流程。每个案例我都会详细拆解思路,提供完整的代码,更重要的是,我会告诉你我在实际操作中踩过的坑和总结的经验。无论你是想爬取论坛讨论、收集商品评价,还是分析电影数据,这里都有可以直接上手的方案。
1. 静态页面抓取:从论坛帖子开始
很多人学爬虫的第一个障碍是不知道从哪里下手。我的建议是,从结构相对简单、数据完全在HTML源码中的静态页面开始。论坛帖子就是个绝佳的起点——页面结构清晰,数据量大,而且通常没有复杂的反爬机制。
1.1 理解网页结构与数据定位
打开任何一个论坛帖子页面,按F12进入开发者工具,你会看到密密麻麻的HTML代码。新手看到这个可能会头晕,但其实只需要关注几个关键点:
- 用户信息:通常包含在特定的
class或id属性中 - 评论内容:一般包裹在
<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'

2577

被折叠的 条评论
为什么被折叠?



