之前的文章都是单线程爬取,速度有限。如果爬取几千上万条数据,单线程可能要等几分钟甚至几十分钟。本文以豆瓣 Top250 为例,使用多线程 + 数据可视化,爬完数据后直接生成统计图。
一、多线程爬虫是什么
单线程就像一个人在搬砖,多线程就是多个人同时搬砖。
| 爬取250条数据 | ~10秒 | ~3秒 |
| 原理 | 爬完一页再爬下一页 | 4页同时爬 |
| 代码复杂度 | 简单 | 稍复杂 |
Python 中用 concurrent.futures.ThreadPoolExecutor 实现,使用非常简单。
二、核心实现
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
from lxml import etree
HEADERS = {'User-Agent': 'Mozilla/5.0'}
BASE_URL = "https://movie.douban.com/top250?start={}&filter="
def scrape_page(page):
"""爬取单页(25条)"""
url = BASE_URL.format(page * 25)
resp = requests.get(url, headers=HEADERS, timeout=10)
resp.encoding = "utf-8"
tree = etree.HTML(resp.text)
movies = []
items = tree.xpath('//ol[@class="grid_view"]/li')
for item in items:
rank = item.xpath('.//em/text()')[0]
title = item.xpath('.//span[@class="title"]/text()')[0]
rating = item.xpath('.//span[@class="rating_num"]/text()')[0]
people_text = item.xpath('.//span[contains(text(), "人评价")]/text()')
people = people_text[0].strip() if people_text else "0人评价"
movies.append({
"rank": int(rank),
"title": title,
"rating": float(rating),
"people": people
})
return movies
def crawl_all():
"""多线程爬取全部10页"""
all_movies = []
with ThreadPoolExecutor(max_workers=5) as executor:
# 提交所有任务
futures = {executor.submit(scrape_page, page): page for page in range(10)}
# 收集结果
for future in as_completed(futures):
page = futures[future]
try:
movies = future.result()
all_movies.extend(movies)
print(f"第{page+1}页完成,当前共{len(all_movies)}条")
except Exception as e:
print(f"第{page+1}页失败: {e}")
# 按排名排序
all_movies.sort(key=lambda x: x["rank"])
return all_movies
运行
import time
start = time.time()
movies = crawl_all()
print(f"\\n爬取完成!共{len(movies)}条,耗时{time.time()–start:.1f}秒")
输出效果:
第1页完成,当前共25条
第4页完成,当前共50条
第2页完成,当前共75条
第3页完成,当前共100条
…
爬取完成!共250条,耗时2.8秒
三、数据统计分析
爬完数据后用 pandas 分析一下:
import pandas as pd
df = pd.DataFrame(movies)
# 评分分布统计
print("评分统计:")
print(df["rating"].describe())
# 评分最高的10部
print("\\n评分最高的10部:")
print(df.head(10)[["rank", "title", "rating"]])
# 评分区间分布
print("\\n评分区间分布:")
for i in range(4, 10):
count = len(df[(df["rating"] >= i) & (df["rating"] < i+1)])
print(f" {i}~{i+1}分: {count}部")
四、数据可视化(matplotlib)
将统计结果生成图表,直观展示数据分布:
import matplotlib.pyplot as plt
import matplotlib
matplotlib.rc('font', family='Microsoft YaHei') # 显示中文
matplotlib.use('Agg') # 兼容无界面环境
def plot_rating_distribution(df):
"""评分分布柱状图"""
# 统计各评分区间的电影数量
bins = [i/2 for i in range(16, 22)] # 8.0 ~ 10.0
labels = [f"{bins[i]:.1f}~{bins[i+1]:.1f}" for i in range(len(bins)–1)]
counts = []
for i in range(len(bins)–1):
count = len(df[(df["rating"] >= bins[i]) & (df["rating"] < bins[i+1])])
counts.append(count)
plt.figure(figsize=(10, 6))
bars = plt.bar(labels, counts, color='#1f77b4', alpha=0.8)
plt.title('豆瓣电影Top250 评分分布', fontsize=16)
plt.xlabel('评分区间', fontsize=12)
plt.ylabel('电影数量', fontsize=12)
# 在柱子上标出数字
for bar, count in zip(bars, counts):
plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,
str(count), ha='center', fontsize=10)
plt.tight_layout()
plt.savefig('豆瓣评分分布.png', dpi=100)
plt.close()
print("图表已保存: 豆瓣评分分布.png")
def plot_top10_bar(df):
"""评分最高10部电影的横向柱状图"""
top10 = df.head(10)
plt.figure(figsize=(10, 8))
bars = plt.barh(top10["title"], top10["rating"], color='#ff7f0e', alpha=0.8)
plt.title('豆瓣电影Top250 评分最高Top10', fontsize=16)
plt.xlabel('评分', fontsize=12)
plt.xlim(9.0, 10.0)
# 在柱子旁标出分數
for bar, rating in zip(bars, top10["rating"]):
plt.text(bar.get_width() + 0.02, bar.get_y() + bar.get_height()/2,
str(rating), va='center', fontsize=10)
plt.tight_layout()
plt.savefig('豆瓣评分Top10.png', dpi=100)
plt.close()
print("图表已保存: 豆瓣评分Top10.png")
完整主程序
import time
if __name__ == "__main__":
start = time.time()
# 1. 多线程爬取
movies = crawl_all()
print(f"爬取完成!共{len(movies)}条,耗时{time.time()–start:.1f}秒\\n")
# 2. 转为 DataFrame
df = pd.DataFrame(movies)
# 3. 输出统计
print(df["rating"].describe())
# 4. 生成图表
plot_rating_distribution(df)
plot_top10_bar(df)
print(f"\\n全部完成!总耗时{time.time()–start:.1f}秒")
五、多线程 vs 单线程速度对比
| 单线程(逐页爬) | ~10秒 | 基准 |
| 多线程(5线程) | ~2.8秒 | 快3~4倍 |
| 多线程(10线程) | ~1.5秒 | 快6~7倍 |
六、踩坑提醒
1. 线程数不是越多越好
线程数超过 10 后速度提升不明显,反而可能被网站封 IP。建议 5~8 个线程。
2. 多线程爬取顺序是乱的
多线程执行时页面返回顺序不确定,最后需要按排名排序:
movies.sort(key=lambda x: x["rank"])
3. Matplotlib 中文乱码
添加以下配置解决:
matplotlib.rc('font', family='Microsoft YaHei')
matplotlib.rc('axes', unicode_minus=False)
总结
多线程爬虫 + 数据可视化是爬虫开发的进阶技能。多线程大幅提升爬取效率,可视化让分析结果一目了然。这套思路不仅适用于豆瓣,也适用于任何需要大规模数据采集+展示的场景。
如果对你有帮助,欢迎点赞、评论、关注【张老师技术栈】,持续分享 Java/Python/爬虫 实战干货。


