in

如何使用 HTTPX 和 Python 进行网页爬取

如何使用 HTTPX 和 Python 进行网页爬取

HTTPX 是一个新的强大的 Python HTTP 客户端库。当涉及到网络抓取中的 HTTP 连接时,它正迅速成为最受欢迎的选项,因为它提供异步客户端和 http2 支持。

在这个重点教程中,我们将了解是什么让 Python 的 httpx 如此适合网络抓取以及如何有效地使用它。

安装 httpx

HTTPX 是一个纯 Python 包,因此可以使用pip控制台命令轻松安装:

$ pip install httpx

或者,可以使用 poetry 项目包管理器安装它:

$ poetry init -d httpx
# or
$ poetry add httpx

使用 HTTPX

HTTPX 可以直接用于单个请求,支持大多数流行的 HTTP 函数,如 GET、POST 请求,并且可以直接将 JSON 响应解包为 python 字典:

import httpx

# GET request
response = httpx.get("https://httpbin.dev/get")
print(response)
data = response.json()
print(data['url'])

# POST requests
payload = {"query": "foo"}
# application/json content:
response = httpx.post("https://httpbin.dev/post", json=payload)
# or formdata:
response = httpx.post("https://httpbin.dev/post", data=payload)
print(response)
data = response.json()
print(data['url'])

.json()这里我们使用响应的方法使用 httpx 进行 JSON 加载。Httpx 带有许多方便且可访问的快捷方式,使其成为一个非常易于访问的 Web 抓取 HTTP 客户端。

使用 httpx 客户端

对于 Web 抓取,最好使用httpx.Client可以为整个 httpx 会话应用自定义设置(例如标头、cookie 和代理以及配置)的工具:

import httpx

with httpx.Client(
    # enable HTTP2 support
    http2=True,
    # set headers for all requests
    headers={"x-secret": "foo"},
    # set cookies
    cookies={"language": "en"},
    # set proxxies
    proxies={
        # set proxy for all http:// connections:
        "http": "http://222.1.1.1:8000",
        # set proxy for all https:// connections:
        "https": "http://222.1.1.1:8000",
        # socks5, socks4 and socks4a proxies can be used as well:
        "https": "socks5://222.1.1.1:8000",
    }
) as session:

httpx 客户端对所有请求应用一组配置,甚至跟踪服务器设置的 cookie。

异步使用 httpx

要与 Python 异步使用 httpx,可以使用对象asynciohttpx.AsyncClient()

import asyncio
import httpx

async def main():
    async with httpx.AsyncClient(
        # to limit asynchronous concurrent connections limits can be applied:
        limits=httpx.Limits(max_connections=10),
        # tip: increase timeouts for concurrent connections:
        timeout=httpx.Timeout(60.0),  # seconds
        # note: asyncClient takes in the same arguments like Client (like headers, cookies etc.)
    ) as client:
        # to make concurrent requests asyncio.gather can be used:
        urls = [
            "https://httpbin.dev/get",
            "https://httpbin.dev/get",
            "https://httpbin.dev/get",
        ]
        responses = asyncio.gather(*[client.get(url) for url in urls])
        # or asyncio.as_completed:
        for result in asyncio.as_completed([client.get(url) for url in urls]):
            response = await result
            print(response)

asyncio.run(main())

请注意,当使用async with所有连接时,应在关闭语句之前完成async with,否则将引发异常:

RuntimeError: Cannot send a request, as the client has been closed.

除了该async with语句之外,还可以手动打开/关闭 httpx AsyncClient:

import asyncio
import httpx

async def main():
    client = httpx.AsyncClient()

    # do some scraping
    ...

    # close client
    await client.aclose()

asyncio.run(main())

HTTPX 故障排除

虽然 Python 的 httpx 是一个很棒的库,但它很容易遇到一些常见的问题。以下是使用 httpx 进行网页抓取时可能遇到的一些常见问题以及如何解决这些问题:

httpx.TimeoutException

httpx.TimeoutExcception当请求花费的时间超过指定/默认超时持续时间时,会发生错误。尝试提高超时参数:

httpx.get("https://httpbin.org/delay/10", timeout=httpx.Timeout(60.0))

httpx.ConnectError

httpx.ConnectError当检测到可能由以下原因引起的连接问题时会引发异常:

  • 互联网连接不稳定。
  • 服务器无法访问。
  • URL 参数错误。

httpx.TooManyRedirects

httpx.TooManyRedirects当请求超过允许的最大重定向次数时引发。

这可能是由抓取的 Web 服务器或 httpx 重定向解析逻辑的问题引起的。它可以通过手动解析重定向来修复:

response = httpx.get(
    "https://httpbin.dev/redirect/3",
    allow_redirects=False,  # disable automatic redirect handling
)
# then we can check whether we want to handle redirecting ourselves:
redirect_location = response.headers["Location"]

httpx.HTTPStatusError

httpx.HTTPStatusError使用raise_for_status=True不在 200-299 范围内的参数和服务器响应状态代码(如 404)时会引发错误:

response = httpx.get(
    "https://httpbin.dev/redirect/3",
    raise_for_status=True,
)

当 200-299 范围之外的网络抓取状态代码可能意味着抓取器被阻止。

httpx.UnsupportedProtocol

httpx.UnsupportedProtocol当协议中提供的 URL 丢失或不属于http://https://file://的一部分时,将引发错误ftp://。当 URL 缺少该部分时,最常遇到这种情况https://

重试 HTTPX 请求

HTTPX 客户端没有任何重试功能,但它可以轻松地与 Python 中流行的重试包集成,如tenacity ( pip install tenacity)。

使用tenacity我们可以添加重试逻辑,以重试 200-299 范围之外的状态代码、httpx 异常,甚至通过检查响应主体中的失败关键字:

import httpx
from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type, retry_if_result

# Define the conditions for retrying based on exception types
def is_retryable_exception(exception):
    return isinstance(exception, (httpx.TimeoutException, httpx.ConnectError))

# Define the conditions for retrying based on HTTP status codes
def is_retryable_status_code(response):
    return response.status_code in [500, 502, 503, 504]

# Define the conditions for retrying based on response content
def is_retryable_content(response):
    return "you are blocked" in response.text.lower()

# Decorate the function with retry conditions and parameters
@retry(
    retry=(retry_if_exception_type(is_retryable_exception) | retry_if_result(is_retryable_status_code) | retry_if_result(is_retryable_content)),
    stop=stop_after_attempt(3),
    wait=wait_fixed(5),
)
def fetch_url(url):
    try:
        response = httpx.get(url)
        response.raise_for_status()
        return response
    except httpx.RequestError as e:
        print(f"Request error: {e}")
        raise e

url = "https://httpbin.org/get"
try:
    response = fetch_url(url)
    print(f"Successfully fetched URL: {url}")
    print(response.text)
except Exception as e:
    print(f"Failed to fetch URL: {url}")
    print(f"Error: {e}")

上面我们使用了 tenacity 的retry装饰器并为常见的 httpx 错误定义了我们的重试规则。

轮换代理重试

当涉及到处理阻塞时,当使用 httpx 代理轮换的 Web 抓取可以与tenacity重试功能一起使用时。

在这个例子中,我们将看看在抓取块上旋转代理和标头的常见网络抓取模式。我们将添加一个重试:

  • 重试状态代码 403 和 404
  • 最多重试 5 次
  • 在重试之间随机休眠 1-5 秒
  • 为每次重试更改随机代理
  • 为每次重试更改随机用户代理请求标头

使用 httpx 和韧性:

import httpx
import random
from tenacity import retry, stop_after_attempt, wait_random, retry_if_result
import asyncio


PROXY_POOL = [
    "http://2.56.119.93:5074",
    "http://185.199.229.156:7492",
    "http://185.199.228.220:7300",
    "http://185.199.231.45:8382",
    "http://188.74.210.207:6286",
    "http://188.74.183.10:8279",
    "http://188.74.210.21:6100",
    "http://45.155.68.129:8133",
    "http://154.95.36.199:6893",
    "http://45.94.47.66:8110",
]
USER_AGENT_POOL = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:54.0) Gecko/20100101 Firefox/54.0",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0.1 Safari/604.3.5",
]


# Define the conditions for retrying based on HTTP status codes
def is_retryable_status_code(response):
    return response.status_code in [403, 404]


# callback to modify scrape after each retry
def update_scrape_call(retry_state):
    # change to random proxy on each retry
    new_proxy = random.choice(PROXY_POOL)
    new_user_agent = random.choice(USER_AGENT_POOL)
    print(
        "retry {attempt_number}: {url} @ {proxy} with a new proxy {new_proxy}".format(
            attempt_number=retry_state.attempt_number,
            new_proxy=new_proxy,
            **retry_state.kwargs
        )
    )
    retry_state.kwargs["proxy"] = new_proxy
    retry_state.kwargs["client_kwargs"]["headers"]["User-Agent"] = new_user_agent


@retry(
    # retry on bad status code
    retry=retry_if_result(is_retryable_status_code),
    # max 5 retries
    stop=stop_after_attempt(5),
    # wait randomly 1-5 seconds between retries
    wait=wait_random(min=1, max=5),
    # update scrape call on each retry
    before_sleep=update_scrape_call,
)
async def scrape(url, proxy, **client_kwargs):
    async with httpx.AsyncClient(
        proxies={"http://": proxy, "https://": proxy},
        **client_kwargs,
    ) as client:
        response = await client.get(url)
        return response

以上是如何应用重试逻辑的简短演示,该逻辑可以在每次重试时轮换代理和用户代理字符串。

首先,我们定义我们的代理和用户代理池,然后使用@retry装饰器将我们的抓取功能与坚韧的重试逻辑包装起来。

为了修改每次重试,我们使用的before_sleep参数可以在每次重试时使用新参数更新我们的抓取函数调用。

这是一个示例测试运行:

async def example_run():
    urls = [
        "https://httpbin.dev/ip",
        "https://httpbin.dev/ip",
        "https://httpbin.dev/ip",
        "https://httpbin.dev/status/403",
    ]
    to_scrape = [scrape(url=url, proxy=random.choice(PROXY_POOL), headers={"User-Agent": "foo"}) for url in urls]
    for result in asyncio.as_completed(to_scrape):
        response = await result
        print(response.json())


asyncio.run(example_run())

常问问题

为了总结这个 Python httpx 介绍,让我们看一下与使用 httpx 进行网络抓取相关的一些常见问题。

HTTPX 与请求

Requests 是最流行的 Python HTTP 客户端,以易于访问和使用着称。它也是 HTTPX 的灵感来源,HTTPX 是具有现代 Python 功能(如异步支持和 http2)的请求的自然继承者。

HTTPX 与 Aiohttp

Aiohttp 是最早支持 asyncio 的 HTTP 客户端之一,也是 HTTPX 的灵感来源之一。这两个包非常相似,虽然 aiohttp 更成熟,而 httpx 更新但功能更丰富。因此,当谈到 aiohttp 与 httpx 时,后者在网络抓取中更受欢迎,因为它支持 http2。

如何将 HTTP2 与 httpx 一起使用?

Httpx 支持 http2 版本,推荐用于网络抓取,因为它可以大大降低抓取器块率。默认情况下不启用 HTTP2,并且必须在对象http2=True中使用该参数。httpx.Client(http2=True)httpx.AsyncClient(http2=True)

如何自动跟随 httpx 中的重定向?

与请求等其他 Python 库不同,Httpx 默认不遵循重定向。allow_redirects=True在 httpx 请求方法httpx.get(url, allow_redirects=True)或 httpx 客户端对象中使用参数后启用自动重定向httpx.Client(allow_redirects=True)

概    括

HTTPX 是一个出色的新 HTTP 客户端库,它正在迅速成为 Python 网络抓取社区中的事实标准。它提供了 http2 和 asyncio 支持等功能,可降低阻塞风险并允许并发网络抓取。

与坚韧一起,httpx 使请求 Web 资源变得轻而易举,具有强大的重试逻辑,如代理和用户代理头旋转。

Written by 河小马

河小马是一位杰出的数字营销行业领袖,广告中国论坛的重要成员,其专业技能涵盖了PPC广告、域名停放、网站开发、联盟营销以及跨境电商咨询等多个领域。作为一位资深程序开发者,他不仅具备强大的技术能力,而且在出海网络营销方面拥有超过13年的经验。