前言
在互联网时代,数据无处不在。无论是做市场调研、竞品分析,还是建立个人数据集,网页爬虫都是一项必备技能。2026 年的今天,虽然各大网站的反爬机制越来越完善,但 Python 爬虫的基础依然没有变——requests 和 BeautifulSoup 这对黄金组合仍然是入门的最佳选择。
本文将带你从零开始,系统地学习如何使用 Python 的 requests 库发送 HTTP 请求,配合 BeautifulSoup 解析 HTML 文档,最终实现一个完整的网页数据抓取项目。全文包含大量可直接运行的代码示例,适合有一定 Python 基础但从未接触过爬虫的读者。
一、环境准备与安装
在开始之前,请确保你的 Python 版本在 3.8 以上。推荐使用虚拟环境来管理项目依赖:
python -m venv scraper-env
source scraper-env/bin/activate # Linux/Mac
# 或 scraper-env\Scripts\activate # Windows
pip install requests beautifulsoup4 lxml
这里我们额外安装了 lxml 解析器,它比 Python 自带的 html.parser 速度更快、容错性更好。后续所有示例均基于这三个库。
二、requests 库基础
2.1 GET 请求——获取网页内容
requests 库最基础的功能就是发送 HTTP 请求。GET 是最常用的方法,用于从服务器获取资源。下面是一个最简单的例子:
import requests
url = 'https://httpbin.org/get'
response = requests.get(url)
print(response.status_code) # 200 表示成功
print(response.text) # 返回的 HTML 或 JSON 文本
response.status_code 返回 HTTP 状态码,200 代表请求成功,404 代表页面不存在,503 表示服务器暂时不可用。建议总是先检查状态码再处理内容。
2.2 POST 请求——提交数据
当需要向服务器提交数据时(如表单登录、搜索查询),使用 POST 方法:
data = {'username': 'test', 'password': '123456'}
response = requests.post('https://httpbin.org/post', data=data)
print(response.json()) # 直接解析 JSON 响应
如果提交的是 JSON 格式,则使用 json 参数:
import json
data = {'query': 'Python 爬虫', 'page': 1}
response = requests.post('https://httpbin.org/post', json=data)
2.3 请求头(Headers)——伪装浏览器
很多网站会检查请求头中的 User-Agent 字段,如果发现是 Python 的默认标识,可能会直接拒绝响应。因此,我们需要设置合理的请求头:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/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',
'Referer': 'https://www.google.com/',
}
response = requests.get('https://example.com', headers=headers)
设置合适的 User-Agent 是最基本的反反爬措施。Referer 告诉服务器请求从哪里来,有些网站会检查这个字段防止盗链。
2.4 超时设置(Timeout)
网络请求不可靠,如果不设置超时,程序可能在网络故障时永远挂起:
try:
response = requests.get('https://example.com', timeout=5) # 5 秒超时
response.raise_for_status() # 状态码不是 2xx 时抛出异常
except requests.exceptions.Timeout:
print('请求超时,请检查网络连接')
except requests.exceptions.ConnectionError:
print('网络连接失败')
except requests.exceptions.HTTPError as e:
print(f'HTTP 错误:{e}')
timeout 参数可以传一个数字(秒),也可以传一个元组 (connect_timeout, read_timeout) 分别控制连接超时和读取超时。
2.5 Session 会话——保持登录状态
当需要模拟登录或保持 Cookie 时,使用 Session 对象。它会自动保存服务器返回的 Cookie,并在后续请求中携带:
session = requests.Session()
# 第一次请求:登录
login_data = {'username': 'demo', 'password': 'demo123'}
session.post('https://httpbin.org/post', data=login_data)
# 后续请求自动携带 Cookie
response = session.get('https://httpbin.org/cookies')
print(response.text)
Session 不仅可以保持 Cookie,还可以统一设置 headers,避免每次重复编写:
session = requests.Session()
session.headers.update({
'User-Agent': 'Mozilla/5.0 ...',
'Accept-Language': 'zh-CN,zh;q=0.9',
})
# 所有通过 session 发出的请求都会带上上述 headers
resp1 = session.get('https://example.com/page1')
resp2 = session.get('https://example.com/page2')
三、BeautifulSoup 解析基础
3.1 解析器的选择
获取到网页内容后,我们需要从 HTML 中提取有用的信息。BeautifulSoup 是 Python 最流行的 HTML/XML 解析库:
from bs4 import BeautifulSoup
html = '标题
正文内容
'
soup = BeautifulSoup(html, 'lxml') # 使用 lxml 解析器
常用的解析器对比:
- html.parser:Python 内置,无需额外安装,但速度一般
- lxml:速度快,容错性强,推荐使用(需 pip install lxml)
- html5lib:解析最严谨,但速度最慢
3.2 find() 和 find_all()
find() 返回第一个匹配的元素,find_all() 返回所有匹配的元素列表。这两个方法是最常用的查找方式:
# 按标签名查找
h1 = soup.find('h1') # 返回第一个 h1 标签
all_p = soup.find_all('p') # 返回所有 p 标签
# 按 class 查找
content = soup.find('p', class_='content')
# 按 id 查找
title = soup.find(id='main-title')
# 组合条件
item = soup.find('div', class_='item', id='special')
# 限制返回数量
first_three = soup.find_all('li', limit=3)
获取元素的属性非常直观:
link = soup.find('a')
print(link.get('href')) # 获取 href 属性
print(link.text.strip()) # 获取纯文本内容
print(link.get('class')) # 获取 class 列表
# 获取所有链接
all_links = [a.get('href') for a in soup.find_all('a') if a.get('href')]
3.3 select() 与 CSS 选择器
如果你熟悉 CSS,select() 方法会让你感觉更加亲切。它支持几乎所有 CSS 选择器语法:
# 标签选择器
soup.select('h1') # 所有 h1 标签
soup.select('div') # 所有 div 标签
# class 选择器
soup.select('.content') # class="content" 的元素
# ID 选择器
soup.select('#main-title')
# 层级选择器
soup.select('div p') # div 内部的 p 标签
soup.select('div > p') # div 的直接子元素 p
# 属性选择器
soup.select('a[href]') # 所有带 href 的 a 标签
soup.select('a[href^="https"]') # href 以 https 开头的 a 标签
soup.select('a[href$=".pdf"]') # href 以 .pdf 结尾的 a 标签
# 伪类选择器
soup.select('li:nth-child(2)') # 第二个 li 元素
soup.select('li:first-child') # 第一个 li 元素
select_one() 返回第一个匹配元素,相当于 find():
first_link = soup.select_one('a.external')
3.4 遍历文档树
除了查找方法,BeautifulSoup 还提供了丰富的文档树遍历API:
# 父子关系
div = soup.find('div')
print(div.parent) # 父节点
print(div.children) # 子节点迭代器
print(div.contents) # 子节点列表
# 兄弟关系
p = soup.find('p')
print(p.next_sibling) # 下一个兄弟节点
print(p.previous_sibling) # 上一个兄弟节点
# 搜索所有子孙节点
all_texts = soup.find_all(string=True) # 所有文本节点
四、实战项目:抓取豆瓣电影 Top250
理论知识学得差不多了,现在我们来做一个完整的实战项目——抓取豆瓣电影 Top250 排行榜的电影信息。豆瓣 Top250 是一个非常适合初学者的目标:页面结构规整,没有复杂的 JavaScript 渲染,反爬机制也相对温和。
4.1 分析目标页面
先打开浏览器的开发者工具(F12),查看 https://movie.douban.com/top250 的 HTML 结构。可以看到每部电影的信息都包裏在
- 标题:
- 评分:
- 评价人数:中的最后一个
- 简介:
- 链接:
标签的 href 属性翻页规律也很明显:
?start=0第一页,?start=25第二页,?start=50第三页,以此类推,每页 25 部电影。4.2 编写爬虫代码
首先定义 User-Agent 轮换池和基础请求函数:
import requests from bs4 import BeautifulSoup import time import random import csv # User-Agent 轮换池 USER_AGENTS = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1', ] def get_random_headers(): return { 'User-Agent': random.choice(USER_AGENTS), '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', } def fetch_page(url): """带错误处理的页面请求函数""" try: resp = requests.get(url, headers=get_random_headers(), timeout=10) resp.raise_for_status() # 检查编码 resp.encoding = resp.apparent_encoding return resp.text except requests.exceptions.Timeout: print(f'超时:{url}') return None except requests.exceptions.HTTPError as e: print(f'HTTP 错误 {e.response.status_code}:{url}') return None except Exception as e: print(f'未知错误:{e}') return None4.3 解析单页数据
接下来我们解析每一页的电影数据:
def parse_movie_list(html): soup = BeautifulSoup(html, 'lxml') movies = [] # 每部电影在- 标签中,class 包含 "item" for item in soup.select('li'): title_el = item.select_one('.title') rating_el = item.select_one('.rating_num') star_el = item.select_one('.star') quote_el = item.select_one('.quote span') link_el = item.select_one('a') # 有些电影没有 quote(简介),需要处理 if not title_el or not rating_el: continue movie = { 'title': title_el.text.strip(), 'rating': rating_el.text.strip(), 'quote': quote_el.text.strip() if quote_el else '', 'url': link_el.get('href') if link_el else '', } # 提取评价人数 if star_el: stars = star_el.find_all('span') if len(stars) >= 3: movie['reviews'] = stars[-1].text.strip().replace('人评价', '') else: movie['reviews'] = '0' movies.append(movie) return movies
4.4 多页抓取与礼貌爬虫
爬取多页时,我们一定要遵守礼貌爬虫的规则:在两次请求之间添加适当的延迟,避免对目标服务器造成压力:
def crawl_douban_top250(): base_url = 'https://movie.douban.com/top250' all_movies = [] print('开始抓取豆瓣电影 Top250...') for page in range(10): # 总共 10 页 start = page * 25 url = f'{base_url}?start={start}' print(f'正在抓取第 {page+1}/10 页...') html = fetch_page(url) if html is None: print(f'第 {page+1} 页抓取失败,跳过') continue movies = parse_movie_list(html) all_movies.extend(movies) print(f'第 {page+1} 页获取到 {len(movies)} 部电影') # 礼貌爬虫:随机等待 1~3 秒 if page < 9: # 最后一页不需要等待 delay = random.uniform(1, 3) print(f'等待 {delay:.1f} 秒...') time.sleep(delay) return all_movies4.5 数据导出到 CSV
抓取到的数据可以保存为 CSV 文件,方便用 Excel 或数据分析工具打开:
def save_to_csv(movies, filename='douban_top250.csv'): if not movies: print('没有数据可保存') return fieldnames = ['title', 'rating', 'reviews', 'quote', 'url'] with open(filename, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(movies) print(f'成功保存 {len(movies)} 条记录到 {filename}') # 主函数:一键执行完整流程 if __name__ == '__main__': movies = crawl_douban_top250() save_to_csv(movies) print(f'抓取完成!共获取 {len(movies)} 部电影信息')五、异常处理与调试技巧
5.1 常见的 HTTP 错误
状态码 含义 常见原因 解决方案 403 Forbidden 服务器拒绝访问 检查 User-Agent,添加 Referer,尝试使用代理 404 Not Found 页面不存在 检查 URL 拼写,确认是否需要添加参数 429 Too Many Requests 请求频率过高被限流 增加 time.sleep 时间,降低请求频率 503 Service Unavailable 服务器过载或维护 等待一段时间后重试 5.2 重试机制
对于网络不稳定的情况,实现指数退避重试策略:
from functools import wraps def retry(max_retries=3, delay=1): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if attempt == max_retries - 1: raise wait = delay * (2 ** attempt) # 指数退避:1, 2, 4 秒 print(f'第 {attempt+1} 次失败:{e},{wait:.1f} 秒后重试...') time.sleep(wait) return None return wrapper return decorator @retry(max_retries=3, delay=1) def fetch_with_retry(url): return requests.get(url, timeout=5)六、礼貌爬虫的五大原则
作为一名合格的爬虫开发者,我们必须遵守以下准则:
6.1 遵守 robots.txt
在抓取任何网站之前,先查看其
/robots.txt文件。例如https://example.com/robots.txt中可能定义了哪些路径允许爬取。虽然 robots.txt 没有法律约束力,但尊重它是行业基本礼仪。6.2 控制请求频率
任何时候都不能以超过人类浏览的速度去抓取数据。一般来说,两次请求之间至少间隔 1~3 秒。使用
time.sleep(random.uniform(1, 3))可以让请求间隔更自然。6.3 设置合理的 User-Agent
不要使用 Python 的默认 User-Agent。轮换使用多个真实的浏览器 UA 字符串,降低被识别为爬虫的概率。
6.4 识别并避免触发反爬
常见反爬手段包括:
- IP 封禁:短时间内同一 IP 请求过多会被封。解决方案:使用代理 IP 池
- Cookie 验证:部分网站需要在请求中携带特定的 Cookie。解决方案:使用 Session 维护会话
- JavaScript 渲染:内容通过 JS 动态加载,直接请求 HTML 获取不到。解决方案:使用 Selenium 或 Playwright
- 验证码:触发频率限制后出现验证码。解决方案:降低频率,或接入打码平台
6.5 尊重数据版权
抓取的数据不要用于商业用途(除非获得授权),也不要大规模转载、售卖。合理使用爬虫获取的数据,遵守目标网站的条款和政策。
七、进阶方向与总结
7.1 本文总结
通过本文的学习,你掌握了:
- 使用 requests 库发送 GET/POST 请求,设置 headers、timeout 和 Session
- 使用 BeautifulSoup 的 find/find_all 和 select/select_one 解析 HTML
- 完整的实战项目:豆瓣电影 Top250 数据抓取
- 数据导出到 CSV 格式
- 异常处理、重试机制和礼貌爬虫的最佳实践
7.2 下一步学习方向
当你掌握了基础爬虫后,可以朝着以下方向继续进阶:
- 异步爬虫:使用 aiohttp + asyncio 实现大规模并发爬取,效率提升 10~100 倍
- 动态页面抓取:学习 Selenium、Playwright 或 Puppeteer 处理 JavaScript 渲染的页面
- Scrapy 框架:Python 最强大的爬虫框架,内置调度、去重、管道、中间件等机制
- 数据清洗与存储:将爬取的数据存入 MongoDB、MySQL 或 Elasticsearch
- 分布式爬虫:使用 Scrapy-Redis 或 Celery 实现多机协同抓取
爬虫是一门实践性极强的技术,只看不练永远无法真正掌握。建议你现在就打开电脑,跟着本文的代码一步步操作,从最简单的页面开始,逐步挑战更复杂的目标。
记住:能力越大,责任越大。合理使用爬虫技术,做一个有职业道德的开发者。
- 简介: