1. 为什么选择lxml和XPath解析HTML第一次接触网页数据抓取时我试过用正则表达式匹配HTML标签结果被各种嵌套结构和属性变化折磨得够呛。直到发现lxml这个神器配合XPath语法解析效率直接提升10倍不止。lxml是Python生态中性能顶尖的HTML/XML解析库底层用C语言实现。实测解析一个2MB的HTML文件BeautifulSoup需要300毫秒而lxml仅需30毫秒。更关键的是XPath这种路径表达式语法能像文件系统路径一样直观定位节点。比如要获取所有商品价格直接写//span[classprice]就行。最近帮朋友抓取某电商平台数据时遇到个典型场景页面包含200个商品卡片每个卡片里有商品名、价格、评分等数据但DOM结构非常复杂。用XPath只需要几行代码titles tree.xpath(//div[contains(class,product-card)]//h3/text()) prices tree.xpath(//div[contains(class,price-box)]/span[1]/text())这种场景下XPath的contains、text()等函数特别实用。相比正则表达式XPath代码可读性更高后期维护也方便。有个经验之谈当网页结构经常变动时用class属性定位比用标签层级更稳定。2. 环境搭建与基础操作2.1 快速安装指南新手最容易卡在环境配置上。最近在Windows 11上实测用清华镜像源安装最稳定pip install lxml -i https://pypi.tuna.tsinghua.edu.cn/simple如果遇到C库缺失错误常见于Linux需要先安装开发工具链。在Ubuntu上可以这样操作sudo apt-get install libxml2-dev libxslt-dev python3-dev2.2 HTML加载的三种姿势我常用的HTML加载方式有三种各有适用场景从字符串加载适合快速测试from lxml import etree html divp测试内容/p/div tree etree.HTML(html) # 自动补全缺失标签从本地文件加载适合静态页面parser etree.HTMLParser(encodingutf-8) tree etree.parse(page.html, parserparser)网络请求加载实战最常用import requests resp requests.get(https://example.com) tree etree.HTML(resp.content) # 注意用content而非text踩过的一个坑有些网站编码声明不准确导致中文乱码。这时候可以手动指定编码html resp.content.decode(gbk) # 针对国内某些网站 tree etree.HTML(html)3. XPath核心语法精要3.1 节点定位的六种武器经过上百个页面的实战我总结出最常用的6种定位方式绝对路径结构稳定时用tree.xpath(/html/body/div[2]/section/ul/li)相对路径更灵活tree.xpath(//ul/li/div[classitem])属性过滤最常用技巧tree.xpath(//a[href/detail?id123])文本匹配抓取特定内容tree.xpath(//span[text()现货])模糊查询应对动态classtree.xpath(//div[contains(class, product-)])轴定位复杂关系处理tree.xpath(//div[contains(text(),价格)]/following-sibling::span)3.2 实战中的进阶技巧去年抓取新闻网站时遇到个棘手问题发布时间藏在># 提取时间戳并转换为日期对象 timestamps tree.xpath(//div[classnews-item]/data-time) from datetime import datetime dates [datetime.fromtimestamp(int(ts)) for ts in timestamps]另一个实用技巧是处理动态加载的AJAX内容。比如某网站用滚动加载更多商品可以通过分析XHR请求直接获取JSON数据import json data tree.xpath(//script[typeapplication/json]/text())[0] products json.loads(data)[productList]4. 电商数据抓取实战4.1 商品列表解析以某电商平台为例商品列表页通常包含以下元素商品名称价格可能有原价和促销价评价数量商品链接对应的XPath选择器可以这样写products [] items tree.xpath(//div[contains(class,item-container)]) for item in items: product { name: item.xpath(.//h3/text())[0].strip(), price: item.xpath(.//span[classprice]/text())[0], reviews: int(item.xpath(.//a[contains(href,reviews)]/text())[0].split()[0]), url: item.xpath(.//a[contains(href,product)]/href)[0] } products.append(product)注意相对路径前的.很重要表示在当前节点下查找否则会变成全局搜索。4.2 分页处理技巧分页抓取时最容易遇到反爬。我的经验是优先找page这样的明确参数没有明显分页时观察加载更多按钮的AJAX请求控制请求频率加随机延迟import time import random for page in range(1, 6): url fhttps://example.com/list?page{page} resp requests.get(url, headers{User-Agent: Mozilla/5.0}) time.sleep(random.uniform(1, 3)) # 随机延迟 # 解析逻辑...5. 性能优化与异常处理5.1 提升解析速度的三板斧预编译XPath频繁使用的XPath应该预先编译from lxml import etree find_title etree.XPath(//h1/text()) find_title(tree) # 比直接xpath()快30%减少DOM操作一次性获取所有需要的数据避免重复解析# 不好的做法 titles [t.xpath(./text())[0] for t in tree.xpath(//h2)] # 好的做法 titles tree.xpath(//h2/text())使用多线程对于大量页面可以用线程池并行处理from concurrent.futures import ThreadPoolExecutor def parse_page(html): tree etree.HTML(html) return tree.xpath(//title/text())[0] with ThreadPoolExecutor(4) as executor: results executor.map(parse_page, html_list)5.2 必须掌握的异常处理网页解析中最常遇到的三种异常节点不存在XPath返回空列表price tree.xpath(//span[classprice]/text()) if not price: # 处理缺失价格的情况 price N/A编码问题特别是GBK与UTF-8混用try: html resp.content.decode(utf-8) except UnicodeDecodeError: html resp.content.decode(gbk, errorsignore)反爬机制封IP、验证码等if 验证码 in resp.text: raise Exception(触发反爬需要处理验证码)6. 数据清洗与存储6.1 清洗脏数据的五种武器空白字符处理text 价格¥199 clean_text text.strip().replace(价格, )多余标签去除from lxml.html.clean import Cleaner cleaner Cleaner(styleTrue, scriptsTrue) clean_html cleaner.clean_html(raw_html)中文数字转换def chn_to_num(text): mapping {一:1, 两:2, 三:3} return int(.join(str(mapping.get(c, c)) for c in text if c.isdigit()))价格提取正则import re price re.search(r¥(\d\.?\d*), text).group(1)日期标准化from dateutil.parser import parse date parse(2023年7月15日).strftime(%Y-%m-%d)6.2 存储方案选择根据数据量大小我常用的三种存储方式CSV小数据量import csv with open(products.csv, w, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesproducts[0].keys()) writer.writeheader() writer.writerows(products)SQLite中等数据量import sqlite3 conn sqlite3.connect(data.db) conn.execute(CREATE TABLE IF NOT EXISTS products (name TEXT, price REAL, reviews INT)) conn.executemany(INSERT INTO products VALUES (:name,:price,:reviews), products)MongoDB大数据量from pymongo import MongoClient client MongoClient() db client[ecommerce] db.products.insert_many(products)7. 反反爬策略实战最近帮客户抓取某房产网站时遇到几个典型反爬措施User-Agent检测解决方案是轮换UAheaders { User-Agent: random.choice([ Mozilla/5.0 (Windows NT 10.0; Win64; x64), Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ]) }请求频率限制需要添加延迟time.sleep(random.uniform(0.5, 2))IP封禁建议使用代理IP池proxies { http: http://user:passproxy_ip:port, https: http://user:passproxy_ip:port }动态参数需要解析JavaScript生成的tokentoken re.search(rtoken: (.?), resp.text).group(1)8. 项目经验分享去年用lxml完成的一个电商价格监控项目核心逻辑是每天定时抓取目标商品页面解析价格和库存状态价格变动时发送邮件提醒关键代码片段def check_price(): tree etree.HTML(fetch_page()) price float(tree.xpath(//meta[itempropprice]/content)[0]) if price target_price: send_alert_email(product[name], price) schedule.every().day.at(09:00).do(check_price) while True: schedule.run_pending() time.sleep(60)这个项目运行半年后帮客户节省了15%的采购成本。最大的收获是XPath选择器要定期维护因为电商网站前端经常改版。建议把关键XPath存在配置文件中方便随时调整。