<
  • 主题:
  • + -
  • 清除背景
  • 禁用背景

python爬虫库

字数:28046 写于:2022-09-27
最新更新:2022-09-27 阅读本文预计花费您81分钟

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,常见异常有:

httpx.HTTPError ├── RequestError │ ├── ConnectError │ ├── ReadError │ ├── WriteError │ └── ... └── HTTPStatusError
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
“Beautiful Soup”这一名称来源于《爱丽丝梦游仙境》中一首名为 "Beautiful Soup"的诗,HTML 和 XML 经常被形容为“标签汤”(tag soup),以指代格式混乱的网页,BeautifulSoup 库的作用就是把这些乱糟糟的标签“汤”解析成规整、可操作的"Beautiful Soup"
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的属性:

<a href="/1" class="item">文章</a> tag = soup.a tag['href'] # 获取href属性 tag['href'] = "/a/b" #支持直接赋值
# 常用属性访问方式 tag.name # 标签名 tag.attrs # 所有属性(字典) tag['href'] # 获取属性值,可以直接赋值修改 tag.get('href') # 获取属性,不存在返回 None tag.get('href', '/a/b') # 同上,不存在时指定一个默认值 tag['class'] # 返回列表,如 ['btn', 'red'],可以直接赋值修改类 tag.text # 所有文本拼接 tag.get_text() # tag.get_text(strip=True) # 去空白 tag.get_text(separator='|') # 用分隔符 tag.string # 唯一子文本(只有一个时),可以直接赋值修改文本 tag.strings # 生成器,所有文本 tag.stripped_strings # 去空白文本

获得标签内的文本

  • 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

修改文档

tag.name = 'div' # 改标签名 tag['class'] = 'new' # 改属性 tag['new-attr'] = 'value' # 加属性 del tag['class'] # 删属性 tag.string = 'new text' # 改文本 tag.append('extra') # 追加内容 tag.insert(0, 'first') # 指定位置插入 new_tag = soup.new_tag('a') # 创建新标签 new_tag.string = 'click' tag.append(new_tag) tag.decompose() # 删除标签 tag.extract() # 提取(移除并返回) tag.replace_with(new_tag) # 替换 tag.clear() # 清空内容

输出

str(soup) # 转字符串 soup.prettify() # 格式化输出,会按嵌套层级缩进 tag.encode('utf-8') # 编码

模板参考

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)

实战

爬取网站https://books.toscrape.com/catalogue/page-1.html 中所销售的书,返回数据结果 [{ "书名": "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())
# 直接指定文件名 tree = etree.parse("data.xml") # 默认为xml解析 # 或打开的文件变量 with open("data.xml", "rb") as f: tree = etree.parse(f) # html解析 tree = etree.parse("page.html", etree.HTMLParser())

创建解析器:用于手动指定解析选项

parser = etree.HTMLParser( encoding='utf-8', remove_comments=True, remove_blank_text=True, ) tree = etree.fromstring(html, parser)
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] 索引
#Element(单个元素)对象 和 ElementTree(整个树结构)对象 都支持以下 xpath() 方法 # 基本查找 root.xpath('//div') # 所有 div root.xpath('//div[@class="a"]') # 属性筛选,类名为a的div,注意: #如果该元素有多个类名需要完整写入全部类名 #如果a只是其中一个类名,需要用root.xpath('//div[contains(@class,"a")]') root.xpath('//a/@href') # 取属性值 root.xpath('//p/text()') # 取文本,文本通常包含大量空白符 text_list=type_list = [ # 移除文本前后空白,以及本身就是空白的值 text.strip() (移除文本行前后空白) for text in root.xpath("//ul[@class='nav nav-list']/li/a/text()") if text.strip() (移除整个文本行都为空白的值) ]

# 层级关系
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
root.cssselect('div.item') # 等价 //div[contains(@class,'item')] root.cssselect('#main a') # id + 后代 root.cssselect('a[href]')

节点属性操作

XPath 的一大优势是可以直接查询属性值,而不需要先获取 Tag 再访问属性 tree.xpath('//a/@href') tree.xpath('//div/@class')

# 也可以获取节点后,通过以下方法获取、修改节点的属性
el.get('href') # 获取属性(不存在返回 None)
el.get('href', '') # 带默认值
el.set('class', 'new') # 设置属性
del el.attrib['class'] # 删除属性
el.attrib # 属性字典
el.keys() # 所有属性名
el.items() # 所有 (k, v) 对

获取文本内容

el.text # 起始标签后的文本 el.tail # 结束标签后的文本 el.text_content() # 该节点及所有子节点的文本(html 模块) ''.join(el.itertext()) # 递归取所有文本(etree)

遍历节点

对于获取的节点列表,或者需要当前节点的父、子、兄弟节点,可通过以下操作

for child in el: # 直接子节点 print(child.tag)

el.iter() # 深度优先遍历所有节点
el.iter('div') # 只遍历 div
el.iterchildren() # 遍历直接子节点
el.getparent() # 父节点
el.getnext() / el.getprevious() # 兄弟节点

创建与修改节点

# 创建节点 new = etree.Element('div', id='x') new.text = '内容'

# 添加子节点
parent.append(new)
parent.insert(0, new) # 指定位置

# 删除节点
parent.remove(child)
child.getparent().remove(child)

# 复制节点
import copy
new = copy.deepcopy(old)

序列化输出

# 转字符串 etree.tostring(el, encoding='unicode') etree.tostring(el, pretty_print=True, encoding='unicode')

# 写文件
tree.write('out.html', encoding='utf-8', pretty_print=True, method='html')

# HTML 特殊处理
html.tostring(el, encoding='unicode')

实用操作

# 判断节点是否存在 if root.xpath('//div[@id="x"]'): ...

# 安全取值
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'')

上一篇:python数据文件操作
下一篇:python常用库
z z z z z