requests
requests属于第三方库,比标准库 urllib.request 更简洁,是 Python 中常用的 HTTP 客户端库,常用于发送 HTTP/HTTPS 请求、获取响应数据,安装库:
pip install requests
如果要让 requests 模块支持 SOCKS 协议,需要额外安装依赖 PySocks
pip install PySocks
常用方法
- requests.get(url, params, **kwargs)发送 GET 请求
- requests.post(url, data, json, **kwargs)发送 POST 请求
- requests.put(url, data, **kwargs)发送 PUT 请求
- requests.delete(url, **kwargs)发送 DELETE 请求
- requests.head(url, **kwargs)发送 HEAD 请求
- requests.patch(url, data, **kwargs)发送 PATCH 请求
- requests.request(method, url, **kwargs)通用方法,method 参数可指定任意请求方式
常用参数
params查询参数,常用于 GET 请求
params = {'key1': 'value1', 'key2': 'value2'} r = requests.get('https://httpbin.org/get', params = params) #实际请求: https://httpbin.org/get?key1=value1&key2=value2 #可以使用元组列表表示重复参数 params = [ ("tag", "python"), ("tag", "web"), ]data为 POST 请求添加表单数据,默认按照表单方式发送:application/x-www-form-urlencoded
data = {'username': 'admin', 'password': '123456'} r = requests.post('https://httpbin.org/post', data=data) # 以 application/x-www-form-urlencoded 提交JSON为 POST 请求添加JSON,python会自动设置Content-Type: application/json,且 Requests 会自动将 Python 对象序列化为 JSON,不需要 json.dumps()
payload = {'name': 'Tom', 'age': 18} r = requests.post('https://httpbin.org/post', json=payload)files文件上传,建议通过 with 文件上下文管理器。上传时 Requests 会自动构造 multipart/form-data
files = {'file': open('test.txt', 'rb')} r = requests.post('https://httpbin.org/post', files=files) # 指定文件名和类型 files = {'file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}请求头 headers
headers = { 'User-Agent': 'Mozilla/5.0', 'Authorization': 'Bearer token123' } r = requests.get('https://httpbin.org/get', headers=headers)
- Cookies
cookies = {'session_id': 'abc123'} r = requests.get('https://httpbin.org/cookies', cookies=cookies)
- 超时 timeout
r = requests.get('https://httpbin.org/get', timeout=5) # 5 秒超时 r = requests.get('https://httpbin.org/get', timeout=(3, 10)) # 连接3s,读取10s
代理 proxies
proxies = { "http": "socks5h://127.0.0.1:10808", "https": "socks5h://127.0.0.1:10808", } r = requests.get('https://httpbin.org/get', proxies=proxies)重定向 allow_redirects,Requests 默认会跟随常见的301、302、303、307、308重定向,可手动关闭,可通过 r.history查看重定向历史,通过r.url查看最终URL
r = requests.get('http://github.com', allow_redirects=False) # 禁止重定向证书验证 verify,开发、测试环境中有时会指定 verify=False 来跳过验证 HTTPS 证书,生产代码建议不要关闭证书验证,会降低 HTTPS 的安全性
r = requests.get('https://example.com', verify=False) # 忽略SSL验证(会警告)身份验证 auth,用于发送简单的 HTTP Basic Authentication,对于 Bearer Token、API Key 等认证,通常直接通过 headers 指定
requests.get(
url,
auth=("username", "password")
)响应对象(Response)常用属性与方法
- r.text将响应内容解码为字符串形式,python 会根据 HTTP 头中的 Content-Type 或 response.encoding 推断编码,然后自动解码,适合处理文本内容,如:HTML、JSON、XML、纯文本等,也适合作为html解析器的输入:resp = requests.get(url) # 获得响应 soup = BeautifulSoup(resp.text, "html.parser") #解析 HTML
- r.content响应内容的原始字节(bytes),通常用来处理图片、文件、可执行文件等,如下载图片:resp = requests.get(url, stream=True, timeout=10) # 流式下载 with open("image.png", "wb") as f: f.write(resp.content) # 保存为图片
- r.status_codeHTTP 状态码
- r.encoding查看或设置文本编码,一般情况下 Requests 会根据 HTTP 响应头等信息确定编码,也可手动指定:r.encoding = "utf-8"
- r.json()将响应解析为 JSON(返回 dict),要求响应正文是有效 JSON,否则会抛出异常
- r.headers响应头,字典类型,大小写不敏感,支持字典的常用操作
- r.cookies响应中的 Cookies,操作这些Cookies的方法:
- set(name, value):添加一个cookie
- set_cookie(cookie):塞入另一个cookie对象
- get(name, default=None, domain=None, path=None):按名字取 cookie 值
- update({other}):用另一个 dict 批量更新
- keys() / values() / items():遍历
- pop(name, default) / popitem():删除
- clear(domain=None, path=None):清空
- r.url最终请求的 URL(含重定向后)
- r.apparent_encoding从内容推测的编码
- r.elapsed请求耗时(timedelta)
- r.history重定向历史列表
- r.ok状态码 < 400 返回 True,用于粗略判断请求是否成功,实际开发应该使用下面的 raise_for_status()
- r.raise_for_status()状态码非 2xx 抛出异常,但注意,某个API在200时返回的也是业务错误信息,因此这只能代表HTTP请求正常,无法代表业务操作成功
Session 会话
Session 用于在多个请求之间保持状态,如:保持登录状态
s = requests.Session()
s.headers.update({'User-Agent': 'Mozilla/5.0'})
s.cookies.set('token', 'abc123')
r1 = s.get('https://httpbin.org/cookies')
r2 = s.post(
'https://httpbin.org/post',
data={"username": "tom", "password": "123"}
)
# 同一个 Session 中 cookies、headers 自动保持
异常处理
requests.exceptions.RequestException是所有 requests 异常的基类,一般业务代码直接捕获该异常即可:
try:
......
except requests.exceptions.RequestException as e:
print(f"请求失败!{e}")也可以单独处理这些异常,常见的异常有:
- requests.exceptions.ConnectionError
- requests.exceptions.Timeout
- requests.exceptions.HTTPError
- requests.exceptions.TooManyRedirects
from requests.exceptions import (
RequestException, Timeout, ConnectionError,
HTTPError, TooManyRedirects
)
try:
r = requests.get('https://httpbin.org/get', timeout=5)
r.raise_for_status()
except Timeout:
print('请求超时')
except HTTPError as e:
print(f'HTTP错误: {e.response.status_code}')
except ConnectionError:
print('连接错误')
except RequestException as e:
print(f'请求异常: {e}')
流式下载
默认情况下,Requests 会将响应内容读取到内存,可以指定stream=True,然后通过 r.iter_content()方法分块读取
r = requests.get(url, stream=True)
with open("file.zip", "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
常用模板
模板参考
import requests
import sys
# 默认请求头:模拟浏览器,避免被部分站点直接拒绝
DEFAULT_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/154.0.0.0 Safari/537.36"
)
}
# 默认超时(秒):连接 + 读取
DEFAULT_TIMEOUT = 10
def fetch(url, timeout=DEFAULT_TIMEOUT, headers=None):
"""
发起 GET 请求,成功返回 Response 对象,失败则打印错误并退出程序。
:param url: 请求地址
:param timeout: 超时秒数,默认 10
:param headers: 自定义请求头,默认使用 DEFAULT_HEADERS
:return: requests.Response 对象(状态码为 2xx)
:raises SystemExit: 请求失败时以退出码 1 终止程序
"""
# 允许调用方覆盖请求头
if headers is None:
headers = DEFAULT_HEADERS
try:
# 发起 GET 请求
r = requests.get(url, headers=headers, timeout=timeout)
# 状态码非 2xx 时抛出 HTTPError
r.raise_for_status()
return r
# ---------- 异常处理:统一打印到 stderr 并以退出码 1 结束 ----------
except requests.exceptions.Timeout:
print("[ERROR] 请求超时", file=sys.stderr)
sys.exit(1)
except requests.exceptions.ConnectionError:
print("[ERROR] 连接错误(网络不通 / 域名解析失败)", file=sys.stderr)
sys.exit(1)
except requests.exceptions.HTTPError as e:
status = e.response.status_code if e.response is not None else "N/A"
print(f"[ERROR] HTTP 错误: {e},状态码: {status}", file=sys.stderr)
sys.exit(1)
except requests.exceptions.RequestException as e:
# 兜底:捕获其他 requests 异常
print(f"[ERROR] 其他请求异常: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
resp = fetch("https://example.com")
print(resp.text)
# 处理响应 .....
POST + JSON
r = requests.post(
url,
json={"name": "Tom", "age": 20},
timeout=10,
)
r.raise_for_status()
data = r.json()
文件/图片下载
r = requests.get(url, stream=True, timeout=10)
r.raise_for_status()
with open('logo.png', 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
文件/图片下载
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open('bigfile.zip', 'wb') as f:
for chunk in r.iter_content(chunk_size=1024 * 1024):
if chunk:
f.write(chunk)
Session管理:模拟登录
with requests.Session() as session:
session.headers.update({"User-Agent": "Mozilla/5.0"})
login_data = {'username': 'admin', 'password': '123'}
session.post(login_url, data=login_data)
r = session.get(target_url) # 已带登录态
r.raise_for_status()
httpx
HTTPX 是 Python 的第三方 HTTP 客户端库,用于发送 HTTP/HTTPS 请求、处理响应,以及进行同步和异步网络通信。requests 用于处理 简单、成熟、同步 HTTP 请求,HTTPX 很多功能和 requests 相同,主要增加了:
- 原生异步 API:AsyncClient + await
- HTTP/2 支持
- 更完整的 连接池、超时、代理、认证等客户端级配置
- 同时提供同步 API 和异步 API
- API 整体设计与 requests 接近,迁移成本较低
安装与导入
HTTPX 不是 Python 标准库,需要安装:
pip install httpx
HTTPX 同时提供同步和异步接口,通常直接完整导入:
import httpx
常用请求
HTTPX 的基本请求方式与 requests 基本一致,包含:
- response = httpx.get(url)
- response = httpx.post(url, data=data)
- response = httpx.put(url, data=data)
- response = httpx.delete(url)
这些方法所使用的参数也一样:params( URL 查询参数)、headers(请求头)、cookies(Cookie)、data(表单数据)、json(JSON 数据)、content(原始请求体)、timeout(超时时间) ,响应对象的属性、方法也相似:
- response.status_code
- response.headers
- response.text
- response.content
- response.json()
- response.url
- response.cookies
Client
httpx中的 Client / AsyncClient 用来替代 Session,相比于每次都 httpx.get(...) ,HTTPX 更推荐在需要发送多个请求时使用 Client,它可以保持和复用:
- TCP 连接,减少重复建立 TCP/TLS 连接的开销
- Cookie
- Headers
- 认证信息
- Proxy
- Timeout
- HTTP/2 配置
推荐使用 width 下文管理器管理 Client,不需要手动 client.close(),Client 支持 requests 的大部分参数,额外支持 http2 = True 来启用 http2。对于异步任务,必须将 async with 封装在async def 函数内,而不允许在同步函数/模块顶层用,这是因为只有将这些异步操作定义在一个函数内,调度器才能实现暂停执行该协程,继续执行事件循环内的另一个任务,如果在顶层,压根就无法实现暂停顶层函数交还控制权的行为了
# 同步
with httpx.Client(
base_url="https://api.example.com", #支持相对url,requests 的 Session 没有原生 base_url
headers={"X-Token": "abc"},
cookies={"session": "..."},
timeout=10,
follow_redirects=True
) as client:
r = client.get("/users")
# 异步,必须封装在async def 函数内,而不允许在同步函数/模块顶层用 async with
sync def f():
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
r = await client.get("/users")
异步请求
相比于 requests,HTTPX 最大的新增能力是原生支持 asyncio,此时需要使用 AsyncClient()
# 同步
with httpx.Client() as client:
response = client.get(url)
# 异步
async with httpx.AsyncClient() as client:
response = await client.get(url)
import asyncio
import httpx
# 单个异步请求
async def main():
async with httpx.AsyncClient() as client:
response = await client.get("https://example.com")
print(response.text)
# 并发请求
async with httpx.AsyncClient() as client:
responses = await asyncio.gather(
client.get(url1),
client.get(url2),
client.get(url3),
)
#顶层的异步main(),必须显式启动一个事件循环
asyncio.run(main())
HTTP/2
HTTPX 支持 HTTP/2,需要首先安装依赖
pip install "httpx[http2]"
通过参数 http2=True即可启用HTTP/2
with httpx.Client(http2=True) as client:
r = client.get("https://www.google.com")
print(r.http_version) # 查看实际使用的HTTP版本,"HTTP/2"
Timeout
requests 默认无限等待,需要手动指定 timeout,而 httpx 库默认超时是 5 秒,可以通过 timeout=None 禁用超时,或简单通过httpx.get(url, timeout= 8)指定超时,此外,httpx 支持通过httpx.Timeout 指定更细粒度的超时配置:
- connect 建立连接超时
- read 等待服务器返回数据超时
- write 上传请求数据超时
- pool 从连接池获取连接超时
timeout = httpx.Timeout(
10.0,
connect=5.0,
read=10.0,
write=10.0,
pool=5.0,
)
httpx.get(url, timeout=timeout)
重定向
requests 默认启用重定向:allow_redirects=True ,httpx 默认不跟随重定向(坑),需要手动指定
# 普通请求
httpx.get(url,follow_redirects=True)
# Client
with httpx.Client(follow_redirects=True) as client:
# 同样可查看重定向历史、最终地址
r.history
r.url
代理Proxy
# 简单配置代理
httpx.get(url, proxy="http://127.0.0.1:8080")
# 按协议分流(类似 requests 的 proxies)
httpx.get(url, mounts={
"http://": httpx.HTTPTransport(proxy="http://proxy1:8080"),
"https://": httpx.HTTPTransport(proxy="http://proxy2:8080"),
})
Streaming:流式请求/响应
对于大文件、长响应等情况,可以进行流式处理,而不是一次性将整个响应加载到内存,核心方法是:
同步
- iter_bytes()字节块
- iter_text()文本块
- iter_lines()文本行
异步 - aiter_bytes()异步字节块
- aiter_text()异步文本块
- aiter_lines()异步文本行
# 同步
with httpx.stream("GET", url) as response:
for chunk in response.iter_bytes():
...
# 异步
async with httpx.AsyncClient() as client:
async with client.stream("GET", url) as response:
async for chunk in response.aiter_bytes():
...
手动构造Request/Response 对象
HTTPX 还可以显式构造 Request对象,深度开发需要,简单了解,核心方法是build_request()和dend()
request = client.build_request(
"POST",
url,
json=data,
)
response = client.send(request)
异常处理Exceptions
与 requests 库相似,r.raise_for_status()用于把 4xx / 5xx HTTP 状态转换成异常,但其他情况,如:网络连接失败则属于另一类 RequestError。HTTPX 异常的基类为HTTPError,常见异常有:
try:
response = httpx.get(url)
response.raise_for_status()
except httpx.HTTPError as e:
print(e)
实战模板
爬取页面:https://quotes.toscrape.com/ 前2页的名人名言,保持为json格式
import httpx
import json
import asyncio
from urllib.parse import urljoin
from lxml import etree
base_url="https://quotes.toscrape.com/page/"
quotes=[]
# 异步请求页面
async def fetch_page(path,client,sem):
#分页面的url为:https://quotes.toscrape.com/page/2/ 形式
url = urljoin(base_url,str(path)) #拼接url,接收参数为str
async with sem:
resp = await client.get(url)
resp.raise_for_status()
return resp.url,resp.text
# 解析html
def parse_html(url,html):
print(f"正在解析{url}")
root = etree.HTML(html)
quotes = root.xpath("//div[@class='quote']")
page_result=[]
for ele in quotes:
author = ele.xpath(".//small[@class='author']/text()")[0].strip()
content = ele.xpath(".//span[@class='text']/text()")[0].strip()
page_result.append({
"作者":author,
"内容":content})
return page_result
async def main():
sem = asyncio.Semaphore(5)#通过信号量限制协程并发
async with httpx.AsyncClient(
follow_redirects=True,
timeout=10) as client:
# 这里只简单爬取前2页,return_exceptions=True保证请求错误时也返回结果
result = await asyncio.gather(*[fetch_page(path,client,sem) for path in range(1,3)],return_exceptions=True)
for item in result:
if isinstance(item,Exception):
print(f"请求失败!",item)
continue #当请求某个页面失败,item里只有一个异常结果,因此要跳过包含解包在内的逻辑
url,html = item
parse_result = parse_html(url,html)
quotes.extend(parse_result)
with open('quote.json','w',encoding="utf-8") as f:
json.dump(quotes,f,ensure_ascii=False,indent=2)
if __name__ == "__main__":
asyncio.run(main())
BeautifulSoup
BeautifulSoup 是 Python 中用于解析和遍历 HTML/XML 文档的第三方库,常用于网页爬虫中的 HTML 数据提取。BeautifulSoup 的包名是beautifulsoup4,导入名是bs4
- 安装:
pip install beautifulsoup4 - 导入:
from bs4 import BeautifulSoup
soup = BeautifulSoup('<body>...', "html.parser") # 字符串解析
soup = BeautifulSoup(open('a.html'), 'lxml') # 从文件解析四大对象类型
BeautifulSoup 包含四大对象:
- BeautifulSoup:代表整个文档,大致类似于JS中的 Document,通过 soup = BeautifulSoup() 返回
- Tag:标签对象,如:soup.div 即可得到一个 Tag 对象,大致类似于JS中的 Element
- NavigableString:代表元素中的文本节点,相当于 Text
- Comment:HTML注释节点,如:<!-- xxx -->,相当于JS中的 Comment
创建BeautifulSoup对象
soup = BeautifulSoup(文件句柄或字符串, 解析器)
- 如果解析对象是文件,不能直接传入文件名,而需要为打开的文件句柄,推荐在 with 语句块中处理文件
with open(file_name, "r", encoding="utf-8") as f: soup = BeautifulSoup(f, "html.parser") .......
常用的解析器有:
- html.parser是 Python 标准库自带的 HTML 解析器,一般不需要额外安装
- lxml 速度较快,为第三方解析器,参考下文 lxml 库笔记
- xml解析器
- html5lib更严格地模拟浏览器解析 HTML
查找元素
创建 BeautifulSoup 对象后,可以从中获取 html 中标签,也可以通过链式调用,从已经捕获的标签中继续获取其子标签等,因此下述方法可用于:
- 用于BeautifulSoup 对象:soup.select(xxx) 从整个文档查找指定元素
- 用于 Tag 对象:soup.select('div.main')[0].select_one("a[href]"),从类名为 main 的div的子DOM中查找a
直接访问标签
- soup.xxx直接访问标签,如:soup.title、soup.h1等,适合确定页面只有一个目标标签的简单情况
select()——CSS 选择器(推荐)
通过 CSS Selector 定位元素,BeautifulSoup 对+、~ 的支持依赖底层解析器,html.parser支持有限,lxml更完整,CSS Selector核心方法为:
- soup.select_one()只获取第一个元素,如果没有返回None
- soup.select() 返回所有匹配的元素,返回值可通过列表方式访问,如:soup.select('div')[0].xxx
基础选择器
soup.select('div') # 标签
soup.select('*') # 所有元素
soup.select('.class') # 类
soup.select('#id') # id
soup.select('div.class') # 标签 + 类
soup.select('div#id') # 标签 + id
soup.select('.a.b') # 同时含 a 和 b 两个类
选择多个标签
soup.select('div, p') # 同时选 div 和 p
soup.select('h1, h2, h3') # 多个标签
soup.select('.a, #b, div.c') # 混合
层级/组合选择器
soup.select('div > p') # 直接子元素
soup.select('div p') # 所有后代
soup.select('div + p') # 紧邻的下一个兄弟
soup.select('div ~ p') # 后面所有同级兄弟
属性选择器
soup.select('a[href]') # 有 href 属性
soup.select('a[href="http://x.com"]') # 属性等于
soup.select('a[href^="http"]') # 以...开头
soup.select('a[href$=".com"]') # 以...结尾
soup.select('a[href*="baidu"]') # 包含
soup.select('a[href~="word"]') # 属性值中含独立单词
soup.select('[data-id="1"]') # 自定义标签
soup.select('div[class="a b"]') # 属性完全匹配(注意顺序)
soup.select('a[href][target="_blank"]') # 多属性组合
soup.select('input[type="text"][name="q"]')
伪类选择器
soup.select('div.item:nth-of-type(2)') # 同类型中的第 2 个
soup.select('div:nth-child(2)') # 父元素下第 2 个子元素
soup.select('li:first-child') # 第一个子元素
soup.select('li:last-child') # 最后一个子元素
soup.select('li:first-of-type') # 同类型第一个
soup.select('li:last-of-type') # 同类型最后一个
soup.select('li:only-child') # 唯一的子元素
soup.select('li:nth-last-child(1)') # 倒数第 1 个
soup.select('p:not(.hidden)') # 排除某类
soup.select('div:empty') # 空元素
find()/find_all()
- find()查找并返回第一个符合条件的元素
- find_all()查找所有符合条件的元素,返回一个list,找不到时返回None,可以指定以下参数:
- name:指定标签名,如:find("div")
- id:指定ID,如:soup.find("div", id="main")
- class_:指定类名,由于 class 是python关键字,因此这里要写为 class_,如:soup.find("div", class_="strong")
- 支持更多HTML属性,且可以搭配正则:
soup.find_all('a')
soup.find_all(['a', 'b']) # 多个标签
soup.find_all('a', class_='btn') # 按 class(注意下划线)
soup.find_all('a', id='link1') # 按 id
soup.find_all('a', href=True) # 含某属性
soup.find_all('a', href=re.compile('^http')) # 正则
soup.find_all('a', attrs={'data-id': '1','href': '/home'}) # 多属性
soup.find_all('a', limit=3) # 限制数量
soup.find_all(string=True) # 查找文本
soup.find_all(string='text') # 按指定文本
soup.find_all(string=re.compile('文本'))
soup.find_all('a', recursive=False) # 只查直接子节点
父/子/兄弟元素
以下方法都可以获得子元素、父元素、兄弟元素等其他Element,但对于复杂页面,很多时候直接重新使用 CSS Selector 会更清晰
# 通过直接查找
tag.contents # 子节点列表
tag.children # 子节点生成器
tag.descendants # 所有后代
tag.parent # 父节点
tag.parents # 所有祖先
tag.next_sibling # 下一个兄弟
tag.previous_sibling # 上一个兄弟
tag.next_siblings # 后面所有兄弟
tag.previous_siblings # 前面所有兄弟
# 通过find
soup.find_parent('div') # 父节点
soup.find_parents('div')
tag.find_next_sibling('p') # 后面的兄弟
tag.find_previous_sibling('p') # 前面的兄弟
tag.find_next('a') # 之后第一个
tag.find_previous('a') # 之前第一个
获取Tag对象属性
在拿到一个DOM后,可通过以下方法访问该DOM的属性:
获得标签内的文本
- tag.string如果标签内容只有字符串时(不能再有其他任意Element),返回该字符串,如果还有其他内容返回None
- tag.text获取指定 tag 及其子标签中的文本
- 推荐使用:tag.get_text("分隔符",strip=True)指定 tag 及其子标签中的文本,且可以指定返回结果的文本分隔符,是否去除两边空白符
# 假设HTML结构为
<div class="title">Hello
<span>word</span>
<span>python</span>
</div>
#python代码
print(soup.select('.title')[0].string) # 返回None,它只能在<div class="title">Hello</div>时返回内容
print(soup.select('.title')[0].text)
# 返回Hello
# word
# python
print(soup.select('.title')[0].get_text("==",strip=True)) #返回 Hello==word==python
print(soup.select('.title')[0].find(string=True,recursive=True)) # 只获取.title的直接子元素,且需要为字符串类型,因此返回Hello
修改文档
输出
模板参考
import httpx
from bs4 import BeautifulSoup
response = httpx.get(url)
soup = BeautifulSoup(response.text, "html.parser")
for item in soup.select(".item"):
title = item.select_one(".title")
link = item.select_one("a")
title = title.get_text(" ", strip=True)
link = link.get("href")
print(title, link)
实战
[{
"书名": "A Light in the Attic",
"星级": 3,
"价格": "£51.77",
"状态": "在售"
},
......
]from bs4 import BeautifulSoup
import requests
import json
import sys
url="https://books.toscrape.com/catalogue/page-1.html"
source_html="book.html"
data_json="book.json"
book_star_map={"One":1,"Two":2,"Three":3,"Four":4,"Five":5}
# 1. 请求网页并保存HTML
try:
resp=requests.get(
url,
headers={"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36"},
timeout=10
)
resp.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"请求失败!{e}")
sys.exit(1)
with open(source_html,"w",encoding="utf-8") as f:
f.write(resp.text)
# 2. 读取本地HTML
with open(source_html,"r",encoding="utf-8") as f:
soup = BeautifulSoup(f,'html.parser')
# 3. 解析数据
book_list = []
for item in soup.select(".col-xs-6"):
# 书名
book_name = item.select_one('h3 a')["title"]
# 星级
star=item.select_one('.star-rating')['class'][1]
book_stars=book_star_map[star]
# 价格与销售状态
product_info=item.select_one(".product_price")
book_price=product_info.select_one(".price_color").get_text("",strip=True)
availability = product_info.select_one(".availability")
book_status = "在售" if "instock" in availability["class"] else "缺货"
book_list.append({
"书名":book_name,
"星级":book_stars,
"价格":book_price,
"状态":book_status,
})
# 4.保存数据
with open(data_json,"w",encoding="utf-8") as f:
json.dump(book_list,f,ensure_ascii=False,indent=2)
lxml
lxml 是 Python 的第三方 XML/HTML 解析库,底层是基于 C 语言实现的 libxml2、libxslt,特点是解析速度快、功能完整、支持 XPath,适合需要高性能或复杂节点定位的网页/XML 解析。
进行网页爬取时,BeautifulSoup 的接口通常更直观;但如果需要大量解析、复杂 XPath 查询或 XML 处理,lxml 更常用
- BeautifulSoup 更偏向于提供方便、易用的 HTML 文档操作接口,适合快速解析网页
- lxml 更偏向于高性能的结构化文档解析,HTML/XML 都支持,并且原生支持 XPath
- BeautifulSoup 可以使用 lxml 作为底层解析器:BeautifulSoup(html, "lxml")
安装与导入
lxml 是第三方库,需要使用 pip 安装:
pip install lxml
内含HTML解析器、XML解析器:
- HTML解析:使用 HTMLParser 作为解析器,规则宽松,容忍未闭合标签 <br>、<img>,容忍 & 不转义
- XML解析:使用 XMLParser(libxml2 的 XML 解析器),规则严格,必须有唯一的根节点,标签必须正确闭合,& 必须转义为 &,如果内容不符合 XML 规范,会直接抛异常
#实际使用中最常见的是 etree 模块和 html 模块
#etree是HTML/XML 通用解析器,html解析器是html专用解析器,对html容错强
from lxml import etree
或
from lxml import html
解析文档
lxml提供了两个主要解析模块:
- html:专门用于解析html
- etree:lxml库的底层树解析 API,提供能力:
- etree.parse():默认解析为xml,可手动指定解析方式,参考下文
- etree.XML():解析为xml
- etree.HTML():解析为html
lxml.etree
从文本解析
- etree.HTML('html字符串') HTML解析,返回 Element,常用于解析http响应返回的html,如:tree = etree.HTML(resp.text)
- etree.XML('xml字符串') XML解析,返回 Element
- etree.fromstring('字符串',解析器)默认以XML方式解析,返回 Element,可指定解析器,且有:
- etree.fromstring(text, etree.HTMLParser())等价于etree.HTML(text)
- etree.fromstring(text, etree.XMLParser())等价于etree.XML(text)
- 但解析器可以指定更详细的解析参数(参考下文创建解析器)
import requests
import sys
from lxml import etree
try:
resp =requests.get(
url,
timeout=10
)
resp.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"请求失败!{e}")
sys.exit(1)
tree = etree.HTML(resp.text) # 解析响应返回的html
从文件解析
- etree.parse(文件,解析器)从文件或文件对象解析 XML/HTML,返回 ElementTree,默认情况下使用 xml 解析器,则有:
- etree.parse(path, etree.HTMLParser())从文件解析html
- etree.parse(path) 从文件解析xml,等价于etree.parse(path, etree.XMLParser())
创建解析器:用于手动指定解析选项
lxml.html
- html.fromstring(text)解析 HTML 字符串/片段,返回Element
- html.parse(source)解析 HTML 文件/URL 等,返回ElementTree
- html.tostring(element)将 HTML 元素序列化,可指定编码方式
getroot()
上述解析方式中,如果方法返回的是ElementTree,可通过tree.getroot()拿到根 Element
XPath查询
使用 XPath(XML Path Language,语法参考 xml 笔记)语法获得元素:
- /div从文档根节点查找,直接子节点为div的所有元素,如:ul.xpath('/li'),将忽略当前上下文节点ul,查找根节点的直接子节点li
- //div从文档根节点查找,任意后代节点div元素,如:ul.xpath('//li'),将忽略当前上下文节点ul,查找全局所有li节点
- ./div从当前节点的直接后代子节点中查找,如:ul.xpath('./li'),查找当前ul的直接子节点li
- .//div从当前节点的任意后代子节点中查找,如:ul.xpath('.//li'),查找当前ul的后代节点中的所有li
- xpath() 始终返回列表,取单个需加 [index] 索引
# 层级关系
root.xpath('//ul/li') # 直接子节点
root.xpath('//ul//li') # 所有后代
root.xpath('//div[1]') # 第一个
root.xpath('//div[last()]') # 最后一个
root.xpath('//div[position()<3]') # 前两个
# 根据属性查询
root.xpath('//a[@class="btn"]')
root.xpath('//div[@id="main"]')
root.xpath('//a[@href]') # 是否有href属性
# 条件与逻辑
root.xpath('//div[@id="x" or @class="y"]')
root.xpath('//div[contains(@class,"item")]')
root.xpath('//div[starts-with(@id,"box")]')
root.xpath('//div[text()="确定"]') # 内容为指定文本
root.xpath('//div[contains(text(),"关键词")]') #包含指定文本
#通常用于Element:
# 相对路径 / 父节点 / 兄弟节点
el.xpath('./span') # 相对当前节点
el.xpath('..') # 父节点
el.xpath('following-sibling::li') # 后续兄弟
el.xpath('preceding-sibling::li') # 前面兄弟
CSS 选择器
CSS 选择器需要 cssselect:
pip install cssselect
节点属性操作
# 也可以获取节点后,通过以下方法获取、修改节点的属性
el.get('href') # 获取属性(不存在返回 None)
el.get('href', '') # 带默认值
el.set('class', 'new') # 设置属性
del el.attrib['class'] # 删除属性
el.attrib # 属性字典
el.keys() # 所有属性名
el.items() # 所有 (k, v) 对
获取文本内容
遍历节点
对于获取的节点列表,或者需要当前节点的父、子、兄弟节点,可通过以下操作
el.iter() # 深度优先遍历所有节点
el.iter('div') # 只遍历 div
el.iterchildren() # 遍历直接子节点
el.getparent() # 父节点
el.getnext() / el.getprevious() # 兄弟节点
创建与修改节点
# 添加子节点
parent.append(new)
parent.insert(0, new) # 指定位置
# 删除节点
parent.remove(child)
child.getparent().remove(child)
# 复制节点
import copy
new = copy.deepcopy(old)
序列化输出
# 写文件
tree.write('out.html', encoding='utf-8', pretty_print=True, method='html')
# HTML 特殊处理
html.tostring(el, encoding='unicode')
实用操作
# 安全取值
val = (root.xpath('//a/@href') or [''])[0]
# 命名空间(XML)
ns = {'x': 'http://example.com/ns'}
root.xpath('//x:item', namespaces=ns)
# 去除命名空间前缀
etree.tostring(el).replace(b'ns0:', b'')