【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库:

pip install pyquery

安装成功后会看到类似以下输出:

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可以从多种来源创建对象:

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)

运行结果:

<html><body><div class="container">内容</div></body></html>
Example Domain
<html><body><div>内容</div></body></html>

2. CSS选择器

PyQuery使用CSS选择器来查找元素,语法与jQuery完全一致:

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')}")

运行结果:

找到 2 个div元素
容器标题: PyQuery示例
内容部分: 这是第一段内容 这是重要内容
重要段落: 这是重要内容
菜单链接数量: 3
直接子元素数量: 3
外部链接: /contact

3. 操作元素

PyQuery提供了丰富的方法来获取和操作元素:

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()}")

运行结果:

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的过滤和遍历方法:

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}")

运行结果:

第一个水果: 苹果
重要水果: 橙子, 葡萄
找到 5 个水果
列表项数量: 5
父元素标签: ul
其他水果数量: 4
水果列表: ['苹果', '香蕉', '橙子', '梨', '葡萄']

四、在Python爬虫中使用PyQuery

下面我们通过一个完整的例子来展示如何在爬虫中使用PyQuery:

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()

运行结果:

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那样:

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)

运行结果(实际结果可能会因时间而不同):

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的一个主要特点是支持链式调用,让代码更简洁:

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}")

运行结果:

通过链式调用找到的标题: 重要标题
通过分步调用找到的标题: 重要标题

2. DOM操作

PyQuery不仅可以提取数据,还可以修改DOM结构:

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)

运行结果:

添加类后:
<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的伪类选择器:

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)}")

运行结果:

第一个水果: 苹果
最后一个水果: 葡萄
偶数索引水果: 苹果, 橙子, 葡萄
奇数索引水果: 香蕉, 梨
第3个水果: 橙子
包含特定文本的div数量: 1
空div数量: 1

4. 表单处理

PyQuery提供了处理HTML表单的特殊方法:

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()})")

运行结果:

用户名: test_user
修改后的用户名: new_user
记住我选项是否选中: True
选中的角色: 管理员 (值: admin)

七、PyQuery与其他解析库的比较

让我们比较一下PyQuery与其他常用的解析库:

特性PyQueryBeautifulSoupXPath/lxml
语法风格jQuery风格Python原生风格XPath表达式
选择器CSS选择器多种选择器XPath选择器
性能良好(基于lxml)一般优秀
内存占用中等较高较低
易用性非常好(对熟悉jQuery的人)很好较复杂
DOM操作丰富有限有限
向上遍历支持支持支持
文档质量一般优秀良好

代码比较

让我们看看同一个任务使用不同库的代码比较:

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}秒")

运行结果:

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可能生成这样的选择器:

#main-content > div.article > h2.title

2. 选择器优化策略

浏览器生成的选择器通常很长且脆弱,以下是一些优化技巧:

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()}")

运行结果:

复杂选择器找到: 文章标题
ID选择器找到: 文章标题
类选择器找到: 文章标题
属性选择器找到: 文章标题
简单选择器找到: 文章标题

3. 选择器测试

在编写爬虫前,最好先测试你的选择器:

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')  # 不存在的选择器

运行结果:

选择器 '.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源码,确认元素存在
  • 使用浏览器开发者工具验证选择器
  • 尝试更简单的选择器
  • 对于动态内容,可能需要考虑其他解决方案
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)} 个元素")

运行结果:

选择器 '.content-old' 没有匹配到任何元素
页面包含 2 个div元素:
- 类名: 'container', 内容: '旧内容'
- 类名: 'content-old', 内容: '旧内容'

使用更宽松的选择器 'div[class^="content"]' 找到 1 个元素

2. 提取的文本或属性为空

可能原因

  • 元素确实没有内容
  • 内容在子元素中
  • 文本是动态加载的

解决方案

  • 确认HTML中元素是否有内容
  • 尝试使用text()获取所有文本,包括子元素文本
  • 检查是否需要使用html()而不是text()
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()}'")

运行结果:

空元素文本: ''
空元素HTML: 'None'
父元素文本: '子元素中的文本'
父元素HTML: '<span>子元素中的文本</span>'
复杂元素文本: '文本1 文本2 文本3'
复杂元素HTML: '
    文本1
    <span>文本2</span>
    文本3
  '

3. 多个元素的处理问题

问题:PyQuery对象代表多个元素时,部分方法(如text()html())只返回第一个元素的结果。

解决方案:使用items()方法遍历所有元素:

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}")

运行结果:

直接获取text()的结果 (只返回第一个元素的文本): '第一项'
使用items()获取所有文本: ['第一项', '第二项', '第三项']
直接获取html()的结果 (只返回第一个元素的HTML): '第一项'
使用items()获取所有HTML: ['第一项', '第二项', '第三项']

4. 编码问题

问题:有时内容显示乱码或不正确的字符。

解决方案:确保正确处理编码:

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. 选择器设计

  • 简洁性:使用尽可能简单的选择器,提高代码可读性和维护性
  • 健壮性:选择器应该对网页结构的小变化具有抵抗力
  • 特定性:选择器应该精确到只选择你需要的元素
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()}")

运行结果:

复杂选择器: 文章标题
简洁选择器: 文章标题

2. 性能优化

  • 限制查找范围:先找到一个容器,然后在容器内查找,而不是从整个文档查找
  • 缓存结果:重复使用的查询结果应该被缓存
  • 避免重复解析:避免多次创建PyQuery对象
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()

运行结果:

未优化方法耗时: 0.010967秒
优化方法耗时: 0.008974秒
性能提升: 22.21%

3. 错误处理

  • 检查元素是否存在:在访问属性或内容前,先检查元素是否存在
  • 提供默认值:为可能不存在的数据提供默认值
  • 使用try-except:捕获可能的异常
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}")

运行结果:

WARNING:root:作者元素不存在,使用默认值
ERROR:root:提取日期时出错: 'NoneType' object has no attribute 'strip'
提取的文章数据: {'title': '文章标题', 'author': '未知作者', 'date': '未知日期'}

4. 结构化代码

  • 模块化:将不同的抓取任务分解为单独的函数
  • 配置分离:将选择器和其他配置从代码逻辑中分离
  • 日志记录:记录关键操作和潜在问题
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}")

运行结果:

INFO:root:成功提取文章: 结构化代码的重要性

提取的文章信息:
title: 结构化代码的重要性
content: 这是一篇关于代码结构的文章
date: 2023-07-20
author: 张三

十一、结合其他库使用PyQuery

PyQuery可以与其他库结合使用,以发挥各自的优势:

1. PyQuery与Requests结合

这是最常见的组合,用Requests获取网页,用PyQuery解析:

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网站内容):

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处理:

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)}")

运行结果:

提取的表格数据:
   名称   价格 库存
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爬虫详解】第五篇:使用正则表达式提取网页数据

本文章已经生成可运行项目
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Luck_ff0810

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值