Selector选择器完全指南 - CSS与XPath数据提取技术详解
📂 所属阶段:第一阶段 — 初出茅庐(框架核心篇)
🔗 相关章节:Spider 实战 · Item 与 Item Loader
目录
Selector基础概念
在Scrapy爬虫开发中,Selector是数据提取的核心工具。它基于parsel库构建,同时支持CSS选择器和XPath表达式,让我们能从HTML/XML文档中精准定位所需数据。
Selector是如何工作的?
Selector的工作流程其实很简单:
- 接收HTML/XML内容
- 构建DOM树结构
- 应用选择器表达式定位元素
- 提取匹配的内容
- 返回结果对象
从Response获取Selector
在Scrapy中,Response对象已经内置了Selector,我们可以直接使用,无需额外创建:
def relationship_with_response(response):
"""
演示Response与Selector的关系
"""
# 方法1: 直接使用Response的css/xpath方法(推荐)
title1 = response.css('h1::text').get()
title2 = response.xpath('//h1/text()').get()
# 方法2: 通过response.selector访问
selector = response.selector
title3 = selector.css('h1::text').get()
title4 = selector.xpath('//h1/text()').get()
# 两种方法结果完全相同
return {
'title': title1,
'equivalent': title1 == title3
}
CSS选择器详解
CSS选择器是前端开发中常用的元素定位方式,在Scrapy中同样适用,而且语法简洁,易于理解。
常用CSS选择器类型
1. 基础选择器
"""
元素选择器: 通过标签名定位
- div: 所有<div>元素
- a: 所有<a>元素
类选择器: 通过class属性定位
- .product: class包含"product"的元素
- .hot.item: 同时包含"hot"和"item"类的元素
ID选择器: 通过id属性定位
- #main: id为"main"的元素
属性选择器: 通过其他属性定位
- [href]: 有href属性的元素
- [href="https://example.com"]: href等于特定值的元素
- [class*="product"]: class包含"product"的元素
"""
2. 组合选择器
"""
后代选择器: div p - div内所有层级的p元素
子选择器: div > p - div的直接子元素p
相邻兄弟: h1 + p - 紧跟在h1后的p元素
通用兄弟: h1 ~ p - h1后的所有同级p元素
"""
3. 伪类选择器
"""
:first-child: 第一个子元素
:last-child: 最后一个子元素
:nth-child(n): 第n个子元素
:not(selector): 排除符合条件的元素
"""
CSS选择器实战示例
def css_selectors_practical(response):
"""
CSS选择器实战用法
"""
results = {}
# 提取文本
results['titles'] = response.css('h1, h2, h3::text').getall()
# 提取属性
results['links'] = response.css('a::attr(href)').getall()
# 提取外部链接
results['external_links'] = response.css(
'a[href^="http://"], a[href^="https://"]::attr(href)'
).getall()
# 提取第一个产品名称
results['first_product'] = response.css(
'.product:first-child .name::text'
).get()
return results
XPath选择器详解
XPath是一种在XML文档中查找信息的语言,功能比CSS选择器更强大,尤其适合处理复杂的HTML结构。
XPath基础语法
"""
/: 从根节点开始
//: 从任意位置开始
.: 当前节点
..: 父节点
@: 属性
text(): 节点文本
路径示例:
//div: 文档中所有div元素
/div: 根节点下的div元素
//div[@class="product"]: class为product的div
//div[1]: 第一个div元素
//div[last()]: 最后一个div元素
"""
XPath轴选择
XPath的轴选择功能非常强大,可以让我们轻松定位元素的亲属关系:
"""
parent::*: 父元素
child::*: 子元素
ancestor::*: 所有祖先元素
descendant::*: 所有后代元素
following-sibling::*: 后面的同级元素
preceding-sibling::*: 前面的同级元素
"""
XPath常用函数
"""
contains(@attr, 'value'): 属性包含值
starts-with(@attr, 'value'): 属性以值开头
normalize-space(): 去除多余空白
position(): 元素位置
last(): 最后一个元素位置
count(): 计数
"""
XPath实战示例
def xpath_selectors_practical(response):
"""
XPath选择器实战用法
"""
results = {}
# 提取所有标题
results['headings'] = response.xpath(
'//h1/text() | //h2/text() | //h3/text()'
).getall()
# 提取外部链接
results['external_links'] = response.xpath(
'//a[starts-with(@href, "http://") or starts-with(@href, "https://")]/@href'
).getall()
# 提取h2后的第一个段落
results['next_p'] = response.xpath(
'//h2/following-sibling::p[1]/text()'
).getall()
# 提取有折扣的产品
results['discount_products'] = response.xpath(
'//div[@class="product" and .//span[@class="discount"]]//h3/text()'
).getall()
return results
get与getall方法
在Scrapy中,提取结果主要有两个方法:get()和getall(),它们的用法和返回值有明显区别。
get()方法
get()方法返回第一个匹配的结果,如果没有匹配项则返回None,还可以设置默认值:
def get_method_example(response):
"""
get()方法示例
"""
# 返回第一个标题,无匹配则为None
title = response.css('h1::text').get()
# 返回第一个链接,无匹配则使用默认值
first_link = response.css('a::attr(href)').get(default='No link found')
return {
'title': title,
'first_link': first_link
}
getall()方法
getall()方法返回所有匹配结果的列表,即使没有匹配项也会返回空列表[]:
def getall_method_example(response):
"""
getall()方法示例
"""
# 返回所有链接列表
all_links = response.css('a::attr(href)').getall()
# 无匹配时返回空列表
no_match = response.css('.nonexistent::text').getall() # 返回[]
return {
'all_links': all_links,
'no_match': no_match
}
性能对比
get()方法通常比getall()更快,因为它找到第一个匹配项就停止了:
import time
def performance_test(response):
"""
get() vs getall() 性能对比
"""
# 测试get()
start = time.time()
for _ in range(1000):
response.css('div.item h2::text').get()
get_time = time.time() - start
# 测试getall()
start = time.time()
for _ in range(1000):
response.css('div.item h2::text').getall()
getall_time = time.time() - start
return {
'get_time': get_time,
'getall_time': getall_time,
'get_is_faster': get_time < getall_time
}
高级选择器技巧
混合使用CSS和XPath
CSS选择器简洁,XPath功能强大,我们可以结合两者的优势:
def mixed_selectors(response):
"""
混合使用CSS和XPath
"""
results = {}
# 先用CSS定位,再用XPath细化
results['css_then_xpath'] = response.css('.product').xpath('./h2/text()').getall()
# 先用XPath定位,再用CSS提取
results['xpath_then_css'] = response.xpath('//div[@class="item"]').css('.price::text').getall()
return results
嵌套选择器处理复杂结构
对于列表类数据,我们可以先选择父元素,再在父元素内提取子元素:
def nested_extraction(response):
"""
嵌套提取示例
"""
products = []
# 先选择所有产品容器
for product in response.css('.product'):
# 在每个产品容器内提取信息
item = {
'name': product.css('.name::text').get(),
'price': product.css('.price::text').get(),
'url': product.css('a::attr(href)').get()
}
products.append(item)
return products
健壮的选择器策略
网页结构经常变化,我们需要使用多个备选选择器来提高爬虫的健壮性:
def robust_extraction(response):
"""
健壮的提取策略
"""
def extract_with_fallbacks(selectors):
"""尝试多个选择器,返回第一个有效结果"""
for sel in selectors:
try:
if sel.startswith('xpath:'):
result = response.xpath(sel[6:]).get()
else:
result = response.css(sel).get()
if result and result.strip():
return result.strip()
except:
continue
return None
# 标题的多个备选选择器
title_selectors = [
'h1.product-title::text',
'h1::text',
'title::text',
'xpath://h1/text()',
'xpath://title/text()'
]
return {
'title': extract_with_fallbacks(title_selectors)
}
性能优化策略
选择器优化
选择器越具体,性能越好:
def optimized_selectors(response):
"""
优化选择器性能
"""
# 好的做法:使用具体的选择器
good = response.css('div.product.highlighted .name::text').get()
# 避免的做法:过于宽泛的选择器
# bad = response.css('*[class*="product"] *::text').get()
return good
批量处理
先选择父元素,再批量处理子元素,避免多次遍历DOM:
def batch_processing(response):
"""
批量处理示例
"""
# 好的做法:一次选择,多次处理
products = response.css('div.product')
data = []
for product in products:
data.append({
'name': product.css('.name::text').get(),
'price': product.css('.price::text').get()
})
# 避免的做法:多次遍历DOM
# names = response.css('div.product .name::text').getall()
# prices = response.css('div.product .price::text').getall()
return data
实战应用场景
电商产品数据提取
class ProductExtractor:
"""电商产品数据提取器"""
def __init__(self):
# 定义多个备选选择器
self.selectors = {
'name': ['h1.product-title::text', 'h1::text'],
'price': ['.price::text', '.current-price::text'],
'description': ['.product-detail::text', '.description::text'],
'images': ['.gallery img::attr(src)', '.product-image::attr(src)']
}
def extract(self, response):
"""提取产品数据"""
product = {}
for field, sels in self.selectors.items():
product[field] = self._extract_with_fallbacks(response, sels)
# 特殊处理:提取规格参数
product['specs'] = self._extract_specs(response)
return product
def _extract_with_fallbacks(self, response, selectors):
"""尝试多个选择器"""
for sel in selectors:
result = response.css(sel).get()
if result and result.strip():
return result.strip()
return None
def _extract_specs(self, response):
"""提取规格参数"""
specs = {}
for row in response.css('.spec-table tr'):
key = row.css('td:first-child::text').get()
value = row.css('td:last-child::text').get()
if key and value:
specs[key.strip()] = value.strip()
return specs
新闻文章内容提取
class NewsExtractor:
"""新闻文章内容提取器"""
def extract(self, response):
"""提取新闻内容"""
return {
'title': self._extract_title(response),
'author': self._extract_author(response),
'date': self._extract_date(response),
'content': self._extract_content(response),
'tags': self._extract_tags(response)
}
def _extract_title(self, response):
"""提取标题"""
selectors = ['h1.article-title::text', 'h1::text', 'title::text']
return self._try_selectors(response, selectors)
def _extract_author(self, response):
"""提取作者"""
selectors = ['.author::text', '.byline::text']
author = self._try_selectors(response, selectors)
if author:
# 清理作者名
author = author.replace('作者:', '').replace('By ', '')
return author
def _extract_date(self, response):
"""提取发布时间"""
selectors = ['.publish-date::text', 'time::text']
return self._try_selectors(response, selectors)
def _extract_content(self, response):
"""提取正文内容"""
# 尝试常见的正文容器
containers = ['.article-content', '.content', '.post-content']
for container in containers:
paragraphs = response.css(f'{container} p::text').getall()
if paragraphs and len(paragraphs) > 2:
return '\n'.join(p.strip() for p in paragraphs if p.strip())
return None
def _extract_tags(self, response):
"""提取标签"""
tags = response.css('.tag::text, .tags a::text').getall()
# 去重并清理
return list(set(tag.strip() for tag in tags if tag.strip()))
def _try_selectors(self, response, selectors):
"""尝试多个选择器"""
for sel in selectors:
result = response.css(sel).get()
if result and result.strip():
return result.strip()
return None
常见问题与解决方案
问题1: 提取的文本有多余空白
解决方案: 使用strip()或XPath的normalize-space()
# 方法1: Python处理
text = response.css('h1::text').get()
clean_text = text.strip() if text else ''
# 方法2: XPath处理
clean_text = response.xpath('normalize-space(//h1/text())').get()
问题2: 需要提取包含HTML的内容
解决方案: 直接获取元素HTML或使用string()函数
# 获取包含HTML的内容
html_content = response.css('.content').get()
# 提取纯文本(去除HTML标签)
plain_text = response.xpath('string(//div[@class="content"])').get()
问题3: 选择器找不到元素
可能原因:
- 网页是动态加载的(JavaScript渲染)
- 选择器写得不对
- 网页结构变化了
解决方案:
- 检查网页源代码(不是开发者工具中的DOM)
- 使用多个备选选择器
- 对于动态内容,考虑使用Selenium或Playwright
最佳实践建议
选择器编写原则
- 具体优先: 使用具体的选择器,避免过于宽泛
- 多手准备: 为重要字段准备多个备选选择器
- CSS优先: 简单场景用CSS,复杂逻辑用XPath
- 集中管理: 将选择器集中定义,便于维护
错误处理策略
- 始终处理
None值
- 使用默认值
- 记录提取失败的情况
- 实现降级方案
💡 核心要点: Selector是Scrapy数据提取的核心,熟练掌握CSS和XPath选择器是爬虫开发的基本功。合理的选择器策略和错误处理能让你的爬虫更稳定、更高效。
🔗 相关教程推荐
🏷️ 标签云: Scrapy Selector CSS选择器 XPath 数据提取 爬虫框架 Python爬虫