Python爬虫实战:7个经典案例解析与源码分享(附完整资料包)
1. 从零开始:Python爬虫到底能做什么?
如果你刚接触编程,或者听说过Python爬虫但不知道它具体能干嘛,那我用大白话给你解释一下。你可以把互联网想象成一个巨大的、没有围墙的图书馆,里面的信息浩如烟海。爬虫,就像是你派去这个图书馆的一个不知疲倦的、超级高效的“小机器人”。你告诉它:“去帮我找所有关于‘Python入门教程’的书籍信息,包括书名、作者和简介。” 这个小机器人就会自动地、一页一页地去浏览,把你要的信息精准地抓取回来,整理成表格或者文档。这个过程,就是爬虫的核心工作——自动化地获取和整理网络公开数据。
我刚开始学爬虫的时候,也觉得它很神秘,甚至有点“黑客”的感觉。但实际用下来,你会发现它其实是一个超级实用的工具,能帮你解决很多实际生活中的“信息焦虑”。比如,你想买一个笔记本电脑,但各个电商平台的评价看得眼花缭乱,手动整理太费时间。这时候,写一个爬虫,让它自动去抓取某东、某宝上关于这款电脑的几千条评论,然后你再用程序分析一下好评和差评的关键词,是不是瞬间就清晰了?再比如,你想追踪某个行业的最新动态,每天手动去刷新闻网站效率太低,爬虫可以定时帮你抓取指定网站的最新文章标题和链接,自动推送到你的邮箱。
所以,爬虫的本质是一个提高信息获取效率的自动化工具。它非常适合数据分析师、市场研究人员、学生(写论文找数据)、甚至是对某个领域充满好奇心的爱好者。学习爬虫,你不需要是编程大神,只要你有清晰的逻辑和一点点耐心,就能上手。接下来,我会通过7个由浅入深的实战案例,手把手带你体验爬虫的魔力,每个案例我都会附上详细的源码和踩坑心得,保证你看得懂、学得会、用得上。
2. 环境搭建与核心库扫盲
工欲善其事,必先利其器。在开始我们的爬虫冒险之前,得先把“装备”准备好。别担心,安装过程非常简单,就像给你的电脑装几个新软件一样。
2.1 安装Python与必备库
首先,确保你的电脑上已经安装了Python。我强烈建议使用Python 3.7或以上的版本,因为很多新特性对爬虫更友好。你可以去Python官网下载安装包,记得安装时勾选“Add Python to PATH”,这样在命令行里就能直接使用了。
安装好Python后,我们就需要几个核心的“武器库”。打开你的命令行(Windows上是CMD或PowerShell,Mac/Linux上是终端),输入以下命令,一行一行执行:
pip install requests
pip install lxml
pip install beautifulsoup4
pip install selenium
我来简单说说这几个库是干嘛的:
- requests:这是爬虫的“双手”,负责向网站发出“请求”(说“你好,我想看看你的内容”),并把网站的“回应”(网页内容)拿回来。没有它,爬虫寸步难行。
- lxml:这是一个非常高效和强大的解析库,特别是它的XPath功能,就像一把精准的“手术刀”,能从复杂的网页HTML代码中,快速、准确地“切”出你想要的那部分数据。
- beautifulsoup4:我们通常叫它Beautiful Soup或BS4。它和lxml功能类似,也是用来解析网页的,但它的语法更接近自然语言,对新手更友好,像是一把“智能镊子”,用起来很顺手。
- selenium:这是一个“大杀器”。有些网站的数据不是直接写在网页源代码里的,而是通过JavaScript代码在浏览器里动态加载出来的(比如很多评论区、瀑布流页面)。用requests直接抓取,只能拿到一个空壳。Selenium可以模拟一个真实的浏览器去打开网页,等所有数据都加载完毕后再去抓取,相当于派了一个“机器人浏览器”。
2.2 理解网页结构与数据定位
在开始写代码前,我们得先知道怎么在网页的“源代码森林”里找到我们想要的“数据果实”。这里有两个最重要的概念:HTML和开发者工具。
每个网页都是由HTML代码构成的,它定义了网页的结构和内容。你可以通过右键点击网页空白处,选择“检查”或“审查元素”来打开开发者工具(快捷键通常是F12)。这个工具是爬虫工程师的“眼睛”。
打开后,你会看到Elements(元素)标签页,里面就是当前网页的HTML代码树状结构。当你把鼠标移动到某行代码上时,网页上对应的区域就会高亮显示。我们的任务,就是找到我们想要的数据(比如一个商品标题、一段评论)被包裹在哪一段HTML标签里。
举个例子,一个电影标题在HTML里可能长这样:<span class="title">肖申克的救赎</span>。这里,<span>是标签,class="title"是这个标签的属性,肖申克的救赎就是标签内的文本,也就是我们要的数据。我们写爬虫,就是要告诉程序:“去网页里找到所有class属性为title的span标签,然后把里面的文本给我拿出来。”
常用的定位方法有两种:
- XPath:一种在XML/HTML文档中查找信息的语言,路径表达式非常灵活强大。比如,上面的例子用XPath可以写成:
//span[@class="title"]/text()。 - CSS选择器:Beautiful Soup主要使用的方式,语法和前端CSS选择元素很像。上面的例子用CSS选择器就是:
span.title。
在接下来的案例里,我会反复教你怎么使用开发者工具找到这些“路径”,这是爬虫最核心的基本功。
3. 案例一:用正则表达式抓取论坛帖子
第一个案例,我们从最基础也最直接的工具——正则表达式开始。正则表达式(Regular Expression,简称regex或re)是一套处理字符串的强力规则,可以用来检索、替换那些符合某个模式(规则)的文本。在网页结构简单、数据规律明显的时候,用正则表达式“硬匹配”往往最快。
3.1 目标分析与思路拆解
我们的目标是爬取某个论坛帖子里所有的回复内容,包括回复用户、回复时间和回复正文,并且这个帖子至少有5页。我们以某个篮球论坛的热门讨论帖为例。
思路很简单:
- 观察规律:首先手动打开帖子的第一页、第二页,看看浏览器地址栏的URL有什么变化。通常会发现页码(pn)参数在变,比如
pn=1,pn=2。这样我们就能通过循环生成所有页面的链接。 - 查看源码:在帖子页面右键“查看网页源代码”,找到一条回复的HTML代码块,观察用户、时间、内容这三部分信息前后有什么独特的、可识别的文本特征。
- 编写正则:根据观察到的特征,设计正则表达式来“捕捉”这些信息。
- 循环抓取与保存:用一个循环,依次请求每一页,用正则提取数据,并保存到文件(比如CSV)中。
3.2 实战代码与逐行解析
下面是我写的代码,我加了非常详细的注释,你几乎可以照着抄,只需要替换一下URL和正则表达式里的特征字符串。
import csv
import requests
import re
import time
def main(page):
# 1. 构建每一页的URL,{page}是页码变量
url = f'https://tieba.baidu.com/p/7882177660?pn={page}'
# 2. 设置请求头,模拟真实浏览器访问,这是绕过简单反爬的基础
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36'
}
# 3. 发送GET请求,获取网页HTML文本
resp = requests.get(url, headers=headers)
html = resp.text
# 4. 使用正则表达式提取数据
# 注意:这里的正则模式需要根据你目标网页的实际源码进行调整!
# 查找所有评论内容。模式意思是:找到 `style="display:;">` 和 `</div>` 之间的所有内容(非贪婪模式)
comments = re.findall('style="display:;"> (.*?)</div>', html)
# 查找所有评论用户。模式:找到包含 `class="p_author_name j_user_card"` 的a标签里的文本
users = re.findall('class="p_author_name j_user_card" href=".*?" target="_blank">(.*?)</a>', html)
# 查找所有评论时间。模式:找到 `楼</span><span class="tail-info">` 和 `</span><div` 之间的文本
comment_times = re.findall('楼</span><span class="tail-info">(.*?)</span><div', html)
# 5. 将同一楼层的用户、时间、评论内容对应起来,并写入文件
for u, c, t in zip(users, comments, comment_times):
# 简单的数据清洗:过滤掉内容里包含img/div标签的异常数据,以及用户名过长的异常数据
if 'img' in c or 'div' in c or len(u) > 50:
continue
csvwriter.writerow((u, t, c)) # 写入一行数据
print(u, t, c) # 同时在控制台打印,方便查看进度
print(f'第{page}页爬取完毕')
if __name__ == '__main__':
# 打开(或创建)一个CSV文件,用于追加写入数据
with open('tieba_comments.csv', 'a', encoding='utf-8', newline='') as f:
csvwriter = csv.writer(f)
# 写入CSV文件的表头
csvwriter.writerow(('评论用户', '评论时间', '评论内容'))
# 循环爬取前7页
for page in range(1, 8):
main(page)
time.sleep(2) # 每爬一页暂停2秒,礼貌访问,避免给服务器造成压力
关键点与踩坑提醒:
- 请求头(Headers):一定要加上
User-Agent,这是浏览器的“身份证”。没有它,很多网站会直接拒绝你的请求,认为你是机器人。 - 正则表达式:这是本案例的难点和核心。
.*?中的问号表示“非贪婪匹配”,意思是匹配尽可能少的字符,否则可能会一下子匹配到整个页面最后一个</div>,导致抓取失败。写正则时,最好先用开发者工具复制一小段包含目标的源码,在Python交互环境或在线正则测试工具里反复调试。 - 异常处理:代码里简单的
if判断是一种数据清洗。实际爬取中,可能会遇到楼层结构不一致(比如广告楼)、数据缺失等情况,更健壮的做法是用try...except包裹起来。 - 延时(time.sleep):
time.sleep(2)非常重要!毫无节制地快速请求网站,轻则IP被暂时封禁,重则可能触犯法律。这是爬虫工程师的职业道德和自我保护。
4. 案例二:多线程爬取小说并存入数据库
当你需要爬取大量数据,比如一本好几百章的小说时,一页一页顺序爬取会非常慢。这时候,多线程就派上用场了。它可以让你同时发起多个请求,速度提升非常明显。同时,我们把数据存到MySQL数据库里,方便后续的管理和查询。
4.1 项目架构与数据库设计
这个案例的流程比第一个复杂一些,我们先把架子搭好:
- 入口页分析:找到小说目录页,分析章节链接的规律。
- 章节链接提取:使用XPath从目录页提取所有章节的标题和完整链接,存入列表。
- 内容抓取函数:编写一个函数,负责根据单个章节链接,抓取该章节的正文内容。
- 数据库操作封装:编写连接、关闭数据库的函数。
- 多线程调度:创建线程池,让多个“工人”(线程)同时去执行“抓取章节内容并存入数据库”这个任务。
数据库设计很简单,我们只需要一张表,包含小说名、章节名和章节内容三个字段。
CREATE TABLE novel (
id INT AUTO_INCREMENT PRIMARY KEY,
novel_name VARCHAR(255),
chapter_name VARCHAR(255),
content TEXT
);
4.2 完整代码实现与线程池应用
下面是完整的代码,我使用了concurrent.futures模块中的ThreadPoolExecutor,这是Python中非常方便易用的线程池实现。
import requests
from lxml import etree
import re
import pymysql
from time import sleep
from concurrent.futures import ThreadPoolExecutor
# ---------- 数据库相关函数 ----------
def get_conn():
"""创建数据库连接和游标"""
conn = pymysql.connect(
host="127.0.0.1", # 你的数据库地址
user="root", # 你的数据库用户名
password="your_password", # 你的数据库密码
db="novels", # 数据库名
charset="utf8mb4" # 使用utf8mb4以支持更全的字符
)
cursor = conn.cursor()
return conn, cursor
def close_conn(conn, cursor):
"""关闭游标和连接"""
if cursor:
cursor.close()
if conn:
conn.close()
# ---------- 网页请求与解析函数 ----------
def get_xpath_resp(url):
"""发送请求并返回etree对象和响应文本"""
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36'}
resp = requests.get(url, headers=headers)
# 将响应文本解析为lxml的etree对象,方便后续使用XPath
tree = etree.HTML(resp.text)
return tree, resp.text
def get_chapters(url):
"""从小说目录页获取所有章节的标题和链接"""
tree, _ = get_xpath_resp(url)
# 使用XPath获取小说书名
novel_name = tree.xpath('//*[@id="info"]/h1/text()')[0]
print(f"开始爬取小说:《{novel_name}》")
# 使用XPath获取所有章节的dd标签(根据实际网站结构调整)
dds = tree.xpath('/html/body/div[4]/dl/dd')
title_list = []
link_list = []
# 这里只爬取前15章作为演示,你可以去掉[:15]爬取全部
for d in dds[:15]:
title = d.xpath('./a/text()')[0] # 章节标题
link = d.xpath('./a/@href')[0] # 章节相对链接
chapter_url = url + link # 拼接成完整链接
title_list.append(title)
link_list.append(chapter_url)
print(f"发现章节:{title}")
return title_list, link_list, novel_name
def get_content(novel_name, title, url):
"""爬取单个章节内容并存入数据库"""
conn = None
cursor = None
try:
conn, cursor = get_conn()
# SQL插入语句
sql = 'INSERT INTO novel (novel_name, chapter_name, content) VALUES (%s, %s, %s)'
tree, resp_text = get_xpath_resp(url)
# 方法1:使用正则表达式提取内容(适用于内容在特定id的div内)
content = re.findall('<div id="content">(.*?)</div>', resp_text, re.S)[0] # re.S让.匹配包括换行符的所有字符
# 方法2:使用XPath提取内容(如果结构清晰)
# content_list = tree.xpath('//*[@id="content"]//text()')
# content = '\n'.join(content_list)
# 数据清洗:替换掉HTML换行符和空格实体
content = content.replace('<br />', '\n').replace(' ', ' ').strip()
# 移除可能存在的广告文本
content = re.sub(r'全本小说网.*最新章节!', '', content)
print(f"正在保存章节:【{title}】")
cursor.execute(sql, [novel_name, title, content])
conn.commit() # 提交事务
except Exception as e:
print(f"章节【{title}】爬取或保存失败:{e}")
if conn:
conn.rollback() # 发生错误时回滚
finally:
sleep(1) # 每个章节抓取后暂停1秒
close_conn(conn, cursor)
if __name__ == '__main__':
# 小说目录页URL
start_url = 'https://www.qb5.tw/book_116659/'
# 获取所有章节信息
title_list, link_list, novel_name = get_chapters(start_url)
# 使用线程池,最大线程数设为5
with ThreadPoolExecutor(max_workers=5) as executor:
# 提交任务到线程池
for title, link in zip(title_list, link_list):
# submit方法将函数和参数提交,线程池会自动安排线程执行
executor.submit(get_content, novel_name, title, link)
print("所有章节爬取任务已提交到线程池,请等待完成...")
# with语句块结束时会等待所有线程执行完毕
print("小说爬取完成!")
核心技巧与注意事项:
- XPath路径获取:在开发者工具中,右键点击你想要定位的元素,选择“Copy” -> “Copy XPath”,可以快速获得一个绝对路径。但通常我们需要手动修改为更简洁、稳定的相对路径。
- 多线程数量:
ThreadPoolExecutor(max_workers=5)中的5表示同时运行5个线程。不是越多越好,太多线程会加重本地和服务器负担,可能被反爬。一般5-10个是常用范围。 - 数据库连接管理:每个线程内部自己创建和关闭数据库连接。虽然创建连接有一定开销,但这样可以避免多线程共享一个连接导致的混乱。对于爬虫这种IO密集型任务,这点开销可以接受。
- 异常处理:在
get_content函数中,我用try...except...finally包裹了核心操作。这样即使某个章节爬取出错(比如网络波动、页面结构变化),也不会影响其他章节的爬取,并且能确保数据库连接被正确关闭。
5. 案例三:XPath与Beautiful Soup4的正面较量
同一个目标,用不同的工具来实现,是学习编程的好方法。这个案例,我们就用两种最流行的解析库——XPath和Beautiful Soup4,来爬取同一个电影排行榜的数据,并对比它们的异同和优劣。
5.1 XPath版本:精准的路径选择器
XPath的语法就像在文件系统中定位文件,//表示从任意位置开始查找,[@class="title"]表示属性过滤,/text()表示获取文本。
import requests
from lxml import etree
import csv
import time
import random
def get_movie_info_xpath(url, name):
"""使用XPath解析电影详情页"""
headers = {'User-Agent': '你的User-Agent'}
resp = requests.get(url, headers=headers)
tree = etree.HTML(resp.text)
try:
# 导演:XPath路径需要根据实际网页调整
director = tree.xpath('//*[@id="info"]/span[1]/span[2]/a/text()')[0]
# 评分
rating = tree.xpath('//*[@id="interest_sectl"]/div[1]/div[2]/strong/text()')[0]
# 评价人数
rating_num = tree.xpath('//*[@id="interest_sectl"]/div[1]/div[2]/div/div[2]/a/span/text()')[0]
# 上映年份(从标题中提取)
year = tree.xpath('//*[@id="content"]/h1/span[2]/text()')[0]
year = year.strip('()')
print(f"XPath抓取:{name} | 导演:{director} | 评分:{rating} | 评价人数:{rating_num} | 年份:{year}")
return [name, director, rating, rating_num, year]
except IndexError as e:
print(f"电影《{name}》信息提取失败,可能页面结构有变:{e}")
return None
def crawl_with_xpath():
base_url = 'https://movie.douban.com/top250?start={}&filter='
with open('douban_top250_xpath.csv', 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f)
writer.writerow(['电影名称', '导演', '评分', '评价人数', '上映年份'])
for i in range(0, 250, 25): # 总共10页
page_url = base_url.format(i)
resp = requests.get(page_url, headers={'User-Agent': '你的User-Agent'})
tree = etree.HTML(resp.text)
# 获取当前页所有电影的链接和名称
movie_items = tree.xpath('//ol[@class="grid_view"]/li')
for item in movie_items:
detail_link = item.xpath('.//div[@class="hd"]/a/@href')[0]
movie_name = item.xpath('.//span[@class="title"][1]/text()')[0]
info = get_movie_info_xpath(detail_link, movie_name)
if info:
writer.writerow(info)
time.sleep(random.uniform(1, 3)) # 随机延时,更模拟人工
print(f"XPath版:第{i//25 + 1}页完成。")
time.sleep(2)
if __name__ == '__main__':
crawl_with_xpath()
5.2 Beautiful Soup4版本:灵活的Python式查询
Beautiful Soup的API更“Pythonic”,支持通过标签名、属性、CSS选择器等多种方式查找。
from bs4 import BeautifulSoup
import requests
import csv
import time
import random
def get_movie_info_bs4(url, name):
"""使用Beautiful Soup解析电影详情页"""
headers = {'User-Agent': '你的User-Agent'}
resp = requests.get(url, headers=headers)
soup = BeautifulSoup(resp.text, 'html.parser')
try:
# 使用CSS选择器查找导演信息
director_tag = soup.select_one('#info span:nth-of-type(1) span:nth-of-type(2) a')
director = director_tag.text if director_tag else '未知'
# 查找评分
rating_tag = soup.select_one('#interest_sectl .rating_num')
rating = rating_tag.text if rating_tag else '0.0'
# 查找评价人数
num_tag = soup.select_one('#interest_sectl .rating_people span')
rating_num = num_tag.text if num_tag else '0'
# 查找年份(假设在标题的第二个span里)
year_span = soup.find('span', property='v:initialReleaseDate')
year = year_span.text[:4] if year_span else '未知'
print(f"BS4抓取:{name} | 导演:{director} | 评分:{rating} | 评价人数:{rating_num} | 年份:{year}")
return [name, director, rating, rating_num, year]
except Exception as e:
print(f"电影《{name}》信息提取失败:{e}")
return None
def crawl_with_bs4():
base_url = 'https://movie.douban.com/top250?start={}&filter='
with open('douban_top250_bs4.csv', 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f)
writer.writerow(['电影名称', '导演', '评分', '评价人数', '上映年份'])
for i in range(0, 250, 25):
page_url = base_url.format(i)
resp = requests.get(page_url, headers={'User-Agent': '你的User-Agent'})
soup = BeautifulSoup(resp.text, 'html.parser')
# 使用CSS选择器找到所有电影项目
movie_items = soup.select('ol.grid_view li')
for item in movie_items:
detail_link = item.select_one('.hd a')['href']
movie_name = item.select_one('.title').text
info = get_movie_info_bs4(detail_link, movie_name)
if info:
writer.writerow(info)
time.sleep(random.uniform(1, 3))
print(f"BS4版:第{i//25 + 1}页完成。")
time.sleep(2)
if __name__ == '__main__':
crawl_with_bs4()
对比与选择建议:
- XPath:优势在于路径表达式非常强大和灵活,尤其在处理复杂的、嵌套深的页面结构时,可以写出非常精准的定位语句。劣势是语法相对独立,需要学习一套新规则,且路径容易因页面微小改动而失效。
- Beautiful Soup:优势是学习曲线平缓,对于熟悉Python和HTML/CSS的人来说更直观。它的
find()和find_all()方法非常易懂,配合CSS选择器也很强大。劣势是在处理极其复杂的解析任务时,性能可能略逊于lxml(但通常感觉不到),且某些复杂路径用CSS选择器表达不如XPath直接。
我的经验:在实际项目中,我通常会混合使用。对于简单的、结构稳定的页面,用Beautiful Soup写起来快。对于需要精确定位或结构复杂的页面,我会用XPath。很多时候,我会先用浏览器复制XPath,然后在Beautiful Soup里用select_one()(支持CSS选择器)来实现,取两者之长。
6. 案例四:抓取动态加载的电商评论数据
很多现代网站,尤其是电商、社交平台,为了用户体验和性能,会采用**异步加载(Ajax)**技术。这意味着你第一次用requests获取的网页源代码里,并没有评论数据。数据是后来由JavaScript代码动态请求一个接口(API)并填充到页面上的。对付这种网站,有两种主流方法:分析接口直接请求 和 使用Selenium模拟浏览器。这里我们先讲更高效的第一种方法。
6.1 抓包分析与接口寻找
我们以某电商平台的商品评论为例。打开商品页,按F12打开开发者工具,切换到 Network(网络) 标签页。然后,在页面上点击“查看全部评论”或者滚动到评论区域触发加载。
这时,Network面板里会刷出很多新的请求。我们的目标是找到那个真正携带评论数据的请求。通常可以按照以下步骤筛选:
- 点击 XHR 或 Fetch 过滤器,这些通常是数据接口。
- 在请求列表中,寻找名称包含
comment、review、api等关键词的请求。 - 点击一个可疑的请求,查看它的 Headers(请求头) 和 Preview/Response(响应)。
- 在响应里,如果你看到了格式规整的JSON数据,里面包含评论内容、时间、用户昵称等信息,恭喜你,找到目标了!
找到接口后,重点看它的 Request URL(请求地址) 和 Query String Parameters(查询参数)。你会发现,翻页通常是通过改变page或offset参数实现的。
6.2 模拟请求与JSON数据解析
一旦找到了接口和参数规律,用requests模拟这个请求就非常简单了,因为数据已经是结构化的JSON格式,比解析HTML简单得多。
import requests
import csv
import time
import random
def fetch_comments(page):
"""抓取指定页的评论数据"""
# 这是通过抓包分析找到的真实评论接口(示例URL,需替换)
api_url = 'https://club.jd.com/comment/productPageComments.action'
# 构造请求参数,这些参数都是从抓包中复制过来的
params = {
'productId': 100011483893, # 商品ID
'score': 0, # 0表示所有评价,3表示好评,2表示中评,1表示差评
'sortType': 5, # 排序类型,5通常是推荐排序
'page': page, # 页码,从0开始
'pageSize': 10, # 每页条数
'isShadowSku': 0,
'fold': 1
}
# 请求头至关重要,特别是Referer,很多API会校验
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.35 Safari/537.36',
'referer': 'https://item.jd.com/100011483893.html' # 表示请求来自哪个页面
}
try:
# 发送GET请求,并直接解析返回的JSON数据
resp = requests.get(api_url, params=params, headers=headers, timeout=10)
resp.raise_for_status() # 如果状态码不是200,抛出异常
data = resp.json()
# 解析JSON结构,提取评论列表
comments = data.get('comments', [])
comment_data = []
for comment in comments:
content = comment.get('content', '').replace('\n', ' ').strip() # 评论内容,清理换行
creation_time = comment.get('creationTime', '') # 评论时间
score = comment.get('score', 0) # 评分(如5分制)
nickname = comment.get('nickname', '匿名用户') # 用户昵称
comment_info = (score, creation_time, nickname, content)
comment_data.append(comment_info)
print(f"评分{score}星 | 时间{creation_time} | 用户{nickname[:5]}... | 内容{content[:30]}...")
return comment_data
except requests.exceptions.RequestException as e:
print(f"第{page+1}页请求失败:{e}")
return []
except ValueError as e:
print(f"第{page+1}页JSON解析失败:{e}")
return []
if __name__ == '__main__':
all_comments = []
total_pages = 10 # 计划爬取10页,每页10条,共100条
with open('jd_comments.csv', 'w', encoding='utf-8-sig', newline='') as f: # utf-8-sig解决Excel中文乱码
writer = csv.writer(f)
writer.writerow(['评分', '评论时间', '用户昵称', '评论内容'])
for page in range(total_pages):
print(f"正在爬取第 {page + 1} 页...")
page_comments = fetch_comments(page)
if page_comments:
writer.writerows(page_comments) # 一次性写入多行
all_comments.extend(page_comments)
else:
print(f"第 {page + 1} 页未获取到数据,可能已爬完或接口失效。")
break
# 随机延时,模拟人类操作,避免被封
sleep_time = random.uniform(2, 5)
time.sleep(sleep_time)
print(f"爬取结束,共获取到 {len(all_comments)} 条评论数据。")
关键技巧与反爬应对:
- 参数分析:
productId是关键,不同商品不一样。score参数可以控制抓取好评、中评、差评。 - 请求头模拟:
referer字段非常重要,它告诉服务器这个请求是从哪个页面发起的。不加referer,很多接口会返回错误。 - JSON处理:使用
.get(‘key’, default)方法比直接[‘key’]更安全,如果键不存在会返回默认值而不是报错。 - 延时与随机性:
time.sleep(random.uniform(2, 5))比固定的sleep(2)更好,更不容易被识别为机器人。 - 异常处理:网络请求可能失败,JSON结构可能变化,良好的异常处理能让程序更健壮,不会因为一条评论失败而停止整个任务。
7. 案例五:使用Selenium模拟登录与复杂交互
当网站登录需要复杂的验证(如滑动验证码、点选验证码),或者数据加载逻辑极其复杂,难以通过分析接口还原时,Selenium就是终极解决方案。它通过程序控制一个真实的浏览器(如Chrome)来完成所有操作,包括点击、输入、滚动等。
7.1 Selenium环境配置与基础操作
首先,你需要安装浏览器驱动。以Chrome为例:
- 查看你电脑上Chrome的版本(帮助 -> 关于Google Chrome)。
- 去ChromeDriver官网下载对应版本的驱动。
- 将下载的
chromedriver.exe文件放在Python安装目录下,或者放在项目目录里,并在代码中指定路径。
Selenium的基本操作就像你在教一个机器人使用浏览器:
driver.get(url):打开网页。driver.find_element(By.XX, ‘value’):找到一个元素(输入框、按钮等)。element.send_keys(‘text’):向输入框输入文字。element.click():点击元素。driver.implicitly_wait(10):隐式等待,让驱动在查找元素时,如果没立刻找到,会等待最多10秒。
7.2 模拟登录与信息抓取实战
我们模拟登录某个知识分享平台,并搜索、抓取相关问题。这里以扫码登录为例,因为这是目前很多平台的主流登录方式。
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
def crawl_with_selenium():
# 1. 初始化浏览器驱动
# 指定chromedriver路径,如果已在环境变量中可省略
driver = webdriver.Chrome(executable_path='./chromedriver') # 或使用 Service(ChromeDriverManager().install()) 自动管理
# 2. 打开登录页面
login_url = 'https://www.zhihu.com/signin'
driver.get(login_url)
print("请在弹出的浏览器窗口中扫码登录...")
# 等待用户手动扫码登录,这里设置一个较长的等待时间
time.sleep(30) # 给你30秒时间扫码
# 3. 登录后,进行搜索
try:
# 更稳健的方式:使用显式等待,直到搜索框出现并可点击
search_button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, 'Popover1-toggle'))
)
search_button.click()
search_box = driver.find_element(By.ID, 'Popover1-toggle')
search_box.send_keys('Python爬虫学习') # 输入搜索关键词
# 定位搜索按钮并点击(需要根据实际页面调整XPath)
submit_btn = driver.find_element(By.XPATH, '//button[@type="submit"]')
submit_btn.click()
except Exception as e:
print(f"搜索过程出错:{e}")
driver.save_screenshot('error_screenshot.png') # 出错时截图
# 4. 等待结果加载并抓取
time.sleep(5) # 等待结果渲染
# 使用显式等待确保内容区域加载出来
try:
content_area = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, 'Search-result'))
)
# 查找所有结果条目
items = driver.find_elements(By.CSS_SELECTOR, '.Search-result .List-item')
for index, item in enumerate(items[:5]): # 只取前5条结果
try:
title = item.find_element(By.CSS_SELECTOR, '.ContentItem-title a').text
excerpt = item.find_element(By.CSS_SELECTOR, '.RichText').text[:100] # 截取前100字摘要
print(f"结果 {index+1}: {title}")
print(f"摘要: {excerpt}...\n")
except:
continue # 如果某条结果结构不同,跳过
except Exception as e:
print(f"抓取结果时出错:{e}")
# 5. 关闭浏览器
input("按回车键关闭浏览器...") # 程序暂停,方便查看结果
driver.quit()
if __name__ == '__main__':
crawl_with_selenium()
Selenium的优缺点与注意事项:
- 优点:能处理几乎所有网页交互,所见即所得,无需深究复杂的网络请求。
- 缺点:速度慢,因为要启动和渲染整个浏览器;资源消耗大;容易被网站检测到自动化工具(虽然可以通过
options.add_argument(‘–disable-blink-features=AutomationControlled’)等参数进行一些隐藏)。 - 关键技巧:
- 多用等待:不要用固定的
time.sleep,优先使用WebDriverWait配合expected_conditions进行显式等待,这样效率更高。 - 元素定位:优先使用
ID、Name,其次CSS Selector和XPath。XPath尽量用相对路径,避免用包含索引的绝对路径(如/html/body/div[3]/div[2]),因为页面结构一变就失效。 - 处理弹窗和iframe:如果元素在iframe(内嵌框架)里,需要用
driver.switch_to.frame(frame_element)切换进去才能操作。 - 无头模式:在代码调试好后,可以添加
options.add_argument(‘–headless’)让浏览器在后台运行,不显示界面,节省资源。
- 多用等待:不要用固定的
8. 案例六:应对反爬策略与数据存储优化
爬虫工程师和网站维护者之间常常在进行一场“博弈”。随着你爬取规模的增大,一定会遇到各种反爬虫机制。这个案例,我们以一个社交媒体平台为例,来探讨常见的反爬策略及应对方法,并优化我们的数据存储。
8.1 常见反爬策略与破解之道
-
User-Agent检测:这是最基本的。我们的每个案例都设置了UA,但高级反爬会检查UA是否来自常见浏览器且是否合理。对策:使用
fake_useragent库随机生成UA,或者维护一个UA池轮流使用。from fake_useragent import UserAgent ua = UserAgent() headers = {'User-Agent': ua.random} -
IP限制与封禁:短时间内从一个IP发出大量请求,服务器会拒绝服务或封禁IP。对策:
- 设置合理延时:这是最基本的道德和策略。
time.sleep(random.uniform(1, 3))。 - 使用代理IP池:这是应对IP封锁最有效的方法。可以从一些服务商购买,或者寻找免费的代理(但稳定性差)。代码中更换代理:
proxies = { 'http': 'http://your_proxy_ip:port', 'https': 'https://your_proxy_ip:port' } resp = requests.get(url, headers=headers, proxies=proxies)
- 设置合理延时:这是最基本的道德和策略。
-
Cookie/Session验证:登录状态和会话标识。对策:使用
requests.Session()对象保持会话,并在首次登录后保存Cookie,后续请求都带上。session = requests.Session() login_data = {'username': '...', 'password': '...'} session.post(login_url, data=login_data) # 登录 # 后续请求都用这个session,它会自动管理cookies resp = session.get(target_url) -
请求头完整性检查:除了UA,还可能检查
Referer(来源页)、Accept-Language(接受语言)、Accept-Encoding等。对策:通过浏览器开发者工具,把真实请求的所有Headers都复制下来,模拟得越像越好。 -
JavaScript渲染与加密参数:数据由JS生成,或请求参数被加密。对策:这是最棘手的情况。首先尝试用Selenium。如果不想用Selenium,就需要用
PyExecJS等库执行JS代码,或者用逆向工程思维,在浏览器调试器中跟踪JS执行,找到参数生成逻辑并用Python复现。这需要较强的JS功底。
8.2 稳健爬虫示例与数据存储
结合以上策略,我们写一个更稳健的爬取某社交媒体用户微博的示例,并将数据存储到更强大的SQLite数据库中(比MySQL轻量,无需安装服务器)。
import requests
import sqlite3
import time
import random
from fake_useragent import UserAgent
class WeiboCrawler:
def __init__(self, user_id):
self.user_id = user_id
self.base_url = f'https://weibo.com/ajax/statuses/mymblog'
self.ua = UserAgent()
self.session = requests.Session()
self.init_db()
def init_db(self):
"""初始化SQLite数据库和表"""
self.conn = sqlite3.connect('weibo_data.db')
self.cursor = self.conn.cursor()
# 创建表,IF NOT EXISTS 避免重复创建
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS weibo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT,
author TEXT,
text_raw TEXT,
reposts_count INTEGER,
comments_count INTEGER,
attitudes_count INTEGER,
crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
self.conn.commit()
def get_headers(self):
"""生成随机的请求头"""
return {
'User-Agent': self.ua.random,
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Referer': f'https://weibo.com/u/{self.user_id}',
# 注意:实际爬取可能需要更复杂的Cookie,这里仅为示例
}
def fetch_page(self, page):
"""抓取单页微博数据"""
params = {
'uid': self.user_id,
'page': page,
'feature': '0'
}
headers = self.get_headers()
try:
# 使用session,并加入随机延时
time.sleep(random.uniform(3, 7))
resp = self.session.get(self.base_url, params=params, headers=headers, timeout=15)
resp.raise_for_status()
data = resp.json()
if data.get('ok') == 1:
weibo_list = data.get('data', {}).get('list', [])
return weibo_list
else:
print(f"第{page}页返回数据异常:{data.get('msg')}")
return []
except requests.exceptions.RequestException as e:
print(f"第{page}页请求失败:{e}")
return None # 返回None表示网络错误,可能需要重试
except ValueError as e:
print(f"第{page}页JSON解析失败:{e}")
return []
def save_to_db(self, weibo_item):
"""将一条微博数据存入数据库"""
sql = '''
INSERT INTO weibo (created_at, author, text_raw, reposts_count, comments_count, attitudes_count)
VALUES (?, ?, ?, ?, ?, ?)
'''
data = (
weibo_item.get('created_at'),
weibo_item.get('user', {}).get('screen_name'),
weibo_item.get('text_raw'),
weibo_item.get('reposts_count', 0),
weibo_item.get('comments_count', 0),
weibo_item.get('attitudes_count', 0)
)
try:
self.cursor.execute(sql, data)
except sqlite3.Error as e:
print(f"数据入库失败:{e}")
def crawl(self, max_pages=5):
"""主爬取函数"""
for page in range(1, max_pages + 1):
print(f"开始爬取第 {page} 页...")
weibo_list = self.fetch_page(page)
if weibo_list is None:
print(f"第{page}页发生网络错误,暂停一会儿后重试...")
time.sleep(10)
weibo_list = self.fetch_page(page) # 简单重试一次
if weibo_list is None:
print(f"第{page}页重试失败,跳过。")
continue
if not weibo_list:
print(f"第{page}页无数据或已爬完所有页面。")
break
for item in weibo_list:
self.save_to_db(item)
print(f" 已保存微博:{item.get('text_raw')[:50]}...")
self.conn.commit() # 每页提交一次事务
print(f"第 {page} 页爬取完成,共 {len(weibo_list)} 条。")
self.close()
def close(self):
"""关闭数据库连接"""
if self.cursor:
self.cursor.close()
if self.conn:
self.conn.close()
print("数据库连接已关闭。")
if __name__ == '__main__':
# 示例用户ID,需要替换为实际ID
user_id = '2803301701'
crawler = WeiboCrawler(user_id)
crawler.crawl(max_pages=5) # 爬取前5页
print("微博爬取任务全部完成!")
这个案例的进阶点:
- 面向对象封装:将爬虫功能封装成类,结构更清晰,便于维护和扩展。
- SQLite数据库:使用轻量级数据库,数据管理更方便,支持SQL查询分析。
- 健壮性增强:加入了网络请求异常处理、简单的重试机制、数据库操作异常处理。
- 反爬应对:整合了随机UA、Session、随机延时、完整的请求头。
- 数据去重:可以在数据库表中为
微博ID(如果接口提供)建立唯一索引,实现自动去重。
9. 案例七:从爬取到分析——完成一个小型数据项目
爬虫的最终目的不是为了“爬”,而是为了“用”。最后一个案例,我们完整地走一遍流程:确定主题 -> 爬取数据 -> 清洗存储 -> 分析可视化。我们以爬取一个电影票房榜单为例,并做简单的数据分析。
9.1 确定目标与数据抓取
假设我们对近年的电影票房趋势感兴趣。我们找到一个公开的票房数据网站(例如,一些电影数据平台),通过抓包分析找到数据接口。
import requests
import pandas as pd
import json
def fetch_box_office():
"""抓取票房数据"""
url = 'https://ys.endata.cn/enlib-api/api/home/getrank_mainland.do'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Origin': 'https://ys.endata.cn',
'Referer': 'https://ys.endata.cn/',
}
# 通常这类POST请求的参数是表单格式
data = {
'r': '0.9936776079863086',
'top': '50',
'type': '0', # 可能代表榜单类型,如0是总榜
}
all_movies = []
try:
resp = requests.post(url, headers=headers, data=data, timeout=10)
result = resp.json()
# 解析JSON结构,这个结构需要根据实际接口响应调整
movie_list = result.get('data', {}).get('table0', [])
for movie in movie_list:
movie_info = {
'排名': movie.get('Irank'),
'电影名称': movie.get('MovieName'),
'上映时间': movie.get('ReleaseTime'),
'总票房(万)': float(movie.get('BoxOffice', 0)), # 转换为数值
'平均票价': float(movie.get('AvgBoxOffice', 0)),
'平均场次': int(movie.get('AvgAudienceCount', 0))
}
all_movies.append(movie_info)
print(f"抓取:{movie_info['排名']} | {movie_info['电影名称']} | 票房:{movie_info['总票房(万)']}万")
return all_movies
except Exception as e:
print(f"抓取票房数据失败:{e}")
return []
if __name__ == '__main__':
movies = fetch_box_office()
if movies:
# 使用pandas的DataFrame保存数据,非常方便
df = pd.DataFrame(movies)
df.to_csv('box_office.csv', index=False, encoding='utf-8-sig')
print(f"数据已保存到 box_office.csv,共 {len(df)} 条记录。")
print(df.head()) # 查看前几行数据
9.2 数据清洗与Pandas分析
数据抓回来后,往往很“脏”,需要清洗。Pandas是Python数据分析的神器。
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
# 设置中文字体,防止图表乱码
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
def analyze_data():
# 1. 读取数据
df = pd.read_csv('box_office.csv')
print("数据概览:")
print(df.info())
print("\n前5行数据:")
print(df.head())
# 2. 数据清洗
# 检查缺失值
print(f"\n缺失值统计:\n{df.isnull().sum()}")
# 如果有缺失,可以填充或删除
# df = df.dropna() # 删除有缺失的行
# df['总票房(万)'] = df['总票房(万)'].fillna(0) # 填充为0
# 从‘上映时间’中提取‘年份’
df['年份'] = pd.to_datetime(df['上映时间']).dt.year
print(f"\n提取年份后:\n{df[['电影名称', '上映时间', '年份']].head()}")
# 3. 数据分析与可视化
# 分析1:各年度电影总票房趋势
yearly_box_office = df.groupby('年份')['总票房(万)'].sum().sort_index()
plt.figure(figsize=(10, 6))
plt.plot(yearly_box_office.index.astype(str), yearly_box_office.values, marker='o', linewidth=2)
plt.title('各年度上榜电影总票房趋势(单位:万)')
plt.xlabel('年份')
plt.ylabel('总票房(万)')
plt.grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
plt.savefig('yearly_trend.png', dpi=300)
plt.show()
# 分析2:平均票价最贵的10部电影
top10_avg_price = df.nlargest(10, '平均票价')[['电影名称', '年份', '平均票价']]
print(f"\n平均票价最贵的10部电影:")
print(top10_avg_price)
# 分析3:票房与场均人次的关系(散点图)
plt.figure(figsize=(8, 6))
plt.scatter(df['平均场次'], df['总票房(万)'], alpha=0.6, edgecolors='w', s=80)
plt.title('电影平均场次与总票房关系散点图')
plt.xlabel('平均场次')
plt.ylabel('总票房(万)')
plt.grid(True, linestyle='--', alpha=0.5)
plt.tight_layout()
plt.savefig('scatter_plot.png', dpi=300)
plt.show()
# 分析4:计算不同类型电影的平均票房(假设有‘类型’列)
# 如果数据中有‘类型’列,且是逗号分隔的字符串
# df['类型列表'] = df['类型'].str.split(',')
# exploded_df = df.explode('类型列表')
# genre_avg = exploded_df.groupby('类型列表')['总票房(万)'].mean().sort_values(ascending=False)
# print(f"\n各类型电影平均票房:\n{genre_avg.head()}")
if __name__ == '__main__':
analyze_data()
从爬虫到分析的完整闭环:
- 目标驱动:先想清楚你要回答什么问题(例如“近几年票房趋势如何?”、“票价和票房有关系吗?”),再决定爬什么、怎么分析。
- 数据获取:用爬虫(或直接找公开数据集)获取原始数据。
- 数据清洗:这是最耗时但最关键的一步。处理缺失值、异常值、格式转换(如字符串转日期、数值)、数据拆分(如从日期中提取年份)。
- 探索性分析(EDA):使用Pandas进行描述性统计(
df.describe())、分组聚合(groupby)、排序(nlargest)等,对数据有一个整体了解。 - 可视化:用Matplotlib、Seaborn等库将数据转化为图表。一图胜千言,趋势、对比、分布关系一目了然。
- 得出结论:基于分析结果,回答最初的问题,并可能发现新的洞察。
通过这7个案例,我们从最简单的正则表达式匹配,到应对动态页面和反爬策略,最后完成一个完整的数据分析小项目。爬虫技术本身在不断进化,网站的反爬措施也越来越复杂。但万变不离其宗,核心思路就是:模拟合法浏览器的行为,友好、有节制地获取公开数据,并将获取的数据转化为有价值的信息。希望这些实战代码和我的踩坑经验,能帮你打开Python爬虫世界的大门。记住,多动手、多思考、多查阅文档,遇到问题善用搜索引擎和开发者工具,你一定能成为爬虫高手。
更多推荐


所有评论(0)