Python爬虫实战:requests_html与JSON数据处理技巧

1. 为什么选择requests_html与json这对黄金组合

在Python爬虫领域,requests_html库正逐渐成为新一代的轻量级抓取利器。与传统requests+BeautifulSoup组合相比,它最大的特点是内置了完整的HTML解析和JavaScript渲染能力。我在实际项目中验证过,对于90%的现代网页抓取需求,这个组合能提供开箱即用的解决方案。

requests_html的核心优势在于:

  • 自动处理动态渲染:通过集成Pyppeteer(Chromium的无头浏览器),能执行页面中的JavaScript代码
  • 类jQuery的选择器:支持CSS选择器和XPath双模式定位元素
  • 会话保持:自动管理cookies和headers,减少反爬困扰
  • 智能编码检测:自动处理网页编码问题,避免乱码

而json作为数据交换的事实标准,在爬虫中扮演着关键角色。我常遇到的情况是:

  1. 直接解析API返回的JSON数据
  2. 将抓取的HTML内容结构化后转为JSON存储
  3. 使用JSON作为中间格式进行数据清洗

需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。

2. 环境搭建与基础配置

2.1 安装注意事项

安装requests_html时常见的坑是依赖冲突。推荐使用虚拟环境:

bash复制python -m venv scrape_env
source scrape_env/bin/activate  # Linux/Mac
scrape_env\Scripts\activate     # Windows
pip install requests-html pyppeteer

重要提示:首次运行会自动下载Chromium(约200MB),国内用户建议配置镜像源:

python复制import os
os.environ['PYPPETEER_DOWNLOAD_HOST'] = 'https://npm.taobao.org/mirrors'

2.2 基础请求模板

这是我经过多个项目验证的稳健请求模板:

python复制from requests_html import HTMLSession

session = HTMLSession(browser_args=["--no-sandbox", "--disable-setuid-sandbox"])

def safe_get(url, retry=3):
    for i in range(retry):
        try:
            r = session.get(url, timeout=10)
            r.raise_for_status()
            if len(r.content) > 500:  # 过滤无效响应
                return r
        except Exception as e:
            print(f"Attempt {i+1} failed: {str(e)}")
            if i == retry - 1:
                raise
            time.sleep(2**i)  # 指数退避

关键参数说明:

  • browser_args:禁用沙盒模式可解决部分Linux系统的权限问题
  • 指数退避策略:2, 4, 8秒的间隔重试,符合爬虫伦理
  • 内容长度检查:避免返回空白页或验证码页面

3. 实战解析:从HTML到结构化JSON

3.1 动态页面渲染技巧

处理SPA(单页应用)时,必须使用render方法:

python复制r = session.get('https://dynamic-site.com')
r.html.render(sleep=2, keep_page=True, scrolldown=3)

参数优化建议:

  • sleep:根据网络状况调整,2秒适用于大部分场景
  • scrolldown:模拟滚动触发懒加载,数字代表滚动次数
  • keep_page:保持页面对象用于后续操作

3.2 数据提取四步法

这是我总结的高效提取流程:

  1. 定位容器:先用Chrome开发者工具观察整体结构
python复制items = r.html.find('.product-list > li')
  1. 多模式选择:CSS选择器和XPath混合使用
python复制name = item.find('h3', first=True).text
price = item.xpath('//span[@class="price"]/text()')[0]
  1. 属性提取:注意相对路径和绝对路径的区别
python复制image_url = item.absolute_links.pop()  # 转换相对链接
  1. 数据清洗:使用正则处理特殊字符
python复制import re
clean_price = re.sub(r'[^\d.]', '', price)

3.3 转为JSON的最佳实践

结构化转换时推荐使用Python标准库的json模块:

python复制import json

data = []
for item in items:
    data.append({
        "name": name,
        "price": float(clean_price),
        "image": image_url,
        "timestamp": datetime.now().isoformat()
    })

with open('output.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

注意事项:

  • ensure_ascii=False:保证中文正常显示
  • 添加时间戳:便于后续增量爬取
  • 类型转换:提前处理数字、日期等特殊格式

4. 高级技巧与反反爬策略

4.1 请求头优化方案

这是我收集的现代浏览器指纹头:

python复制headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    "Accept-Encoding": "gzip, deflate, br",
    "Referer": "https://www.google.com/",
    "Sec-Ch-Ua": '"Google Chrome";v="91", "Chromium";v="91", ";Not A Brand";v="99"'
}

4.2 智能延迟系统

根据响应状态动态调整请求频率:

python复制class SmartThrottle:
    def __init__(self, base_delay=1.0):
        self.base = base_delay
        self.factor = 1
        
    def adjust(self, response):
        if response.status_code == 429:
            self.factor *= 2
        elif self.factor > 1 and random.random() > 0.7:
            self.factor *= 0.9
            
    def wait(self):
        time.sleep(self.base * self.factor + random.uniform(0, 0.5))

4.3 验证码处理方案

遇到验证码时的应急方案:

  1. 自动识别简单验证码:
python复制from pytesseract import image_to_string

captcha_img = r.html.find('#captcha', first=True)
if captcha_img:
    text = image_to_string(captcha_img)
  1. 人工干预模式:
python复制if "验证码" in r.text:
    input("请在浏览器中手动处理验证码后按回车继续:")
    r = session.get(r.url)  # 复用当前会话

5. 数据存储与管道设计

5.1 增量爬取架构

使用MD5校验实现增量存储:

python复制import hashlib

def get_content_hash(content):
    return hashlib.md5(content.encode('utf-8')).hexdigest()

seen = set()
for item in data:
    item_hash = get_content_hash(json.dumps(item))
    if item_hash not in seen:
        seen.add(item_hash)
        # 存储逻辑...

5.2 多格式输出管道

扩展存储选项:

python复制def save_data(data, format='json'):
    if format == 'json':
        with open('data.json', 'a') as f:
            json.dump(data, f)
    elif format == 'csv':
        pd.DataFrame(data).to_csv('data.csv', mode='a')
    elif format == 'mongodb':
        client = pymongo.MongoClient()
        db = client['scrape_db']
        db.items.insert_many(data)

5.3 错误处理机制

健壮的生产级处理方案:

python复制from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1))
def process_item(item):
    try:
        # 处理逻辑...
        return result
    except Exception as e:
        logger.error(f"处理失败: {str(e)}")
        raise

6. 性能优化实战

6.1 异步请求模式

利用asyncio提升吞吐量:

python复制async def async_fetch(url):
    async with HTMLSession() as session:
        r = await session.get(url)
        await r.html.arender()
        return r

async def main(urls):
    tasks = [async_fetch(url) for url in urls]
    return await asyncio.gather(*tasks)

6.2 内存优化技巧

处理大页面时的内存管理:

python复制with HTMLSession() as session:
    r = session.get(url, stream=True)
    for chunk in r.iter_content(chunk_size=1024):
        process(chunk)  # 流式处理
    r.close()  # 显式释放资源

6.3 分布式扩展思路

使用Redis队列实现任务分发:

python复制import redis
from rq import Queue

conn = redis.Redis()
q = Queue(connection=conn)

def worker_task(url):
    # 爬取逻辑...
    return data

job = q.enqueue(worker_task, url)

7. 真实项目经验分享

在最近一个电商价格监控项目中,我发现几个关键点:

  1. 元素定位陷阱:某些网站会随机生成class名称,此时应该:
python复制# 改用稳定的结构定位
price = item.find('[itemprop="price"]', first=True).attrs['content']
  1. JSON直取技巧:很多网站会将数据藏在