在Scrapy爬虫开发中,经常会遇到目标数据分散在同一个div标签下的多个p标签中,且p标签的数量会根据页面内容动态变化的情况。如果使用固定索引的方式提取p标签内容,当页面p标签数量变化时就会出现提取不全或者报错的问题,结合XPath的批量提取能力可以很好地解决这个问题。

场景说明
假设我们要抓取的页面结构如下,div的类名为content-wrap,内部包含数量不定的p标签,每个p标签存放一段文本,我们需要把所有p标签的文本合并成一个完整的段落存储:
<div class="content-wrap">
<p>第一段文本内容</p>
<p>第二段文本内容</p>
<p>第三段文本内容</p>
<!-- 可能还有更多p标签 -->
</div>
XPath提取所有p标签的核心语法
XPath提供了通配符和后代节点选择的能力,我们可以通过以下路径快速选中目标div下的所有p标签:
- 首先定位到类名为
content-wrap的div节点://div[@class="content-wrap"] - 再选择该div下的所有p标签后代节点:
//div[@class="content-wrap"]//p - 提取所有p标签的文本内容:
//div[@class="content-wrap"]//p/text()
这个XPath表达式不需要关心p标签的数量,不管div下有多少个p标签,都会返回所有p标签的文本组成的列表。
Scrapy中的实现步骤
1. 编写Spider提取逻辑
在Scrapy的Spider文件中,我们可以通过response对象的xpath方法执行上述XPath表达式,然后处理返回的列表:
import scrapy
class ContentSpider(scrapy.Spider):
name = "content_spider"
start_urls = ["http://ipipp.com/sample_page"] # 替换为实际目标页面地址
def parse(self, response):
# 提取所有p标签的文本内容,返回的是包含多个字符串的SelectorList
p_text_list = response.xpath('//div[@class="content-wrap"]//p/text()').getall()
# 过滤空字符串,避免无意义的空白内容
valid_text_list = [text.strip() for text in p_text_list if text.strip()]
# 合并所有文本,用换行符分隔
merged_content = "n".join(valid_text_list)
# 构造返回的数据字典
yield {
"page_url": response.url,
"merged_content": merged_content
}
2. 代码逻辑说明
上述代码的核心逻辑分为三步:
- 使用
getall()方法获取XPath匹配到的所有文本内容,得到一个字符串列表,不管p标签数量多少都会全部返回 - 对列表中的每个文本做去空白处理,过滤掉只有空格或者换行的无效内容
- 使用
join()方法把过滤后的文本列表合并成一个字符串,这里用换行符分隔,也可以根据需求换成其他分隔符比如空格
3. 数据存储配置
如果需要把合并后的数据存储到文件或者数据库,只需要在Scrapy的Pipeline中处理即可,以下是存储到JSON文件的简单示例:
import json
class JsonPipeline:
def open_spider(self, spider):
# 打开文件准备写入
self.file = open("result.json", "w", encoding="utf-8")
def process_item(self, item, spider):
# 把item转成json字符串写入文件
line = json.dumps(dict(item), ensure_ascii=False) + "n"
self.file.write(line)
return item
def close_spider(self, spider):
self.file.close()
然后在settings.py中启用这个Pipeline:
ITEM_PIPELINES = {
"your_project_name.pipelines.JsonPipeline": 300,
}
注意事项
- 如果目标div的类名可能有多个,比如
class="content-wrap other-class",可以使用contains()函数修改XPath://div[contains(@class, "content-wrap")]//p/text() - 如果p标签内部还有子标签比如
span,直接提取text()可能只会拿到部分文本,这时可以改用//div[@class="content-wrap"]//p//text()提取p标签下所有后代节点的文本 - 合并文本的分隔符可以根据实际需求调整,比如需要拼接成连贯段落就使用空格分隔,需要保留段落结构就用换行符分隔
这种方案不需要提前知道p标签的数量,适配所有动态变化的场景,比逐个索引提取的方式更健壮,也能减少很多冗余的判断代码。