如何提升使用request模块发送GET请求获取并解析网页内容的速度
爬虫请求与解析速度优化方案
你感知到的tree = html.fromstring(page.content)行卡顿,大概率是请求IO等待+大页面解析冗余共同导致的,可从以下几个方向优化:
一、请求侧优化(解决80%以上的耗时问题)
- 复用TCP会话:不要每次调用
requests.get都新建连接,改用requests.Session()复用同域名下的连接,减少TCP握手、TLS认证开销,速度可提升30%以上。 - 并行爬取:原代码是串行请求,一个URL请求结束才会发起下一个,爬虫属于IO密集型任务,改用多线程/异步IO并行发起请求,可根据站点并发限制调整同时请求数,速度可提升数倍到数十倍。
- 开启压缩传输:显式指定请求头
Accept-Encoding: gzip, deflate(requests默认已携带,可确认未被修改),降低响应体的传输体积,减少下载耗时。 - 增加缓存:如果有重复爬取相同URL的场景,可接入缓存逻辑,爬过的内容直接读本地缓存,无需重复请求。
二、解析侧优化(针对html.fromstring卡顿问题)
lxml本身是Python生态中最快的HTML解析库之一,卡顿主要是解析冗余内容、遍历范围太大导致:
- 定制解析器,跳过无用内容:创建解析器时配置参数,跳过注释、处理指令等无用内容,同时开启容错模式,减少无效解析开销:
# 全局只需要初始化一次解析器,不要放在循环里 fast_parser = html.HTMLParser( recover=True, # 容错,避免不规范HTML报错 remove_comments=True, # 移除HTML注释 remove_pis=True, # 移除处理指令 no_network=True # 禁止加载外部资源 ) # 解析时传入定制解析器 tree = html.fromstring(page.content, parser=fast_parser)
- 缩小XPath遍历范围:不要每次都从根节点全文档搜索,先定位到父节点再搜索子节点,减少遍历的DOM节点数:
# 原写法是多次全文档扫描 # 优化后先拿父节点 table_headers = tree_1.xpath("//tr[@id = 'tableHeader']")[0] race_number = table_headers.xpath("./td[1]/text()") Distance = table_headers.xpath("./td[3]/text()") TGR_Grade = table_headers.xpath("./td[4]/text()") tip_rows = tree_1.xpath("//tbody/tr[@class='fieldsTableRow raceTipsRow']//div")[0] TGR1 = tip_rows.xpath("./span[1]/text()") TGR2 = tip_rows.xpath("./span[2]/text()") # 其余字段同理
- 规则简单的场景直接用正则匹配:如果提取的字段规则非常固定,可直接对
page.content的字符串做正则匹配,跳过DOM树构建流程,解析速度可提升50%以上。
三、其他代码优化
- 替换重依赖调用:不要用
pd.to_datetime('now').year获取当前年份,改用标准库datetime实现,开销降低一个数量级:
from datetime import date year = date.today().year
- 移除无用代码:原代码中
clean_title变量定义后未使用,可直接删除。
优化后参考代码(核心部分)
import lxml from lxml import html import requests import re import pandas as pd from requests.exceptions import ConnectionError from datetime import date from concurrent.futures import ThreadPoolExecutor, as_completed # 全局初始化复用对象 session = requests.Session() # 加请求头模拟浏览器,避免被站点限流 session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", "Accept-Encoding": "gzip, deflate" }) fast_parser = html.HTMLParser(recover=True, remove_comments=True, remove_pis=True, no_network=True) # 并发数可根据站点实际情况调整,建议不超过10避免被封 MAX_WORKERS = 8 greyhound_url = 'http://thegreyhoundrecorder.com.au/form-guides/' def get_page(url): page = session.get(url) tree = html.fromstring(page.content, parser=fast_parser) my_list = tree.xpath('//tbody/tr/td[2]/a/@href') print('Length of all links = ', len(my_list)) my_url = [page.url.split('/form-guides')[0] + str(s) for s in my_list] return my_url def parse_single_page(t): page_detail = session.get(t) tree_1 = html.fromstring(page_detail.content, parser=fast_parser) title = ''.join(tree_1.xpath('//div/h1[@class="title"]/text()')) # 缩小XPath扫描范围 try: table_header = tree_1.xpath("//tr[@id = 'tableHeader']")[0] race_number = table_header.xpath("./td[1]/text()") Distance = table_header.xpath("./td[3]/text()") TGR_Grade = table_header.xpath("./td[4]/text()") tip_row = tree_1.xpath("//tbody/tr[@class='fieldsTableRow raceTipsRow']//div")[0] TGR1 = tip_row.xpath("./span[1]/text()") TGR2 = tip_row.xpath("./span[2]/text()") TGR3 = tip_row.xpath("./span[3]/text()") TGR4 = tip_row.xpath("./span[4]/text()") except IndexError: # 处理结构异常的页面 return None Track = title.split(' ')[0].strip() date_str = title.split('-')[1].strip() year = date.today().year race_date = pd.to_datetime(date_str + ' ' + str(year)).strftime('%d/%m/%Y') new_rn = [] for number in race_number: match = re.search(r'^(.).*?(\d+)$', number) if match: new_rn.append(match.group(1) + match.group(2)) return (race_date,Track,new_rn,Distance,TGR_Grade,TGR1,TGR2,TGR3,TGR4) def extract_data(my_url): new_list = [] try: with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: futures = [executor.submit(parse_single_page, t) for t in my_url] for future in as_completed(futures): res = future.result() if res: new_list.append(res) return new_list except ConnectionError as e: print('Connection error, connect to a stronger network or reload the page')
内容的提问来源于stack exchange,提问作者chuky pedro
相关产品推荐
相关产品推荐

