精进Beautiful Soup 小技巧(三)---综合提供效率(缓存/error/多线程/异步)

前言:

提高抓取和解析效率的根本还是在于发送请求;如何从这个方面进行效率提升呢?

深入使用requests.Session()

 深入使用requests.Session()

1.持久连接:

当使用 requests.Session() 时,连接会话中所有的请求将优先使用一个TCP连接,即“持久连接”,这样即使你发起多次对同一主机的独立请求,Session 实例会重用底层的连接,从而降低握手的开销。
import requests

# 创建一个会话实例
session = requests.Session()

# 向相同的主机发送多次请求
response_one = session.get('https://httpbin.org/get')
response_two = session.get('https://httpbin.org/get')

# 展示使用了持久连接的行为,两个请求将通过相同的连接发送
print(id(response_one.raw._connection))
print(id(response_two.raw._connection))

# 输出一样的ID,意味着使用的是同一连接

# 事后一定清理:常规操作
session.close()

2.连接适配和参数预设:

Session 对象允许你自定义一些请求细节,如头信息和鉴权凭证等,并在之后的请求中保持这些设置,减少了重复代码的编写。
import requests
from requests.auth import HTTPBasicAuth

# 创建会话实例,并设置默认值
session = requests.Session()
session.headers.update({'user-agent': 'my-app/0.0.1'})
session.auth = HTTPBasicAuth('username', 'password')

# 现在进行的所有的请求都会发送预设的头信息
response = session.get('https://httpbin.org/headers')
print(response.text)  # 应当会见到"user-agent"和之前设定的鉴权信息

# 一般在完成请求后关闭会话
session.close()

3.为请求维持Cookie状态:

Session 对象自动处理请求的 Cookies,所有发给同一个会话的请求将使用同一个Cookie jar,在这样的机制下,所有与server的会话变量都可以一次设立,然后按预期工作。
import requests

# 创建会话实例
session = requests.Session()

# 初次登录以设置cookie
login_res = session.post('https://example.com/login', data={'username':'xxx', 'password':'yyy'})

# Session会保存服务端设置在客户端的cookie信息, 现在进行的请求都将携带这个cookie
profile_res = session.get('https://example.com/profile')

# 经过验证的响应内容
print(profile_res.text)

# 完成所有动作后关闭会话提释放资源
session.close()

你现在应该有了一个清晰的Session如何作为一个持久连接来降低延时的认识,如何使用Session预设请求参数和身份验证方式,以及如何维持cookies的状态以跨请求进行身份维持和通行。在所有会话结束之后,确保调用 .close() 方法至关重要,以确保资源的妥善释放。

异常处理

网络爬虫可能面临各种预料之外的问题,如网络波动、页面结构更改、服务器配置问题等。为了提高脚本的健壮性,应当合理捕获并处理这些异常。

案例1:处理网络请求异常

import requests
import logging
from requests.exceptions import HTTPError, ConnectionError, Timeout, RequestException

# 配置logging,设置日志级别为WARNING,简短日志格式,并将日志输出到控制台。
logging.basicConfig(level=logging.WARNING, 
                    format='%(asctime)s - %(levelname)s - %(message)s')

url = "https://example.com"

try:
    response = requests.get(url, timeout=10)
    response.raise_for_status()

except HTTPError as http_err:
    logging.warning(f"HTTP错误发生了:{http_err}")
except ConnectionError as conn_err:
    logging.warning(f"连接错误发生了:{conn_err}")
except Timeout as timeout_err:
    logging.warning(f"请求超时了:{timeout_err}")
except RequestException as err:
    logging.warning(f"出现了请求错误:{err}")
else:
    print("请求成功完成。")

案例2:处理Beautiful Soup可能的异常

from bs4 import BeautifulSoup
import logging

# 配置logging,设置日志级别并输出到控制台
logging.basicConfig(level=logging.WARNING, 
                    format='%(asctime)s - %(levelname)s - %(message)s')

html_doc = """
This is title
"""

try:
    soup = BeautifulSoup(html_doc, "html.parser")
    title_text = soup.title.text

except AttributeError as e:
    # 如果BeautifulSoup尝试访问不存在的属性会抛出这个错误
	logging.warning(f"未能找到属性。错误:{e}")

except Exception as e:
    # 通用异常捕获,可能在解析HTML文档时遇到其他没有预料到的错误
	logging.error(f"发生错误:{e}")

else:
    print(f"文档的标题是:{title_text}")
  1. 对于Beautiful Soup,在操作前应检查返回对象是否为预期的标签,可以简单通过条件语句实现,例如:if soup.title:
  2. 尝试将异常处理模块化,以便在多处爬虫代码中重复使用。例如,可能为网络请求定义一个函数,并以此处理所有网络请求相关的异常。
  3. 针对预期可能发生的错误,可以定义明确的异常处理逻辑,如网络信号弱时重试操作等。
  4. 最重要的是编写清晰、易读且易于维护的代码,异常处理也要紧密跟随这个准则。

使用多线程和并发

当处理的网页数量庞大时,这一过程往往相当耗时。在Python中通过threading和concurrent.futures模块将Beautiful Soup的使用并行化,显著提升效率。

多线程基础

threading模块允许我们运行多个线程(即任务)来执行代码。在网络请求和HTML解析任务中,多线程能有效减少等待I/O操作(如网络请求)的时间。

使用concurrent.futures简化多线程

concurrent.futures模块提供了一种高级别的异步执行机制,通过ThreadPoolExecutor类我们可以非常方便地创建线程池。

案例一:简单多线程HTML请求和解析

我们首先摆脱繁杂的线程管理,并且用concurrent.futures来提升我们代码的执行速度:
from concurrent.futures import ThreadPoolExecutor
import requests
from bs4 import BeautifulSoup

urls = [
  'https://example.com',
  'https://example.org',
  'https://example.net',
]

def fetch_and_parse(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    title = soup.title.text
    return title

with ThreadPoolExecutor(max_workers=5) as executor:
    futures = [executor.submit(fetch_and_parse, url) for url in urls]
    for future in concurrent.futures.as_completed(futures):
        try:
            data = future.result()
            print(data)
        except Exception as exc:
            print(f"生成异常: {url} {exc}")
在这个案例中,ThreadPoolExecutor创建了一个线程池,异步地请求网页并解析标题标签

案例二:并发实现细粒度Html元素处理

如果网页数据解析涉及大量细致的处理,我们进一步地将Html元素的收集和处理分摊到不同线程去执行。
from concurrent.futures import ThreadPoolExecutor
import requests
from bs4 import BeautifulSoup

url = "https://example.com/products"

def parse_html(html):
    soup = BeautifulSoup(html, "html.parser")
    products = soup.find_all('li', {'class': 'product'})
    return [product.text for product in products]

def get_html(url):
    response = requests.get(url)
    return response.text

with ThreadPoolExecutor() as executor:
    html = executor.submit(get_html, url).result()
    product_texts = executor.submit(parse_html, html).result()

print(product_texts)
executor.submit()负责提交任务给线程池,此处分别用独立的线程下载HTML文档和解析文档中的产品列表。

案例三:避免全局解释器锁(GIL)带来的影响

虽然threading在I/O密集型任务中表现良好,但GIL(Global Interpreter Lock,全局解释器锁)可能会在某些情况下影响效率。此时,我们可以考虑使用 Python 的 multiprocessing 模块。
from multiprocessing.pool import ThreadPool
import requests
from bs4 import BeautifulSoup

urls = ["https://example.com", "https://example.org"]

def fetch_and_parse(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    return soup.title.text

if __name__ == '__main__':
    pool = ThreadPool(processes=2)
    results = pool.map(fetch_and_parse, urls)
    pool.close()
    pool.join()
    for title in results:
        print(title)
通过创建一个基于进程的ThreadPool来完成并发执行,这样就可以绕过GIL的限制,适用于任何数目的cpu密集型和I/O密集型任务。

运用多线程和并发可以大量缩短网页数据处理的时间,对于领域内从事数据采集和分析的从业者来说,这是提升工作效率的重要方法。希望通过本文,您能利用Python提供的并发工具,更高效地实现爬虫和数据解析任务。

你可能感兴趣的:(15天玩转高级python,服务器,运维)