【Python爬虫详解】第四篇:使用解析库提取网页数据——PyQuery

在前几篇文章中,我们已经介绍了BeautifulSoup和XPath两种强大的网页解析工具。本篇文章将介绍另一个优秀的网页解析库:PyQuery。PyQuery是一个模仿jQuery语法的Python库,让我们能够用熟悉的CSS选择器语法来解析和操作HTML文档。

一、PyQuery简介

PyQuery是一个强大而优雅的HTML解析库,它将jQuery的语法和思想带入Python世界。使用PyQuery的主要优势包括:

  1. 熟悉的语法:如果你熟悉jQuery,那么使用PyQuery将非常自然
  2. 简洁优雅:代码简洁,表达能力强
  3. CSS选择器:支持完整的CSS3选择器语法
  4. 链式调用:可以链式调用方法,使代码更简洁
  5. 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选择器:

  1. 在Chrome或Firefox中右键点击元素,选择"检查"
  2. 在元素面板中右键点击HTML代码,选择"Copy" > "Copy selector"
  3. 获取浏览器生成的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世界。主要优势包括:

  1. 熟悉的CSS选择器语法,特别适合Web开发者使用
  2. 链式调用使代码更加简洁
  3. 基于lxml的高性能保证了处理效率
  4. 丰富的DOM操作方法不仅可以提取数据,还能修改DOM结构

在不同的场景中,你可以选择最适合的工具:

  • 简单网页、快速开发:BeautifulSoup
  • 复杂条件、高性能需求:XPath/lxml
  • 喜欢jQuery风格、需要DOM操作:PyQuery

掌握这三种解析工具,几乎可以应对任何网页解析需求。


下一篇:【Python爬虫详解】第五篇:使用正则表达式提取网页数据

相关推荐
??? Meggie3 小时前
Selenium 怎么加入代理IP,以及怎么检测爬虫运行的时候,是否用了代理IP?
爬虫·tcp/ip·selenium
用户199701080184 小时前
深入解析淘宝商品详情 API 接口:功能、使用与实践指南
大数据·爬虫·数据挖掘
西柚小萌新7 小时前
【Python爬虫实战篇】--Selenium爬取Mysteel数据
开发语言·爬虫·python
Auroral1567 小时前
【Python爬虫详解】第四篇:使用解析库提取网页数据——XPath
爬虫
Auroral1567 小时前
【Python爬虫详解】第四篇:使用解析库提取网页数据——BeautifuSoup
爬虫
kadog8 小时前
《Python3网络爬虫开发实战(第二版)》配套案例 spa6
开发语言·javascript·爬虫·python
徒慕风流8 小时前
利用Python爬虫实现百度图片搜索的PNG图片下载
开发语言·爬虫·python
用户1997010801810 小时前
深入研究:Shopee商品列表API接口详解
大数据·爬虫·数据挖掘
攻城狮7号11 小时前
Python爬虫第19节-动态渲染页面抓取之Splash使用下篇
开发语言·爬虫·python·python爬虫