前言:
提高抓取和解析效率的根本还是在于发送请求;如何从这个方面进行效率提升呢?
深入使用requests.Session()
深入使用requests.Session()
1.持久连接:
当使用 requests.Session() 时,连接会话中所有的请求将优先使用一个TCP连接,即"持久连接",这样即使你发起多次对同一主机的独立请求,Session 实例会重用底层的连接,从而降低握手的开销。
python
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 对象允许你自定义一些请求细节,如头信息和鉴权凭证等,并在之后的请求中保持这些设置,减少了重复代码的编写。
python
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的会话变量都可以一次设立,然后按预期工作。
python
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:处理网络请求异常
python
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可能的异常
python
from bs4 import BeautifulSoup
import logging
# 配置logging,设置日志级别并输出到控制台
logging.basicConfig(level=logging.WARNING,
format='%(asctime)s - %(levelname)s - %(message)s')
html_doc = """
<html><title>This is title</title></html>
"""
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}")
对于Beautiful Soup,在操作前应检查返回对象是否为预期的标签,可以简单通过条件语句实现,例如:if soup.title:
尝试将异常处理模块化,以便在多处爬虫代码中重复使用。例如,可能为网络请求定义一个函数,并以此处理所有网络请求相关的异常。
针对预期可能发生的错误,可以定义明确的异常处理逻辑,如网络信号弱时重试操作等。
最重要的是编写清晰、易读且易于维护的代码,异常处理也要紧密跟随这个准则。
使用多线程和并发
当处理的网页数量庞大时,这一过程往往相当耗时。在Python中通过threading和concurrent.futures模块将Beautiful Soup的使用并行化,显著提升效率。
多线程基础
threading模块允许我们运行多个线程(即任务)来执行代码。在网络请求和HTML解析任务中,多线程能有效减少等待I/O操作(如网络请求)的时间。
使用concurrent.futures简化多线程
concurrent.futures模块提供了一种高级别的异步执行机制,通过ThreadPoolExecutor类我们可以非常方便地创建线程池。
案例一:简单多线程HTML请求和解析
我们首先摆脱繁杂的线程管理,并且用concurrent.futures来提升我们代码的执行速度:
python
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元素的收集和处理分摊到不同线程去执行。
python
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 模块。
python
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)