文章目录
- 一、环境准备
-
- [1.1 安装依赖](#1.1 安装依赖)
- [1.2 查看版本](#1.2 查看版本)
- [二、Requests 库基础](#二、Requests 库基础)
-
- [2.1 发送第一个请求](#2.1 发送第一个请求)
- [2.2 Response 对象的核心属性](#2.2 Response 对象的核心属性)
- [2.3 常见 HTTP 状态码](#2.3 常见 HTTP 状态码)
- [三、GET 请求与 POST 请求](#三、GET 请求与 POST 请求)
-
- [3.1 GET 请求](#3.1 GET 请求)
- [3.2 POST 请求](#3.2 POST 请求)
- [3.3 data 与 json 参数的区别](#3.3 data 与 json 参数的区别)
- [四、代理 IP 与异常处理](#四、代理 IP 与异常处理)
-
- [4.1 为什么需要代理](#4.1 为什么需要代理)
- [4.2 代理配置](#4.2 代理配置)
- [4.3 常见异常类型](#4.3 常见异常类型)
- 五、数据解析之正则表达式
- [六、XPath 基础语法](#六、XPath 基础语法)
-
- [6.1 节点类型](#6.1 节点类型)
- [6.2 路径表达式](#6.2 路径表达式)
- [6.3 谓词(筛选条件)](#6.3 谓词(筛选条件))
- [6.4 常用函数](#6.4 常用函数)
- [6.5 逻辑运算符](#6.5 逻辑运算符)
- [七、XPath 解析本地文件与网页](#七、XPath 解析本地文件与网页)
-
- [7.1 解析本地 HTML 文件](#7.1 解析本地 HTML 文件)
- [7.2 解析服务器响应(重点)](#7.2 解析服务器响应(重点))
- [7.3 parse() 与 HTML() 的区别](#7.3 parse() 与 HTML() 的区别)
- 八、实战项目一:站长素材图片爬取
-
- [8.1 页面 URL 规律分析](#8.1 页面 URL 规律分析)
- [8.2 完整代码](#8.2 完整代码)
- [8.3 关键技术点](#8.3 关键技术点)
- [九、实战项目二:豆瓣 Top250 爬取](#九、实战项目二:豆瓣 Top250 爬取)
-
- [9.1 页面 URL 规律](#9.1 页面 URL 规律)
- [9.2 完整代码](#9.2 完整代码)
- [9.3 关键技术点](#9.3 关键技术点)
- 十、总结
一、环境准备
1.1 安装依赖
bash
pip install requests # HTTP 请求库
pip install lxml # XPath 解析库(底层用 C 实现,速度快)
1.2 查看版本
python
import requests
print(requests.__version__) # 例如 2.34.2
二、Requests 库基础
2.1 发送第一个请求
python
import requests
url = "https://www.baidu.com/"
response = requests.get(url)
print(type(response)) # <class 'requests.models.Response'>
requests.get() 返回一个 Response 对象,封装了服务器的全部响应信息。
2.2 Response 对象的核心属性
| 属性 | 说明 | 示例 |
|---|---|---|
response.text |
以字符串形式返回网页源码(自动猜测编码) | '<html>...' |
response.content |
以二进制形式返回(用于图片、文件等) | b'\xff\xd8...' |
response.encoding |
设置/获取响应编码 | response.encoding = 'utf-8' |
response.status_code |
HTTP 状态码 | 200 |
response.url |
最终请求的 URL(可能有重定向) | 'https://www.baidu.com/' |
response.headers |
响应头(字典形式) | {'Content-Type': 'text/html'} |
response.json() |
直接解析 JSON 响应(返回 dict) | {'key': 'value'} |
response.cookies |
响应的 Cookies | RequestsCookieJar |
python
import requests
url = "https://www.baidu.com/"
response = requests.get(url)
response.encoding = 'utf-8' # 设置编码
print(response.text) # 字符串形式源码
print(response.content) # 二进制数据(b 开头)
print(response.url) # 请求的 URL
print(response.status_code) # 状态码 200
print(response.headers) # 响应头
2.3 常见 HTTP 状态码
| 状态码 | 含义 |
|---|---|
200 |
请求成功 |
301 / 302 |
永久/临时重定向 |
400 |
请求语法错误,服务器无法理解 |
401 |
需要身份认证 |
403 |
服务器拒绝访问(常见于反爬) |
404 |
资源未找到 |
500 |
服务器内部错误 |
504 |
网关超时 |
补充知识点 :
response.raise_for_status()会在状态码非 2xx 时主动抛出HTTPError异常,是判断请求是否成功的推荐方式。
三、GET 请求与 POST 请求
3.1 GET 请求
GET 请求的参数直接拼接在 URL 中(?key=value&key2=value2),使用 params 参数传入字典即可,Requests 会自动编码拼接。
python
import requests
url = "https://www.baidu.com/s"
params = {"wd": "迪丽热巴"}
# 请求头:伪装成浏览器,最基础的反爬手段
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/150.0.0.0 Safari/537.36"
}
response = requests.get(url=url, params=params, headers=headers)
response.encoding = 'utf-8'
print(response.text)
说明 :
params中的中文会被自动 URL 编码,无需手动处理。请求头中User-Agent(简称 UA)是最常用的反爬字段,不设置时很多网站会返回 403 或空页面。
3.2 POST 请求
POST 请求的参数放在请求体中,使用 data(表单数据)或 json(JSON 数据)参数传递。
python
import requests
url = "https://fanyi.baidu.com/sug"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/150.0.0.0 Safari/537.36"
}
data = {"kw": "蜘蛛"}
response = requests.post(url=url, data=data, headers=headers)
# 方式一:手动解析 JSON
# import json
# obj = json.loads(response.text)
# 方式二(推荐):直接用 response.json()
obj = response.json()
print(obj)
3.3 data 与 json 参数的区别
| 参数 | Content-Type | 适用场景 |
|---|---|---|
data={"key": "value"} |
application/x-www-form-urlencoded |
表单提交 |
json={"key": "value"} |
application/json |
API 接口(RESTful) |
四、代理 IP 与异常处理
4.1 为什么需要代理
频繁请求同一网站时,本机 IP 可能被封禁。使用代理 IP 可以隐藏真实 IP,降低被封风险。
4.2 代理配置
python
import requests
url = "https://cn.bing.com/search"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/150.0.0.0 Safari/537.36"
}
params = {"q": "python"}
# 代理配置(推荐带上协议前缀)
proxies = {
"http": "http://101.37.23.47:21056",
"https": "http://101.37.23.47:21056"
}
try:
response = requests.get(
url=url, params=params, headers=headers,
proxies=proxies, timeout=8
)
response.encoding = "utf-8"
print(f"状态码:{response.status_code}")
print(response.text[:500])
except requests.exceptions.ProxyError:
print("代理不可用!")
except requests.exceptions.ConnectTimeout:
print("连接代理超时!")
except requests.exceptions.RequestException as e:
print(f"其他异常:{e}")
4.3 常见异常类型
| 异常类 | 触发场景 |
|---|---|
requests.exceptions.ProxyError |
代理连接失败 |
requests.exceptions.ConnectTimeout |
连接超时 |
requests.exceptions.ReadTimeout |
读取响应超时 |
requests.exceptions.HTTPError |
raise_for_status() 检测到非 2xx 状态码 |
requests.exceptions.ConnectionError |
网络连接错误(DNS 失败、拒绝连接等) |
requests.exceptions.RequestException |
所有 requests 异常的基类 |
五、数据解析之正则表达式
在学习 XPath 之前,先体验一下用正则表达式解析网页的方式。
python
import re
import requests
url = "https://www.dytt8899.com/"
response = requests.get(url)
response.encoding = 'gb2312' # 该网站使用 gb2312 编码
# 编译正则模式
# 2026新片精品:定位锚点
# .*?:非贪婪匹配任意字符
# (?P<ul>.*?):命名分组,提取 <ul> 和 </ul> 之间的内容
# re.S:使 . 可以匹配换行符
pattern = re.compile(r"2026新片精品.*?<ul>(?P<ul>.*?)</ul>", re.S)
# finditer 返回迭代器
result = pattern.finditer(response.text)
for i in result:
print(i.group("ul"))
正则核心知识点:
| 符号 | 含义 |
|---|---|
.* |
贪婪匹配,尽可能多匹配 |
.*? |
非贪婪匹配,尽可能少匹配 |
re.S |
使 . 匹配换行符 |
(?P<name>...) |
命名分组,通过 group('name') 提取 |
finditer() |
返回迭代器,适合处理大量匹配结果 |
六、XPath 基础语法
XPath(XML Path Language)是一种在 XML / HTML 文档中查找信息的语言,是爬虫数据解析的核心工具。
6.1 节点类型
- 元素节点 :HTML 标签,如
<div></div> - 属性节点 :元素的属性,如
class="container" - 文本节点 :标签内的文本,如
<a>点击这里</a>中的"点击这里" - 根节点 :整个文档的根元素,如
<html> - 父/子/兄弟节点:节点之间的层级关系
6.2 路径表达式
| 表达式 | 含义 |
|---|---|
/ |
从根节点选取,直接找子节点(一层) |
// |
从任意位置选取,找所有子孙节点(不限层级) |
. |
当前节点 |
.. |
当前节点的父节点 |
@ |
选取属性 |
text() |
选取文本 |
绝对路径(从根开始,层级严格):
/html/body/div[1]/div[2]/a
相对路径(从任意位置匹配,推荐使用):
//div[@class='container']//a
6.3 谓词(筛选条件)
谓词写在 [] 中,用于精确筛选节点。
按位置筛选:
xpath
//ul/li[1] # 第一个 li
//ul/li[last()] # 最后一个 li
//ul/li[position() < 5] # 前 4 个 li
按属性筛选:
xpath
//button[@id='submit'] # id 为 submit 的 button
//ul/li[@class] # 有 class 属性的 li
//ul/li[@id='zwq']/@class # id 为 zwq 的 li 的 class 属性值
6.4 常用函数
| 函数 | 含义 | 示例 |
|---|---|---|
contains(@attr, 'str') |
属性是否包含子串 | //li[contains(@id, 'l')] |
starts-with(@attr, 'str') |
属性是否以子串开头 | //li[starts-with(@id, 'l')] |
string-length(@attr) |
字符串长度 | //li[string-length(@id) = 3] |
text() |
取文本 | //a/text() |
last() |
最后一个 | //li[last()] |
position() |
当前位置 | //li[position() < 5] |
6.5 逻辑运算符
xpath
//ul/li[@id='lhf' or @id='zwq'] # id 为 lhf 或 zwq
//ul/li[@id='lhf' and @class='a'] # 同时满足两个条件
//ul/li[not(@id)] # 没有 id 属性
七、XPath 解析本地文件与网页
7.1 解析本地 HTML 文件
使用 lxml.etree.parse() 解析本地文件:
python
from lxml import etree
# 解析本地文件,返回 ElementTree 对象
html_tree = etree.parse("hello.html")
# 查找 ul 下的 li(相对路径,推荐)
li_list = html_tree.xpath("//ul/li")
# 取文本
texts = html_tree.xpath("//ul/li[@id]/text()")
# 取属性值
classes = html_tree.xpath("//ul/li[@id='zwq']/@class")
# 包含匹配
result = html_tree.xpath("//ul/li[contains(@id, 'l')]/text()")
print(type(li_list)) # <class 'list'>,xpath 始终返回列表
注意 :
xpath()方法始终返回列表 ,即使只匹配到一个元素。取单个值时需要加[0],并建议先判断列表是否为空。
7.2 解析服务器响应(重点)
使用 lxml.etree.HTML() 解析网页源码字符串:提取百度热搜榜标题
python
import requests
from lxml import etree
url = "https://www.baidu.com/"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/150.0.0.0 Safari/537.36"
}
response = requests.get(url=url, headers=headers)
response.encoding = "utf-8"
# 解析网页源码
tree = etree.HTML(response.text)
# 提取"百度一下"按钮的 value 属性
btn_value = tree.xpath("//input[@id='su']/@value")[0]
print(btn_value) # 百度一下
# 提取百度热搜榜标题
hot_titles = tree.xpath(
'//ul[@id="hotsearch-content-wrapper"]'
'/li/a/span[@class="title-content-title"]/text()'
)
for title in hot_titles:
print(title)
7.3 parse() 与 HTML() 的区别
| 方法 | 输入 | 适用场景 |
|---|---|---|
etree.parse(filepath) |
文件路径或文件对象 | 解析本地 XML/HTML 文件 |
etree.HTML(text) |
字符串 | 解析网页源码字符串(爬虫最常用) |
八、实战项目一:站长素材图片爬取
目标网站 :https://sc.chinaz.com/tupian/jianzhutupian.html
目标:批量下载建筑分类图片,支持多页爬取。
8.1 页面 URL 规律分析
第1页:https://sc.chinaz.com/tupian/jianzhutupian.html
第2页:https://sc.chinaz.com/tupian/jianzhutupian_2.html
第3页:https://sc.chinaz.com/tupian/jianzhutupian_3.html
8.2 完整代码
python
import re
import os
import time
import requests
from urllib.parse import urljoin
from lxml import etree
# ========== 基础配置 ==========
SAVE_DIR = "img"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/150.0.0.0 Safari/537.36"
}
os.makedirs(SAVE_DIR, exist_ok=True) # 目录不存在则创建
def safe_filename(name):
"""清理文件名中的非法字符(Windows 不允许 \\/:*?"<>|)"""
return re.sub(r'[\\/:*?"<>|]', "_", name)
def get_page_url(page):
"""根据页码构造 URL"""
if page == 1:
return "https://sc.chinaz.com/tupian/jianzhutupian.html"
return f"https://sc.chinaz.com/tupian/jianzhutupian_{page}.html"
def get_content(page_url):
"""发送请求,返回网页源码"""
response = requests.get(page_url, headers=headers, timeout=10)
response.raise_for_status() # 非 2xx 则抛异常
response.encoding = 'utf-8'
return response.text
def download_images(html):
"""解析并下载图片"""
tree = etree.HTML(html)
# 图片名称(alt 属性)
name_list = tree.xpath("//div[@class='container']//img/@alt")
# 图片真实地址(懒加载用 data-original,而非 src)
img_list = tree.xpath("//div[@class='container']//img/@data-original")
for name, src in zip(name_list, img_list):
filename = safe_filename(name) + ".jpg"
img_url = urljoin("https:", src) # 补全协议头
save_path = os.path.join(SAVE_DIR, filename)
try:
# stream=True 流式下载,适合大文件
img_resp = requests.get(
img_url, headers=headers, timeout=10, stream=True
)
img_resp.raise_for_status()
with open(save_path, "wb") as f:
for chunk in img_resp.iter_content(1024): # 分块写入
f.write(chunk)
print(f"下载成功:{filename}")
except Exception as e:
print(f"下载失败:{filename},原因:{e}")
if __name__ == '__main__':
start_page = int(input("请输入起始页页码:"))
end_page = int(input("请输入结束页页码:"))
for page in range(start_page, end_page + 1):
try:
page_url = get_page_url(page)
print(f"正在抓取第 {page} 页:{page_url}")
html = get_content(page_url)
download_images(html)
time.sleep(1) # 礼貌延迟,避免请求过快
except Exception as e:
print(f"第 {page} 页处理失败:{e}")
8.3 关键技术点
- 懒加载图片 :很多网站图片使用
data-original属性存放真实地址,src是占位图,需注意区分。 - URL 补全 :
urljoin("https:", src)将//xxx.com/img.jpg补全为https://xxx.com/img.jpg。 - 流式下载 :
stream=True+iter_content(1024)分块写入,避免大文件占满内存。 - 文件名清洗 :Windows 文件名不允许
\/:*?"<>|,需替换为合法字符。
九、实战项目二:豆瓣 Top250 爬取
目标网站 :https://movie.douban.com/top250
目标:爬取电影名称、导演、主演、年份、国家、类型、评分、评价人数、摘引,保存为 CSV。
9.1 页面 URL 规律
第1页:https://movie.douban.com/top250?start=0
第2页:https://movie.douban.com/top250?start=25
第3页:https://movie.douban.com/top250?start=50
...
range(0, 250, 25) # 共 10 页
9.2 完整代码
python
import csv
import time
import requests
from lxml import etree
# 请求头:伪装成浏览器,绕过基础反爬
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/150.0.0.0 Safari/537.36"
}
# 打开(或创建)CSV文件,使用utf-8编码,newline=""避免空行
with open("douban_250.csv", "w", encoding="utf-8", newline="") as file:
writer = csv.writer(file)
# 写入表头(字段名)
writer.writerow([
"电影名称", "电影导演", "电影主演", "电影年份",
"电影国家", "电影类型", "电影评分", "评价人数", "电影摘引"
])
# 豆瓣Top250每页25条,共10页,start从0开始,步长25
for start in range(0, 250, 25):
url = f"https://movie.douban.com/top250?start={start}"
# ---- 发送请求 ----
try:
response = requests.get(url=url, headers=headers, timeout=10)
response.raise_for_status() # 如果状态码不是200,抛出HTTPError
except Exception as e:
print(f"页面请求失败:{url} -> {e}")
continue # 跳过当前页,继续下一页
# ---- 解析HTML ----
html = etree.HTML(response.text) # 将HTML字符串解析为Element对象
items = html.xpath("//div[@class='item']") # 选取所有包含电影信息的div容器
# 遍历当前页的每一部电影
for item in items:
try:
# 1. 电影名称:取第一个span[@class='title'],即中文名
title_list = item.xpath(".//span[@class='title'][1]/text()")
title = title_list[0].strip() if title_list else "暂无名称"
# 2. 导演、主演、年份、国家、类型:这些信息在同一个p标签内,分两行
info_lines = item.xpath(".//div[@class='bd']/p[1]/text()")
# 清洗:去除行首行尾空白,并过滤掉空字符串
info_lines = [line.strip() for line in info_lines if line.strip()]
# 初始化默认值
director = actors = year = country = movie_type = "暂无数据"
if info_lines:
# 第一行:导演: xxx 主演: xxx (中间可能包含\xa0不间断空格)
parts = info_lines[0].split("主演:")
director = parts[0].replace("导演:", "").strip()
director = director.replace("\xa0", "").strip() # 去除特殊空格
if len(parts) > 1:
actors = parts[1].strip().replace("\xa0", "").strip()
# 第二行:年份 / 国家 / 类型
if len(info_lines) > 1:
yct = info_lines[1].split("/")
if len(yct) >= 3:
year = yct[0].strip()
country = yct[1].strip()
movie_type = yct[2].strip()
# 3. 评分(例如 9.7)
rating_list = item.xpath(".//span[@class='rating_num']/text()")
rating = rating_list[0].strip() if rating_list else "暂无评分"
# 4. 评价人数(例如 3306774人评价)
rating_num_list = item.xpath(
".//div[@class='bd']/div/span[4]/text()"
)
rating_num = (
rating_num_list[0].replace("人评价", "").strip()
if rating_num_list else "0"
)
# 5. 电影摘引(经典台词)
quote_list = item.xpath(".//p[@class='quote']/span/text()")
quote = quote_list[0].strip() if quote_list else "暂无摘引"
# 将当前电影的所有数据写入CSV的一行
writer.writerow([
title, director, actors, year, country,
movie_type, rating, rating_num, quote
])
except Exception as e:
# 如果某部电影解析失败,打印错误并继续下一部
print(f"解析失败:{e}")
continue
print(f"已抓取第 {start // 25 + 1} 页数据")
time.sleep(3) # 豆瓣反爬严,每页间隔3秒,降低被封风险
print("抓取完成!")
9.3 关键技术点
- 相对 XPath :在循环中对每个
item使用.//开头的相对路径,.表示当前节点,避免匹配到整个文档的其他节点。 - 空列表判断 :
xpath()始终返回列表,取[0]前必须判断是否为空,否则会抛IndexError。 \xa0处理 :网页中常见不间断空格( 的 Unicode 表示),需用.replace("\xa0", "")清理。- CSV 写入 :
newline=""防止 Windows 下出现空行,encoding="utf-8"保证中文正常。
十、总结
本文系统梳理了 Python 爬虫的两大核心工具:
Requests 库:
get()/post()发送请求,params/data/json传递参数Response对象的text/content/json()/status_code等属性headers伪装浏览器、proxies代理 IP、timeout超时控制try-except捕获各类请求异常
XPath 解析:
/与//的区别、.与..的用法- 谓词
[]按位置、属性筛选 contains()/starts-with()/text()/@attr等函数etree.HTML()解析网页、etree.parse()解析本地文件xpath()始终返回列表,取[0]前需判空
两个实战项目覆盖了图片爬取(流式下载、懒加载处理)和结构化数据爬取(CSV 保存、多页遍历、数据清洗),是爬虫入门的典型场景。
温馨提示 :爬取数据时请遵守目标网站的
robots.txt协议和相关法律法规,控制请求频率,避免对服务器造成压力。本文仅用于学习交流。