BeautifulSoup 支持使用 CSS 选择器来查找元素,这种方法语法简洁、功能强大,特别适合熟悉 CSS 的开发者。本章将详细介绍如何使用 select() 和 select_one() 方法通过 CSS 选择器来定位和提取 HTML 元素。
select() 返回所有匹配元素的列表,select_one() 返回第一个匹配元素。
# select() 方法 - 返回所有匹配元素的列表
soup.select(selector) # 返回列表
# select_one() 方法 - 返回第一个匹配元素
soup.select_one(selector) # 返回单个元素或None
| 方法 | 返回值 | 示例 | 说明 |
|---|---|---|---|
select() |
列表(可能为空) | soup.select('div') |
返回所有匹配的Tag对象列表 |
select_one() |
Tag对象或None | soup.select_one('.header') |
返回第一个匹配的Tag对象 |
为了演示各种CSS选择器的用法,我们使用以下HTML文档作为示例:
from bs4 import BeautifulSoup
html_doc = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>CSS选择器示例页面</title>
<meta charset="UTF-8">
<style>
.featured { border: 2px solid gold; }
.new::after { content: " NEW!"; color: red; }
</style>
</head>
<body>
<div id="page-wrapper" class="container">
<header id="main-header" class="site-header">
<h1 class="page-title">产品展示</h1>
<nav class="main-navigation">
<ul>
<li><a href="#home" class="nav-link active" data-page="home">首页</a></li>
<li><a href="#products" class="nav-link" data-page="products">产品</a></li>
<li><a href="#about" class="nav-link" data-page="about">关于</a></li>
<li><a href="#contact" class="nav-link" data-page="contact">联系</a></li>
</ul>
</nav>
</header>
<main class="content-area">
<section id="product-list" class="products-section">
<h2>产品列表</h2>
<div class="product-grid">
<article class="product-card featured" data-id="1" data-category="electronics">
<h3 class="product-name">智能手机</h3>
<p class="product-description">最新款智能手机,功能强大。</p>
<div class="product-meta">
<span class="price">¥2999</span>
<span class="rating">★★★★☆</span>
<span class="tag new">新品</span>
</div>
<button class="btn-buy" data-action="buy">立即购买</button>
</article>
<article class="product-card" data-id="2" data-category="electronics">
<h3 class="product-name">笔记本电脑</h3>
<p class="product-description">高性能笔记本电脑,适合工作和游戏。</p>
<div class="product-meta">
<span class="price">¥6999</span>
<span class="rating">★★★★★</span>
<span class="tag sale">促销</span>
</div>
<button class="btn-buy" data-action="buy">立即购买</button>
</article>
<article class="product-card" data-id="3" data-category="books">
<h3 class="product-name">Python编程书</h3>
<p class="product-description">Python编程入门到精通。</p>
<div class="product-meta">
<span class="price">¥89</span>
<span class="rating">★★★☆☆</span>
<span class="tag">图书</span>
</div>
<button class="btn-buy" data-action="buy">立即购买</button>
</article>
<article class="product-card" data-id="4" data-category="electronics">
<h3 class="product-name">无线耳机</h3>
<p class="product-description">高品质无线蓝牙耳机。</p>
<div class="product-meta">
<span class="price">¥599</span>
<span class="rating">★★★★☆</span>
<span class="tag">配件</span>
</div>
<button class="btn-buy" data-action="buy">立即购买</button>
</article>
</div>
</section>
<aside class="sidebar">
<div class="widget">
<h3>热门分类</h3>
<ul class="category-list">
<li><a href="/category/electronics" class="category-link">电子产品</a></li>
<li><a href="/category/books" class="category-link">图书</a></li>
<li><a href="/category/clothing" class="category-link">服装</a></li>
</ul>
</div>
</aside>
</main>
<footer class="site-footer">
<div class="footer-content">
<p>© 2024 产品展示. 保留所有权利.</p>
<div class="footer-links">
<a href="#privacy" class="footer-link">隐私政策</a>
<a href="#terms" class="footer-link">服务条款</a>
<a href="#help" class="footer-link">帮助中心</a>
</div>
</div>
</footer>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html_doc, 'lxml')
# 选择所有h1标签
h1_tags = soup.select('h1')
print(f"h1标签数量: {len(h1_tags)}")
for h1 in h1_tags:
print(f"- {h1.text}")
# 选择所有p标签
p_tags = soup.select('p')
print(f"\np标签数量: {len(p_tags)}")
print("前3个p标签:")
for i, p in enumerate(p_tags[:3], 1):
print(f"{i}. {p.text[:30]}...")
# 选择所有具有特定class的元素
product_cards = soup.select('.product-card')
print(f"产品卡片数量: {len(product_cards)}")
# 选择具有多个类的元素
featured_products = soup.select('.product-card.featured')
print(f"特色产品数量: {len(featured_products)}")
# 选择某个类下的特定标签
product_names = soup.select('.product-name')
print(f"\n产品名称:")
for name in product_names:
print(f"- {name.text}")
# 选择具有特定ID的元素
main_header = soup.select_one('#main-header')
print(f"主头部: {main_header.name if main_header else '未找到'}")
# ID选择器返回列表(通常只有一个元素)
product_list = soup.select('#product-list')
print(f"产品列表区域: {len(product_list)} 个")
# 结合标签和ID选择器
header_h1 = soup.select_one('header#main-header h1')
print(f"主头部中的h1: {header_h1.text if header_h1 else '未找到'}")
# 选择article中的所有h3标签
article_h3s = soup.select('article h3')
print(f"article中的h3数量: {len(article_h3s)}")
for h3 in article_h3s:
print(f"- {h3.text}")
# 多级后代选择
product_prices = soup.select('.product-card .price')
print(f"\n产品价格:")
for price in product_prices:
print(f"- {price.text}")
# 嵌套选择
nav_links = soup.select('nav ul li a')
print(f"\n导航链接数量: {len(nav_links)}")
for link in nav_links:
print(f"- {link.text}: {link.get('href', '')}")
# 选择直接子元素
product_grid_children = soup.select('.product-grid > article')
print(f"product-grid的直接子元素article数量: {len(product_grid_children)}")
# 与后代选择器对比
all_articles_in_grid = soup.select('.product-grid article')
print(f"product-grid中的所有article数量: {len(all_articles_in_grid)}")
# 选择直接子元素中的特定标签
product_meta_spans = soup.select('.product-card > .product-meta > span')
print(f"\n产品meta中的直接span子元素数量: {len(product_meta_spans)}")
for span in product_meta_spans[:5]: # 只显示前5个
print(f"- {span.text}")
# 选择紧接在另一个元素后的元素
first_product_next = soup.select_one('.product-card + .product-card')
print(f"第一个产品卡片后的产品卡片: {first_product_next.find('h3').text if first_product_next else '无'}")
# 选择h2后的第一个p
h2_following_p = soup.select_one('h2 + p')
print(f"h2后的第一个p: {h2_following_p.text[:30] if h2_following_p else '无'}...")
# 选择所有在指定元素后的兄弟元素
h2_all_following = soup.select('h2 ~ article')
print(f"h2后的所有article兄弟: {len(h2_all_following)}")
# 选择同一父元素下的所有后续兄弟
featured_following = soup.select('.featured ~ .product-card')
print(f"特色产品后的所有产品: {len(featured_following)}")
# 选择具有特定属性的元素
elements_with_id = soup.select('[id]')
print(f"具有id属性的元素数量: {len(elements_with_id)}")
# 选择具有特定属性值的元素
electronics_products = soup.select('[data-category="electronics"]')
print(f"电子产品数量: {len(electronics_products)}")
# 选择具有data-*属性的元素
all_data_attr = soup.select('[data-id]')
print(f"具有data-id属性的元素数量: {len(all_data_attr)}")
# 属性值包含指定字符串
active_link = soup.select('[class*="active"]')
print(f"class包含'active'的元素: {len(active_link)}")
# 属性值以指定字符串开头
data_elements = soup.select('[data-^="data-"]')
print(f"属性以'data-'开头的元素数量: {len(data_elements)}")
# 属性值以指定字符串结尾
category_links = soup.select('[href$="electronics"]')
print(f"href以'electronics'结尾的链接: {len(category_links)}")
# 属性值包含指定单词(空格分隔)
nav_links = soup.select('[class~="nav-link"]')
print(f"class包含'nav-link'单词的元素: {len(nav_links)}")
# 属性值以指定字符串开头
data_id_starts = soup.select('[data-id^="1"]')
print(f"data-id以'1'开头的元素: {len(data_id_starts)}")
# 属性值以指定字符串结尾
category_ends = soup.select('[data-category$="s"]')
print(f"data-category以's'结尾的元素: {len(category_ends)}")
# 组合使用
specific_products = soup.select('[data-category^="elec"][data-id$="2"]')
print(f"data-category以'elec'开头且data-id以'2'结尾的产品: {len(specific_products)}")
# 选择第一个子元素
first_product = soup.select_one('.product-card:first-child')
print(f"第一个产品: {first_product.find('h3').text if first_product else '无'}")
# 选择最后一个子元素
last_product = soup.select_one('.product-card:last-child')
print(f"最后一个产品: {last_product.find('h3').text if last_product else '无'}")
# 选择第n个子元素
third_product = soup.select_one('.product-card:nth-child(3)')
print(f"第三个产品: {third_product.find('h3').text if third_product else '无'}")
# 选择奇数/偶数位置的元素
odd_products = soup.select('.product-card:nth-child(odd)')
even_products = soup.select('.product-card:nth-child(even)')
print(f"奇数位置产品: {len(odd_products)}")
print(f"偶数位置产品: {len(even_products)}")
# 选择特定类型的第一个
first_article = soup.select_one('article:first-of-type')
print(f"第一个article: {first_article.find('h3').text if first_article else '无'}")
# 选择特定类型的最后一个
last_article = soup.select_one('article:last-of-type')
print(f"最后一个article: {last_article.find('h3').text if last_article else '无'}")
# 选择特定类型的第n个
third_article = soup.select_one('article:nth-of-type(3)')
print(f"第三个article: {third_article.find('h3').text if third_article else '无'}")
# 同时选择多个元素
headings = soup.select('h1, h2, h3')
print(f"所有标题数量: {len(headings)}")
print("标题列表:")
for heading in headings:
print(f"- {heading.name}: {heading.text}")
# 选择多个类的元素
special_elements = soup.select('.featured, .sale, .new')
print(f"\n特色、促销或新品元素: {len(special_elements)}")
for elem in special_elements:
print(f"- {elem.text[:20]}...")
# 组合多种选择器
expensive_electronics = soup.select('.product-card[data-category="electronics"] .price')
print("电子产品价格:")
for price in expensive_electronics:
price_text = price.text.replace('¥', '')
price_value = int(price_text) if price_text.isdigit() else 0
if price_value > 1000:
print(f"- 高价: {price.text}")
# 嵌套组合选择器
navigation_items = soup.select('nav > ul > li > a[data-page]')
print(f"\n导航中有data-page属性的链接:")
for item in navigation_items:
print(f"- {item.text}: {item.get('data-page')}")
# 提取所有产品名称
product_names = [card.text for card in soup.select('.product-name')]
print("所有产品名称:")
for name in product_names:
print(f"- {name}")
# 提取产品价格
prices = [price.text for price in soup.select('.price')]
print(f"\n所有产品价格: {prices}")
# 提取链接文本和URL
links_info = []
for link in soup.select('a'):
links_info.append({
'text': link.text,
'href': link.get('href', ''),
'class': link.get('class', [])
})
print(f"\n前3个链接信息:")
for info in links_info[:3]:
print(f"- {info['text']}: {info['href']}")
# 提取所有data-id值
product_ids = [card.get('data-id') for card in soup.select('[data-id]')]
print(f"产品ID列表: {product_ids}")
# 提取分类信息
categories = set(card.get('data-category') for card in soup.select('[data-category]'))
print(f"所有产品分类: {categories}")
# 提取按钮的data-action
button_actions = [btn.get('data-action') for btn in soup.select('[data-action]')]
print(f"按钮动作: {button_actions}")
# 链式使用select
product_grid = soup.select_one('.product-grid')
if product_grid:
# 在找到的元素上继续使用select
featured_in_grid = product_grid.select('.featured')
print(f"产品网格中的特色产品: {len(featured_in_grid)}")
# 链式提取信息
for product in product_grid.select('.product-card'):
name = product.select_one('.product-name')
price = product.select_one('.price')
if name and price:
print(f"- {name.text}: {price.text}")
# 多层链式选择
main_content = soup.select_one('.content-area')
if main_content:
articles = main_content.select('article')
print(f"\n主要内容区的文章数量: {len(articles)}")
# 嵌套选择结构化的数据
products_data = []
for product in soup.select('.product-card'):
product_data = {
'id': product.get('data-id'),
'category': product.get('data-category'),
'name': product.select_one('.product-name').text if product.select_one('.product-name') else '',
'price': product.select_one('.price').text if product.select_one('.price') else '',
'rating': product.select_one('.rating').text if product.select_one('.rating') else '',
'tags': [tag.text for tag in product.select('.tag')],
'description': product.select_one('.product-description').text if product.select_one('.product-description') else ''
}
products_data.append(product_data)
print("产品数据:")
for data in products_data[:2]: # 只显示前2个
print(f"\n产品ID: {data['id']}")
print(f"名称: {data['name']}")
print(f"价格: {data['price']}")
print(f"标签: {data['tags']}")
# 不推荐:过于宽泛的选择器
all_divs = soup.select('div') # 选择所有div,性能较差
# 推荐:更具体的选择器
specific_divs = soup.select('.product-card > div') # 只选择产品卡片下的直接div子元素
# 使用ID选择器(最快)
header = soup.select_one('#main-header') # ID选择器是最快的
# 不推荐:过度嵌套的选择器
deep_nested = soup.select('body > div > main > section > div > article > h3')
# 推荐:简化选择器
simplified = soup.select('.product-name') # 直接使用类选择器
# 如果必须嵌套,尽量浅层
shallow = soup.select('.product-card h3') # 比上面的深层选择器好
# 不推荐:使用select然后取第一个
first_product = soup.select('.product-card')[0] if soup.select('.product-card') else None
# 推荐:直接使用select_one
first_product = soup.select_one('.product-card')
# 对于需要判断是否存在的场景
featured = soup.select_one('.featured')
if featured:
print(f"找到特色产品: {featured.find('h3').text}")
| 特性 | CSS选择器 | find()方法 |
|---|---|---|
| 语法 | CSS语法,熟悉Web开发的开发者更习惯 | Python方法调用,参数形式 |
| 功能 | 支持完整CSS选择器,功能强大 | 参数有限,但可配合正则和函数 |
| 性能 | 通常较快,特别是简单选择器 | 对于复杂选择可能较慢 |
| 易用性 | 链式选择方便,代码简洁 | 需要更多Python代码 |
| 灵活性 | 选择器语法固定 | 可以使用自定义函数,更灵活 |
下面是一个完整的示例,展示如何使用CSS选择器从产品页面提取结构化数据。
def extract_product_catalog(soup):
"""从页面提取完整的产品目录"""
catalog = {
'products': [],
'categories': set(),
'price_range': {'min': float('inf'), 'max': 0},
'stats': {}
}
# 提取所有产品
product_cards = soup.select('.product-card')
for card in product_cards:
product = extract_product_info(card)
catalog['products'].append(product)
# 更新分类集合
if product['category']:
catalog['categories'].add(product['category'])
# 更新价格范围
if product['price_value'] > 0:
catalog['price_range']['min'] = min(catalog['price_range']['min'], product['price_value'])
catalog['price_range']['max'] = max(catalog['price_range']['max'], product['price_value'])
# 计算统计信息
catalog['stats'] = {
'total_products': len(catalog['products']),
'total_categories': len(catalog['categories']),
'featured_products': len([p for p in catalog['products'] if p['is_featured']]),
'electronics_count': len([p for p in catalog['products'] if p['category'] == 'electronics'])
}
return catalog
def extract_product_info(product_element):
"""从单个产品元素提取信息"""
# 提取基本信息
product_info = {
'id': product_element.get('data-id', ''),
'category': product_element.get('data-category', ''),
'name': extract_text(product_element.select_one('.product-name')),
'description': extract_text(product_element.select_one('.product-description')),
'price': extract_text(product_element.select_one('.price')),
'price_value': extract_price_value(product_element.select_one('.price')),
'rating': extract_text(product_element.select_one('.rating')),
'rating_value': extract_rating_value(product_element.select_one('.rating')),
'tags': [tag.text for tag in product_element.select('.tag')],
'is_featured': 'featured' in product_element.get('class', []),
'has_button': bool(product_element.select_one('.btn-buy'))
}
return product_info
def extract_text(element):
"""安全提取文本"""
return element.text.strip() if element else ''
def extract_price_value(price_element):
"""从价格元素提取数值"""
if not price_element:
return 0
text = price_element.text.strip()
# 移除货币符号和非数字字符
import re
numbers = re.findall(r'\d+', text)
return int(numbers[0]) if numbers else 0
def extract_rating_value(rating_element):
"""从评分元素提取数值(1-5)"""
if not rating_element:
return 0
text = rating_element.text.strip()
# 计算星号数量
stars = text.count('★')
half_stars = text.count('☆') * 0.5
return stars + half_stars
# 执行提取
catalog = extract_product_catalog(soup)
print("=== 产品目录统计 ===")
print(f"产品总数: {catalog['stats']['total_products']}")
print(f"分类数量: {catalog['stats']['total_categories']}")
print(f"特色产品: {catalog['stats']['featured_products']}")
print(f"电子产品: {catalog['stats']['electronics_count']}")
print(f"价格范围: ¥{catalog['price_range']['min']} - ¥{catalog['price_range']['max']}")
print("\n=== 所有产品分类 ===")
for category in catalog['categories']:
print(f"- {category}")
print("\n=== 前2个产品详情 ===")
for i, product in enumerate(catalog['products'][:2], 1):
print(f"\n产品 {i}:")
print(f" ID: {product['id']}")
print(f" 名称: {product['name']}")
print(f" 价格: {product['price']}")
print(f" 评分: {product['rating']} ({product['rating_value']}/5)")
print(f" 标签: {', '.join(product['tags'])}")
# 错误:缺少点号
wrong = soup.select('product-card') # 应该是 '.product-card'
# 正确
correct = soup.select('.product-card')
# 检查元素是否存在
elements = soup.select('.non-existent')
if not elements:
print("元素不存在")
else:
print(f"找到 {len(elements)} 个元素")
# 检查页面是否包含预期内容
print("页面标题:", soup.select_one('title').text if soup.select_one('title') else '无')
print("h1数量:", len(soup.select('h1')))
CSS选择器本身不支持按文本内容选择,但可以结合BeautifulSoup的其他功能:
# 方法1:使用find_all配合文本过滤
python_products = []
for product in soup.select('.product-card'):
if 'Python' in product.text:
python_products.append(product)
print(f"包含'Python'的产品: {len(python_products)}")
# 方法2:使用CSS选择器加后续过滤
expensive = []
for price in soup.select('.price'):
price_value = int(price.text.replace('¥', '')) if price.text.replace('¥', '').isdigit() else 0
if price_value > 1000:
expensive.append(price.parent.parent) # 向上找到产品卡片
print(f"价格超过1000的产品: {len(expensive)}")
# 对于复杂结构,可以分步提取
def extract_nested_data(soup):
"""处理复杂嵌套结构的数据提取"""
result = {}
# 第一步:提取主要区域
main_content = soup.select_one('.content-area')
if not main_content:
return result
# 第二步:提取产品区域
product_section = main_content.select_one('#product-list')
if product_section:
result['products'] = extract_products(product_section)
# 第三步:提取侧边栏
sidebar = main_content.select_one('.sidebar')
if sidebar:
result['categories'] = extract_categories(sidebar)
return result
def extract_products(section):
"""从产品区域提取产品"""
products = []
for product in section.select('.product-card'):
products.append({
'name': extract_text(product.select_one('.product-name')),
'price': extract_text(product.select_one('.price'))
})
return products
def extract_categories(sidebar):
"""从侧边栏提取分类"""
categories = []
for link in sidebar.select('.category-link'):
categories.append({
'name': link.text,
'url': link.get('href', '')
})
return categories
# 使用示例
data = extract_nested_data(soup)
print(f"提取的产品数量: {len(data.get('products', []))}")
print(f"提取的分类数量: {len(data.get('categories', []))}")
select()和select_one()方法,你可以使用熟悉的CSS语法来定位和提取HTML元素。无论是简单的标签选择、类选择,还是复杂的属性匹配、伪类选择,CSS选择器都能提供简洁高效的解决方案。在实际项目中,建议根据具体需求选择合适的选择器,并结合性能优化技巧,以获得最佳的解析效果。