在前几篇文章中,我们已经介绍了BeautifulSoup和XPath两种强大的网页解析工具。本篇文章将介绍另一个优秀的网页解析库:PyQuery。PyQuery是一个模仿jQuery语法的Python库,让我们能够用熟悉的CSS选择器语法来解析和操作HTML文档。
一、PyQuery简介
PyQuery是一个强大而优雅的HTML解析库,它将jQuery的语法和思想带入Python世界。使用PyQuery的主要优势包括:
- 熟悉的语法:如果你熟悉jQuery,那么使用PyQuery将非常自然
- 简洁优雅:代码简洁,表达能力强
- CSS选择器:支持完整的CSS3选择器语法
- 链式调用:可以链式调用方法,使代码更简洁
- DOM操作:不仅可以提取数据,还能修改DOM结构
PyQuery结合了BeautifulSoup的简洁性和lxml的高性能,是一个非常值得掌握的网页解析工具。
二、安装PyQuery
首先,我们需要安装PyQuery库:
bash
pip install pyquery
安装成功后会看到类似以下输出:
sql
Collecting pyquery
Downloading pyquery-2.0.0-py3-none-any.whl (22 kB)
Collecting cssselect>=1.2.0
Downloading cssselect-1.2.0-py2.py3-none-any.whl (18 kB)
Requirement already satisfied: lxml>=4.6.0 in c:\users\user\appdata\local\programs\python\python39\lib\site-packages (from pyquery) (4.9.3)
Installing collected packages: cssselect, pyquery
Successfully installed cssselect-1.2.0 pyquery-2.0.0
PyQuery依赖于lxml库,如果你之前没有安装lxml,上面的命令会自动安装它。
三、PyQuery基础语法
1. 创建PyQuery对象
PyQuery可以从多种来源创建对象:
python
from pyquery import PyQuery as pq
# 从HTML字符串创建
html_str = "<html><body><div class='container'>内容</div></body></html>"
doc = pq(html_str)
print(doc)
# 从URL创建
doc = pq(url='https://www.example.com')
print(doc('title').text())
# 从文件创建
# doc = pq(filename='example.html')
# 从lxml对象创建
from lxml import etree
element = etree.HTML("<div>内容</div>")
doc = pq(element)
print(doc)
运行结果:
xml
<html><body><div class="container">内容</div></body></html>
Example Domain
<html><body><div>内容</div></body></html>
2. CSS选择器
PyQuery使用CSS选择器来查找元素,语法与jQuery完全一致:
python
from pyquery import PyQuery as pq
html = """
<div id="container">
<h1 class="title">PyQuery示例</h1>
<div class="content">
<p>这是第一段内容</p>
<p class="important">这是重要内容</p>
</div>
<ul id="menu">
<li><a href="/">首页</a></li>
<li><a href="/about">关于</a></li>
<li><a href="/contact" target="_blank">联系我们</a></li>
</ul>
</div>
"""
doc = pq(html)
# 选择所有div元素
divs = doc('div')
print(f"找到 {len(divs)} 个div元素")
# 通过ID选择
container = doc('#container')
print(f"容器标题: {container.find('h1').text()}")
# 通过class选择
content = doc('.content')
print(f"内容部分: {content.text()}")
# 组合选择器
important_p = doc('p.important')
print(f"重要段落: {important_p.text()}")
# 层级选择器
menu_links = doc('#menu a')
print(f"菜单链接数量: {len(menu_links)}")
# 直接子元素选择器
direct_children = doc('#menu > li')
print(f"直接子元素数量: {len(direct_children)}")
# 属性选择器
external_links = doc('a[target="_blank"]')
print(f"外部链接: {external_links.attr('href')}")
运行结果:
makefile
找到 2 个div元素
容器标题: PyQuery示例
内容部分: 这是第一段内容 这是重要内容
重要段落: 这是重要内容
菜单链接数量: 3
直接子元素数量: 3
外部链接: /contact
3. 操作元素
PyQuery提供了丰富的方法来获取和操作元素:
python
from pyquery import PyQuery as pq
html = """
<div class="container">
<h2 class="title">PyQuery操作示例</h2>
<div class="content">
<p>这是<b>加粗</b>文本和<a href="https://example.com">链接</a></p>
</div>
<div class="footer">页脚信息</div>
</div>
"""
doc = pq(html)
# 获取HTML内容
html_content = doc('.content').html()
print(f"HTML内容: {html_content}")
# 获取文本内容
text_content = doc('.content').text()
print(f"文本内容: {text_content}")
# 获取属性
link = doc('a')
link_url = link.attr('href')
print(f"链接URL: {link_url}")
# 或者使用这种方式
link_url = link.attr.href
print(f"链接URL: {link_url}")
# 获取所有类名
class_names = doc('.container').attr('class')
print(f"容器类名: {class_names}")
# 遍历元素
print("所有div内容:")
for div in doc('div').items():
print(f"- {div.text()}")
运行结果:
javascript
HTML内容: <p>这是<b>加粗</b>文本和<a href="https://example.com">链接</a></p>
文本内容: 这是加粗文本和链接
链接URL: https://example.com
链接URL: https://example.com
容器类名: container
所有div内容:
- PyQuery操作示例 这是加粗文本和链接 页脚信息
- 这是加粗文本和链接
- 页脚信息
4. 过滤和遍历
PyQuery提供了类似jQuery的过滤和遍历方法:
python
from pyquery import PyQuery as pq
html = """
<ul id="fruits">
<li class="apple">苹果</li>
<li class="banana">香蕉</li>
<li class="orange important">橙子</li>
<li class="pear">梨</li>
<li class="grape important">葡萄</li>
</ul>
<p>其他内容</p>
"""
doc = pq(html)
# eq() - 获取指定索引的元素
first_item = doc('#fruits li').eq(0)
print(f"第一个水果: {first_item.text()}")
# filter() - 根据选择器过滤
important_items = doc('#fruits li').filter('.important')
print(f"重要水果: {', '.join(item.text() for item in important_items.items())}")
# find() - 查找后代元素
fruits = doc('#fruits').find('li')
print(f"找到 {len(fruits)} 个水果")
# children() - 获取子元素
list_items = doc('ul').children()
print(f"列表项数量: {len(list_items)}")
# parent() - 获取父元素
parent_element = doc('li').eq(0).parent()
print(f"父元素标签: {parent_element[0].tag}")
# siblings() - 获取兄弟元素
other_fruits = doc('li.apple').siblings()
print(f"其他水果数量: {len(other_fruits)}")
# each() - 遍历元素
fruits_list = []
def collect_fruits(i, elem):
item = pq(elem)
fruits_list.append(item.text())
doc('li').each(collect_fruits)
print(f"水果列表: {fruits_list}")
运行结果:
less
第一个水果: 苹果
重要水果: 橙子, 葡萄
找到 5 个水果
列表项数量: 5
父元素标签: ul
其他水果数量: 4
水果列表: ['苹果', '香蕉', '橙子', '梨', '葡萄']
四、在Python爬虫中使用PyQuery
下面我们通过一个完整的例子来展示如何在爬虫中使用PyQuery:
python
import requests
from pyquery import PyQuery as pq
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
def fetch_webpage(url):
"""获取网页内容"""
try:
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'
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status() # 检查请求是否成功
return response.text
except Exception as e:
logging.error(f"获取网页失败: {e}")
return None
def parse_news_list(html):
"""使用PyQuery解析新闻列表"""
if not html:
return []
doc = pq(html)
news_list = []
# 这里使用一个模拟的HTML结构作为示例
# 实际使用时需要根据目标网页调整选择器
items = doc('.news-item')
logging.info(f"找到 {len(items)} 条新闻")
for i, item in enumerate(items.items()): # 注意使用items()方法
try:
# 提取标题
title = item.find('.title').text()
# 提取链接
link = item.find('a').attr('href')
# 提取摘要
summary = item.find('.summary').text()
# 提取日期
date = item.find('.date').text()
news_list.append({
'title': title,
'link': link,
'summary': summary,
'date': date
})
except Exception as e:
logging.error(f"解析第 {i+1} 条新闻时出错: {e}")
return news_list
# 模拟HTML用于测试
mock_html = """
<div class="news-container">
<div class="news-item">
<h3 class="title">Python 3.10发布</h3>
<a href="https://example.com/news/python-3-10">阅读详情</a>
<p class="summary">Python 3.10带来了许多新特性,包括更好的错误信息和模式匹配</p>
<span class="date">2023-01-15</span>
</div>
<div class="news-item">
<h3 class="title">PyQuery: jQuery风格的Python HTML解析器</h3>
<a href="https://example.com/news/pyquery-intro">阅读详情</a>
<p class="summary">PyQuery让解析HTML变得简单而优雅</p>
<span class="date">2023-01-10</span>
</div>
<div class="news-item">
<h3 class="title">网络爬虫最佳实践</h3>
<a href="https://example.com/news/web-scraping-best-practices">阅读详情</a>
<p class="summary">如何编写高效且负责任的网络爬虫</p>
<span class="date">2023-01-05</span>
</div>
</div>
"""
def main():
"""主函数"""
# 使用模拟HTML进行测试
news_list = parse_news_list(mock_html)
print("\n===== 新闻列表 =====")
for i, news in enumerate(news_list, 1):
print(f"{i}. {news['title']} [{news['date']}]")
print(f" 链接: {news['link']}")
print(f" 摘要: {news['summary']}")
print("-" * 50)
if __name__ == "__main__":
main()
运行结果:
markdown
2023-07-20 15:23:47,652 - INFO: 找到 3 条新闻
===== 新闻列表 =====
1. Python 3.10发布 [2023-01-15]
链接: https://example.com/news/python-3-10
摘要: Python 3.10带来了许多新特性,包括更好的错误信息和模式匹配
--------------------------------------------------
2. PyQuery: jQuery风格的Python HTML解析器 [2023-01-10]
链接: https://example.com/news/pyquery-intro
摘要: PyQuery让解析HTML变得简单而优雅
--------------------------------------------------
3. 网络爬虫最佳实践 [2023-01-05]
链接: https://example.com/news/web-scraping-best-practices
摘要: 如何编写高效且负责任的网络爬虫
--------------------------------------------------
五、实际案例:解析百度热搜榜
我们可以使用PyQuery解析百度热搜榜,就像之前使用XPath和BeautifulSoup那样:
python
from pyquery import PyQuery as pq
import logging
import requests
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
def parse_baidu_hot_search_pyquery(html_file=None):
"""使用PyQuery解析百度热搜榜HTML"""
try:
if html_file:
# 从文件读取HTML
with open(html_file, "r", encoding="utf-8") as f:
html_content = f.read()
else:
# 实时获取百度热搜
url = "https://top.baidu.com/board?tab=realtime"
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"
}
response = requests.get(url, headers=headers)
html_content = response.text
# 保存到文件以备后用
with open("baidu_hot_search_pyquery.html", "w", encoding="utf-8") as f:
f.write(html_content)
logging.info("开始使用PyQuery解析百度热搜榜HTML...")
# 创建PyQuery对象
doc = pq(html_content)
# 使用CSS选择器找到热搜项元素
# 注意:以下选择器基于当前百度热搜页面的结构,如果页面结构变化,可能需要更新
hot_items = doc('.category-wrap_iQLoo')
if not hot_items:
logging.warning("未找到热搜项,可能页面结构已变化,请检查HTML内容和CSS选择器")
return []
logging.info(f"找到 {len(hot_items)} 个热搜项")
# 提取每个热搜项的数据
hot_search_list = []
for index, item in enumerate(hot_items.items(), 1):
try:
# 提取标题
title = item.find('.c-single-text-ellipsis').text().strip()
title = title if title else "未知标题"
# 提取热度(如果有)
hot_value = item.find('.hot-index_1Bl1a').text().strip()
hot_value = hot_value if hot_value else "未知热度"
# 提取排名
rank = index
hot_search_list.append({
"rank": rank,
"title": title,
"hot_value": hot_value
})
except Exception as e:
logging.error(f"解析第 {index} 个热搜项时出错: {e}")
logging.info(f"成功解析 {len(hot_search_list)} 个热搜项")
return hot_search_list
except Exception as e:
logging.error(f"解析百度热搜榜时出错: {e}")
return []
def display_hot_search(hot_list):
"""展示热搜榜数据"""
if not hot_list:
print("没有获取到热搜数据")
return
print("\n===== 百度热搜榜 (PyQuery解析) =====")
print("排名\t热度\t\t标题")
print("-" * 50)
for item in hot_list:
print(f"{item['rank']}\t{item['hot_value']}\t{item['title']}")
if __name__ == "__main__":
# 尝试从之前保存的文件加载,如果没有则直接获取
try:
hot_search_list = parse_baidu_hot_search_pyquery("baidu_hot_search.html")
except FileNotFoundError:
hot_search_list = parse_baidu_hot_search_pyquery()
display_hot_search(hot_search_list)
运行结果(实际结果可能会因时间而不同):
yaml
2023-07-20 15:35:12,854 - INFO: 开始使用PyQuery解析百度热搜榜HTML...
2023-07-20 15:35:13,021 - INFO: 找到 30 个热搜项
2023-07-20 15:35:13,124 - INFO: 成功解析 30 个热搜项
===== 百度热搜榜 (PyQuery解析) =====
排名 热度 标题
--------------------------------------------------
1 4522302 世界首个"人造子宫"获批临床试验
2 4498753 iPhone15全系新增按键
3 4325640 暑假别只顾玩 这些安全知识要牢记
4 4211587 李嘉诚家族被曝已移居英国
5 4109235 苹果公司已开始研发iPhone16
... ... ...
六、PyQuery高级用法
1. 链式调用
PyQuery的一个主要特点是支持链式调用,让代码更简洁:
python
from pyquery import PyQuery as pq
html = """
<div class="container">
<div class="article">
<h2 class="title">文章标题1</h2>
<h2>普通标题</h2>
<h2 class="important title">重要标题</h2>
<p>段落内容</p>
</div>
</div>
"""
doc = pq(html)
# 链式调用示例
titles = doc('.article').find('h2').filter('.important').text()
print(f"通过链式调用找到的标题: {titles}")
# 等价于
articles = doc('.article')
h2_elements = articles.find('h2')
important_h2 = h2_elements.filter('.important')
titles = important_h2.text()
print(f"通过分步调用找到的标题: {titles}")
运行结果:
makefile
通过链式调用找到的标题: 重要标题
通过分步调用找到的标题: 重要标题
2. DOM操作
PyQuery不仅可以提取数据,还可以修改DOM结构:
python
from pyquery import PyQuery as pq
html = """
<div class="container">
<a href="https://example.com">链接</a>
<div class="content">原始内容</div>
<div class="ads">广告内容</div>
</div>
"""
doc = pq(html)
# 添加类
doc('div').addClass('new-class')
print("添加类后:")
print(doc)
# 删除类
doc('div').removeClass('new-class')
print("\n删除类后:")
print(doc)
# 添加属性
doc('a').attr('target', '_blank')
print("\n添加属性后:")
print(doc('a'))
# 设置内容
doc('.content').html('<p>新内容</p>')
print("\n设置内容后:")
print(doc('.content'))
# 插入内容
doc('.container').append('<div class="footer">页脚</div>')
print("\n添加页脚后:")
print(doc)
# 移除元素
doc('.ads').remove()
print("\n移除广告后:")
print(doc)
运行结果:
javascript
添加类后:
<div class="container new-class">
<a href="https://example.com">链接</a>
<div class="content new-class">原始内容</div>
<div class="ads new-class">广告内容</div>
</div>
删除类后:
<div class="container">
<a href="https://example.com">链接</a>
<div class="content">原始内容</div>
<div class="ads">广告内容</div>
</div>
添加属性后:
<a href="https://example.com" target="_blank">链接</a>
设置内容后:
<div class="content"><p>新内容</p></div>
添加页脚后:
<div class="container">
<a href="https://example.com" target="_blank">链接</a>
<div class="content"><p>新内容</p></div>
<div class="ads">广告内容</div><div class="footer">页脚</div></div>
移除广告后:
<div class="container">
<a href="https://example.com" target="_blank">链接</a>
<div class="content"><p>新内容</p></div>
<div class="footer">页脚</div></div>
3. 使用伪类选择器
PyQuery支持CSS3的伪类选择器:
python
from pyquery import PyQuery as pq
html = """
<ul id="fruits">
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
<li>梨</li>
<li>葡萄</li>
</ul>
<div class="empty"></div>
<div>包含文本</div>
"""
doc = pq(html)
# 第一个元素
first_item = doc('li:first')
print(f"第一个水果: {first_item.text()}")
# 最后一个元素
last_item = doc('li:last')
print(f"最后一个水果: {last_item.text()}")
# 偶数索引的元素 (索引从0开始,所以是1,3,5...)
even_items = doc('li:even')
print(f"偶数索引水果: {', '.join(item.text() for item in even_items.items())}")
# 奇数索引的元素
odd_items = doc('li:odd')
print(f"奇数索引水果: {', '.join(item.text() for item in odd_items.items())}")
# 第n个元素
nth_item = doc('li:eq(2)') # 索引从0开始,这是第3个元素
print(f"第3个水果: {nth_item.text()}")
# 包含特定文本的元素
contains_items = doc('div:contains("包含文本")')
print(f"包含特定文本的div数量: {len(contains_items)}")
# 空元素
empty_elements = doc('div:empty')
print(f"空div数量: {len(empty_elements)}")
运行结果:
makefile
第一个水果: 苹果
最后一个水果: 葡萄
偶数索引水果: 苹果, 橙子, 葡萄
奇数索引水果: 香蕉, 梨
第3个水果: 橙子
包含特定文本的div数量: 1
空div数量: 1
4. 表单处理
PyQuery提供了处理HTML表单的特殊方法:
python
from pyquery import PyQuery as pq
html = """
<form id="login-form">
<input id="username" name="username" value="test_user">
<input id="password" name="password" type="password">
<input id="remember-me" name="remember" type="checkbox" checked>
<select id="role">
<option value="user">普通用户</option>
<option value="admin" selected>管理员</option>
<option value="guest">访客</option>
</select>
</form>
"""
doc = pq(html)
# 获取表单元素的值
username = doc('#username').val()
print(f"用户名: {username}")
# 设置表单元素的值
doc('#username').val('new_user')
print(f"修改后的用户名: {doc('#username').val()}")
# 检查复选框是否被选中
is_checked = doc('#remember-me').is_(':checked')
print(f"记住我选项是否选中: {is_checked}")
# 获取下拉框选中的值
selected_role = doc('#role option:selected')
print(f"选中的角色: {selected_role.text()} (值: {selected_role.val()})")
运行结果:
makefile
用户名: test_user
修改后的用户名: new_user
记住我选项是否选中: True
选中的角色: 管理员 (值: admin)
七、PyQuery与其他解析库的比较
让我们比较一下PyQuery与其他常用的解析库:
特性 | PyQuery | BeautifulSoup | XPath/lxml |
---|---|---|---|
语法风格 | jQuery风格 | Python原生风格 | XPath表达式 |
选择器 | CSS选择器 | 多种选择器 | XPath选择器 |
性能 | 良好(基于lxml) | 一般 | 优秀 |
内存占用 | 中等 | 较高 | 较低 |
易用性 | 非常好(对熟悉jQuery的人) | 很好 | 较复杂 |
DOM操作 | 丰富 | 有限 | 有限 |
向上遍历 | 支持 | 支持 | 支持 |
文档质量 | 一般 | 优秀 | 良好 |
代码比较
让我们看看同一个任务使用不同库的代码比较:
python
from pyquery import PyQuery as pq
from bs4 import BeautifulSoup
from lxml import etree
import time
html = """
<html>
<body>
<div class="content">
<h2 class="title">文章标题</h2>
<p>这是一段文本</p>
<a href="https://example.com">链接1</a>
<a href="https://example.org">链接2</a>
</div>
</body>
</html>
"""
# 提取所有链接
# 使用PyQuery
start_time = time.time()
doc = pq(html)
links_pq = [a.attr('href') for a in doc('a').items()]
pq_time = time.time() - start_time
print(f"PyQuery提取的链接: {links_pq}")
print(f"PyQuery耗时: {pq_time:.6f}秒")
# 使用BeautifulSoup
start_time = time.time()
soup = BeautifulSoup(html, 'lxml')
links_bs = [a.get('href') for a in soup.find_all('a')]
bs_time = time.time() - start_time
print(f"BeautifulSoup提取的链接: {links_bs}")
print(f"BeautifulSoup耗时: {bs_time:.6f}秒")
# 使用XPath/lxml
start_time = time.time()
html_element = etree.HTML(html)
links_xpath = html_element.xpath('//a/@href')
xpath_time = time.time() - start_time
print(f"XPath提取的链接: {links_xpath}")
print(f"XPath耗时: {xpath_time:.6f}秒")
# 查找特定元素
print("\n查找特定元素:")
# 使用PyQuery
start_time = time.time()
element_pq = doc('.content h2.title')
pq_time = time.time() - start_time
print(f"PyQuery找到的元素: {element_pq.text()}")
print(f"PyQuery耗时: {pq_time:.6f}秒")
# 使用BeautifulSoup
start_time = time.time()
element_bs = soup.select_one('.content h2.title')
bs_time = time.time() - start_time
print(f"BeautifulSoup找到的元素: {element_bs.text}")
print(f"BeautifulSoup耗时: {bs_time:.6f}秒")
# 使用XPath/lxml
start_time = time.time()
element_xpath = html_element.xpath('//div[@class="content"]//h2[@class="title"]')[0]
xpath_time = time.time() - start_time
print(f"XPath找到的元素: {element_xpath.text}")
print(f"XPath耗时: {xpath_time:.6f}秒")
运行结果:
makefile
PyQuery提取的链接: ['https://example.com', 'https://example.org']
PyQuery耗时: 0.001998秒
BeautifulSoup提取的链接: ['https://example.com', 'https://example.org']
BeautifulSoup耗时: 0.004999秒
XPath提取的链接: ['https://example.com', 'https://example.org']
XPath耗时: 0.000999秒
查找特定元素:
PyQuery找到的元素: 文章标题
PyQuery耗时: 0.000000秒
BeautifulSoup找到的元素: 文章标题
BeautifulSoup耗时: 0.000000秒
XPath找到的元素: 文章标题
XPath耗时: 0.000000秒
注意:实际性能可能因数据量和机器配置而异。上面的例子数据量太小,差异不明显。
八、如何构建正确的CSS选择器
在使用PyQuery时,编写正确的CSS选择器是关键。以下是一些实用技巧:
1. 使用浏览器开发者工具
现代浏览器的开发者工具可以帮助你生成CSS选择器:
- 在Chrome或Firefox中右键点击元素,选择"检查"
- 在元素面板中右键点击HTML代码,选择"Copy" > "Copy selector"
- 获取浏览器生成的CSS选择器
例如,Chrome可能生成这样的选择器:
less
#main-content > div.article > h2.title
2. 选择器优化策略
浏览器生成的选择器通常很长且脆弱,以下是一些优化技巧:
python
from pyquery import PyQuery as pq
html = """
<div id="main">
<div class="container">
<div id="content" class="article-content main-content">
<h2 class="title">文章标题</h2>
<p>内容...</p>
</div>
</div>
</div>
"""
doc = pq(html)
# 浏览器生成的复杂选择器 - 太具体,脆弱
complex_selector = '#main > div.container > div#content.article-content.main-content > h2.title'
print(f"复杂选择器找到: {doc(complex_selector).text()}")
# 优化策略1: 使用ID - 最快且可靠
id_selector = '#content h2'
print(f"ID选择器找到: {doc(id_selector).text()}")
# 优化策略2: 使用类名 - 通常比标签更稳定
class_selector = '.article-content .title'
print(f"类选择器找到: {doc(class_selector).text()}")
# 优化策略3: 使用属性 - 当没有好的ID或类时
attr_selector = 'div[class*="content"] h2'
print(f"属性选择器找到: {doc(attr_selector).text()}")
# 优化策略4: 避免过深嵌套
simple_selector = '.title' # 如果类名是唯一的
print(f"简单选择器找到: {doc(simple_selector).text()}")
运行结果:
makefile
复杂选择器找到: 文章标题
ID选择器找到: 文章标题
类选择器找到: 文章标题
属性选择器找到: 文章标题
简单选择器找到: 文章标题
3. 选择器测试
在编写爬虫前,最好先测试你的选择器:
python
from pyquery import PyQuery as pq
html = """
<div class="container">
<div class="item" id="item1">项目1 <span class="price">¥100</span></div>
<div class="item" id="item2">项目2 <span class="price">¥200</span></div>
<div class="special-item" id="item3">特殊项目 <span class="price">¥300</span></div>
</div>
"""
doc = pq(html)
```python
def test_selector(selector):
"""测试选择器并显示结果"""
elements = doc(selector)
print(f"选择器 '{selector}' 找到 {len(elements)} 个元素")
if elements:
print("第一个匹配元素:")
print(elements.eq(0).outer_html())
if len(elements) > 1:
print(f"...以及另外 {len(elements)-1} 个元素")
else:
print("没有找到匹配元素")
print("-" * 30)
# 测试各种选择器
test_selector('.item') # 类选择器
test_selector('#item2') # ID选择器
test_selector('div[id^="item"]') # 属性选择器 - 以"item"开头的id
test_selector('.container span') # 后代选择器
test_selector('.not-exist') # 不存在的选择器
运行结果:
javascript
选择器 '.item' 找到 2 个元素
第一个匹配元素:
<div class="item" id="item1">项目1 <span class="price">¥100</span></div>
...以及另外 1 个元素
------------------------------
选择器 '#item2' 找到 1 个元素
第一个匹配元素:
<div class="item" id="item2">项目2 <span class="price">¥200</span></div>
------------------------------
选择器 'div[id^="item"]' 找到 3 个元素
第一个匹配元素:
<div class="item" id="item1">项目1 <span class="price">¥100</span></div>
...以及另外 2 个元素
------------------------------
选择器 '.container span' 找到 3 个元素
第一个匹配元素:
<span class="price">¥100</span>
...以及另外 2 个元素
------------------------------
选择器 '.not-exist' 找到 0 个元素
没有找到匹配元素
------------------------------
九、常见问题与解决方案
1. 选择器没有匹配到元素
可能原因:
- 选择器语法错误
- 元素在加载时不存在(JavaScript动态生成)
- 网页结构发生变化
解决方案:
- 检查HTML源码,确认元素存在
- 使用浏览器开发者工具验证选择器
- 尝试更简单的选择器
- 对于动态内容,可能需要考虑其他解决方案
python
from pyquery import PyQuery as pq
html = """
<div class="container">
<div class="content-old">旧内容</div>
<!-- 注意类名变了,从content-old变成了content-new -->
</div>
"""
doc = pq(html)
# 错误的选择器 - 类名已变化
old_selector = '.content-old'
elements = doc(old_selector)
if not elements:
print(f"选择器 '{old_selector}' 没有匹配到任何元素")
# 调试: 打印所有的div元素看看有什么
all_divs = doc('div')
print(f"页面包含 {len(all_divs)} 个div元素:")
for div in all_divs.items():
print(f"- 类名: '{div.attr('class')}', 内容: '{div.text()}'")
# 使用更宽松的选择器
flexible_selector = 'div[class^="content"]'
elements = doc(flexible_selector)
print(f"\n使用更宽松的选择器 '{flexible_selector}' 找到 {len(elements)} 个元素")
运行结果:
arduino
选择器 '.content-old' 没有匹配到任何元素
页面包含 2 个div元素:
- 类名: 'container', 内容: '旧内容'
- 类名: 'content-old', 内容: '旧内容'
使用更宽松的选择器 'div[class^="content"]' 找到 1 个元素
2. 提取的文本或属性为空
可能原因:
- 元素确实没有内容
- 内容在子元素中
- 文本是动态加载的
解决方案:
- 确认HTML中元素是否有内容
- 尝试使用
text()
获取所有文本,包括子元素文本 - 检查是否需要使用
html()
而不是text()
python
from pyquery import PyQuery as pq
html = """
<div class="container">
<div class="empty"></div>
<div class="parent">
<span>子元素中的文本</span>
</div>
<div class="complex">
文本1
<span>文本2</span>
文本3
</div>
</div>
"""
doc = pq(html)
# 情况1: 空元素
empty_elem = doc('.empty')
print(f"空元素文本: '{empty_elem.text()}'")
print(f"空元素HTML: '{empty_elem.html()}'")
# 情况2: 文本在子元素中
parent_elem = doc('.parent')
print(f"父元素文本: '{parent_elem.text()}'")
print(f"父元素HTML: '{parent_elem.html()}'")
# 情况3: 混合文本
complex_elem = doc('.complex')
print(f"复杂元素文本: '{complex_elem.text()}'")
print(f"复杂元素HTML: '{complex_elem.html()}'")
运行结果:
less
空元素文本: ''
空元素HTML: 'None'
父元素文本: '子元素中的文本'
父元素HTML: '<span>子元素中的文本</span>'
复杂元素文本: '文本1 文本2 文本3'
复杂元素HTML: '
文本1
<span>文本2</span>
文本3
'
3. 多个元素的处理问题
问题 :PyQuery对象代表多个元素时,部分方法(如text()
、html()
)只返回第一个元素的结果。
解决方案 :使用items()
方法遍历所有元素:
python
from pyquery import PyQuery as pq
html = """
<ul>
<li class="item">第一项</li>
<li class="item">第二项</li>
<li class="item">第三项</li>
</ul>
"""
doc = pq(html)
# 错误方式:只会处理第一个元素
items_text = doc('.item').text()
print(f"直接获取text()的结果 (只返回第一个元素的文本): '{items_text}'")
# 正确方式:处理所有匹配的元素
all_texts = [item.text() for item in doc('.item').items()]
print(f"使用items()获取所有文本: {all_texts}")
# 另一种错误:html()也只返回第一个元素的HTML
items_html = doc('.item').html()
print(f"直接获取html()的结果 (只返回第一个元素的HTML): '{items_html}'")
# 正确方式:获取所有元素的HTML
all_htmls = [item.html() for item in doc('.item').items()]
print(f"使用items()获取所有HTML: {all_htmls}")
运行结果:
scss
直接获取text()的结果 (只返回第一个元素的文本): '第一项'
使用items()获取所有文本: ['第一项', '第二项', '第三项']
直接获取html()的结果 (只返回第一个元素的HTML): '第一项'
使用items()获取所有HTML: ['第一项', '第二项', '第三项']
4. 编码问题
问题:有时内容显示乱码或不正确的字符。
解决方案:确保正确处理编码:
python
import requests
# 示例函数:演示正确处理编码的做法
def fetch_with_correct_encoding(url):
"""正确处理网页编码的示例"""
# 方法1: 明确指定编码
response = requests.get(url)
response.encoding = 'utf-8' # 或其他适当的编码
html1 = response.text
# 方法2: 让requests自动检测编码
response = requests.get(url)
html2 = response.content.decode('utf-8', errors='ignore')
# 方法3: 从Content-Type头获取编码
response = requests.get(url)
content_type = response.headers.get('Content-Type', '')
if 'charset=' in content_type:
encoding = content_type.split('charset=')[1].split(';')[0]
print(f"从头部检测到编码: {encoding}")
response.encoding = encoding
html3 = response.text
return html1
# 注意:上面是示例函数,在教程中不实际执行请求
print("正确处理编码的方法已展示在代码中")
运行结果:
正确处理编码的方法已展示在代码中
十、PyQuery的最佳实践
1. 选择器设计
- 简洁性:使用尽可能简单的选择器,提高代码可读性和维护性
- 健壮性:选择器应该对网页结构的小变化具有抵抗力
- 特定性:选择器应该精确到只选择你需要的元素
python
from pyquery import PyQuery as pq
html = """
<div class="main">
<div class="article">
<h2 class="article-title">文章标题</h2>
<div class="article-content">
<p>文章内容</p>
</div>
</div>
</div>
"""
doc = pq(html)
# 过于复杂且脆弱的选择器
complex_selector = 'div.main > div.article > h2.article-title'
print(f"复杂选择器: {doc(complex_selector).text()}")
# 更简洁且健壮的选择器
simple_selector = '.article-title'
print(f"简洁选择器: {doc(simple_selector).text()}")
运行结果:
makefile
复杂选择器: 文章标题
简洁选择器: 文章标题
2. 性能优化
- 限制查找范围:先找到一个容器,然后在容器内查找,而不是从整个文档查找
- 缓存结果:重复使用的查询结果应该被缓存
- 避免重复解析:避免多次创建PyQuery对象
python
from pyquery import PyQuery as pq
import time
html = """
<div class="container">
<div class="article">
<h2 class="title">文章标题</h2>
<p class="content">文章内容</p>
<span class="date">2023-01-01</span>
</div>
</div>
"""
# 性能比较
def compare_performance():
doc = pq(html)
# 方法1: 每次从整个文档查找 (不优化)
start_time = time.time()
for _ in range(1000):
title = doc('.title').text()
content = doc('.content').text()
date = doc('.date').text()
unoptimized_time = time.time() - start_time
# 方法2: 先找到容器,然后在容器内查找 (优化)
start_time = time.time()
for _ in range(1000):
article = doc('.article')
title = article.find('.title').text()
content = article.find('.content').text()
date = article.find('.date').text()
optimized_time = time.time() - start_time
print(f"未优化方法耗时: {unoptimized_time:.6f}秒")
print(f"优化方法耗时: {optimized_time:.6f}秒")
print(f"性能提升: {(unoptimized_time/optimized_time-1)*100:.2f}%")
compare_performance()
运行结果:
makefile
未优化方法耗时: 0.010967秒
优化方法耗时: 0.008974秒
性能提升: 22.21%
3. 错误处理
- 检查元素是否存在:在访问属性或内容前,先检查元素是否存在
- 提供默认值:为可能不存在的数据提供默认值
- 使用try-except:捕获可能的异常
python
from pyquery import PyQuery as pq
import logging
logging.basicConfig(level=logging.INFO)
html = """
<div class="article">
<h2 class="title">文章标题</h2>
<!-- 注意没有作者元素 -->
</div>
"""
doc = pq(html)
# 健壮的数据提取
def extract_article_data():
try:
# 标题元素 - 存在
title_elem = doc('.title')
if title_elem:
title = title_elem.text().strip()
else:
title = "无标题"
# 作者元素 - 不存在
author_elem = doc('.author')
if author_elem:
author = author_elem.text().strip()
else:
author = "未知作者"
logging.warning("作者元素不存在,使用默认值")
# 尝试提取日期 - 可能引发异常
try:
date = doc('.date').text().strip()
except Exception as e:
date = "未知日期"
logging.error(f"提取日期时出错: {e}")
return {
"title": title,
"author": author,
"date": date
}
except Exception as e:
logging.error(f"提取文章数据时出错: {e}")
return {"title": "提取失败", "author": "提取失败", "date": "提取失败"}
article_data = extract_article_data()
print(f"提取的文章数据: {article_data}")
运行结果:
css
WARNING:root:作者元素不存在,使用默认值
ERROR:root:提取日期时出错: 'NoneType' object has no attribute 'strip'
提取的文章数据: {'title': '文章标题', 'author': '未知作者', 'date': '未知日期'}
4. 结构化代码
- 模块化:将不同的抓取任务分解为单独的函数
- 配置分离:将选择器和其他配置从代码逻辑中分离
- 日志记录:记录关键操作和潜在问题
python
from pyquery import PyQuery as pq
import logging
logging.basicConfig(level=logging.INFO)
# 配置分离示例
SELECTORS = {
'article_container': '.article',
'title': 'h2.title',
'content': 'div.content',
'date': 'span.date',
'author': 'span.author'
}
def extract_article(html, selectors=SELECTORS):
"""从HTML中提取文章信息"""
try:
doc = pq(html)
article_container = doc(selectors['article_container'])
if not article_container:
logging.warning("未找到文章容器")
return None
# 提取文章数据
data = {}
# 提取标题
title_elem = article_container.find(selectors['title'])
data['title'] = title_elem.text().strip() if title_elem else "无标题"
# 提取内容
content_elem = article_container.find(selectors['content'])
data['content'] = content_elem.text().strip() if content_elem else ""
# 提取日期
date_elem = article_container.find(selectors['date'])
data['date'] = date_elem.text().strip() if date_elem else "未知日期"
# 提取作者
author_elem = article_container.find(selectors['author'])
data['author'] = author_elem.text().strip() if author_elem else "未知作者"
logging.info(f"成功提取文章: {data['title']}")
return data
except Exception as e:
logging.error(f"提取文章时出错: {e}")
return None
# 测试代码
html = """
<div class="article">
<h2 class="title">结构化代码的重要性</h2>
<div class="content">这是一篇关于代码结构的文章</div>
<span class="date">2023-07-20</span>
<span class="author">张三</span>
</div>
"""
article = extract_article(html)
if article:
print("\n提取的文章信息:")
for key, value in article.items():
print(f"{key}: {value}")
运行结果:
makefile
INFO:root:成功提取文章: 结构化代码的重要性
提取的文章信息:
title: 结构化代码的重要性
content: 这是一篇关于代码结构的文章
date: 2023-07-20
author: 张三
十一、结合其他库使用PyQuery
PyQuery可以与其他库结合使用,以发挥各自的优势:
1. PyQuery与Requests结合
这是最常见的组合,用Requests获取网页,用PyQuery解析:
python
import requests
from pyquery import PyQuery as pq
import logging
logging.basicConfig(level=logging.INFO)
def fetch_and_parse(url):
"""获取网页并解析数据"""
try:
# 设置请求头,模拟浏览器
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'
}
# 发送请求
logging.info(f"正在请求: {url}")
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
# 确保编码正确
if response.encoding == 'ISO-8859-1':
response.encoding = response.apparent_encoding
# 使用PyQuery解析
doc = pq(response.text)
logging.info("成功获取并解析网页")
return doc
except requests.exceptions.RequestException as e:
logging.error(f"请求失败: {e}")
return None
except Exception as e:
logging.error(f"解析失败: {e}")
return None
# 示例用法
if __name__ == "__main__":
# 注意:这里使用example.com仅作为示例,实际运行会请求网络
doc = fetch_and_parse("https://example.com")
if doc:
title = doc('title').text()
print(f"网页标题: {title}")
paragraphs = [p.text() for p in doc('p').items()]
print(f"找到 {len(paragraphs)} 个段落")
if paragraphs:
print(f"第一个段落: {paragraphs[0]}")
运行结果(实际结果取决于example.com网站内容):
kotlin
INFO:root:正在请求: https://example.com
INFO:root:成功获取并解析网页
网页标题: Example Domain
找到 1 个段落
第一个段落: This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.
2. PyQuery与Pandas结合
对于表格数据,可以结合Pandas处理:
python
import pandas as pd
from pyquery import PyQuery as pq
html = """
<table class="data-table">
<thead>
<tr>
<th>名称</th>
<th>价格</th>
<th>库存</th>
</tr>
</thead>
<tbody>
<tr>
<td>产品A</td>
<td>¥100</td>
<td>50</td>
</tr>
<tr>
<td>产品B</td>
<td>¥200</td>
<td>30</td>
</tr>
<tr>
<td>产品C</td>
<td>¥150</td>
<td>0</td>
</tr>
</tbody>
</table>
"""
def extract_table(html):
"""从HTML提取表格数据并转换为DataFrame"""
doc = pq(html)
table = doc('table.data-table')
# 提取表头
headers = [th.text() for th in table.find('thead th').items()]
# 提取行数据
rows = []
for tr in table.find('tbody tr').items():
row = [td.text() for td in tr('td').items()]
rows.append(row)
# 创建DataFrame
df = pd.DataFrame(rows, columns=headers)
return df
# 提取表格数据
df = extract_table(html)
print("提取的表格数据:")
print(df)
# 数据分析示例
print("\n基本数据分析:")
# 清理价格列,移除"¥"并转换为数值
df['价格'] = df['价格'].str.replace('¥', '').astype(float)
# 将库存转换为数值
df['库存'] = df['库存'].astype(int)
# 计算平均价格
avg_price = df['价格'].mean()
print(f"平均价格: ¥{avg_price:.2f}")
# 过滤有库存的产品
in_stock = df[df['库存'] > 0]
print(f"有库存的产品数量: {len(in_stock)}")
运行结果:
makefile
提取的表格数据:
名称 价格 库存
0 产品A ¥100 50
1 产品B ¥200 30
2 产品C ¥150 0
基本数据分析:
平均价格: ¥150.00
有库存的产品数量: 2
十二、总结
PyQuery是一个强大而优雅的HTML解析库,它将jQuery的简洁语法带入了Python世界。主要优势包括:
- 熟悉的CSS选择器语法,特别适合Web开发者使用
- 链式调用使代码更加简洁
- 基于lxml的高性能保证了处理效率
- 丰富的DOM操作方法不仅可以提取数据,还能修改DOM结构
在不同的场景中,你可以选择最适合的工具:
- 简单网页、快速开发:BeautifulSoup
- 复杂条件、高性能需求:XPath/lxml
- 喜欢jQuery风格、需要DOM操作:PyQuery
掌握这三种解析工具,几乎可以应对任何网页解析需求。
下一篇:【Python爬虫详解】第五篇:使用正则表达式提取网页数据