爬虫开发中有一类需求非常典型:页面上的目标数据本身是一段纯文本,比如价格、日期、作者名,而我们需要顺着这段文本回溯,拿到包裹它的父标签,进而读取父标签上的class、href等属性。BeautifulSoup对这类场景有原生支持,字符串节点本身就是树的一部分,同样可以调用find_parent等查找方法。这篇文章详细讲解如何定位字符串,以及如何从这个字符串出发获取它的父标签。

一、字符串在BeautifulSoup中是什么角色
很多初学者以为BeautifulSoup的查找方法只针对标签,其实不然。解析后的文档树由Tag、NavigableString和Comment三种核心节点组成,我们看到的每一段可见文本,本质上都是一个NavigableString对象。它和标签一样拥有parent、find_parent、find_all_previous等方法,可以完整参与树的遍历。
看一个最基础的例子。下面的HTML中包含一段文本,我们先用string参数找到它,再观察它的类型和行为:
from bs4 import BeautifulSoup
html = '''
<div class="product">
<span class="price">¥199.00</span>
</div>
'''
soup = BeautifulSoup(html, 'html.parser')
# 用string参数匹配文本内容,返回的是span标签
tag = soup.find('span', string='¥199.00')
print(type(tag)) # <class 'bs4.element.Tag'>
# 访问tag.string拿到的是NavigableString对象
text_node = tag.string
print(type(text_node)) # <class 'bs4.element.NavigableString'>
注意一个关键区别:find('span', string='¥199.00')返回的是span标签本身,而tag.string返回的才是字符串节点。理解这一层关系后,后续的父标签查找就顺理成章了。
二、从字符串节点向上获取父标签的三种方式
1. 直接访问parent属性
每个节点都有parent属性,字符串节点也不例外。它返回直接上一层的标签,如果字符串已经位于文档顶层,则返回None。这种方式最快,适合层级关系明确的场景。
span = soup.find('span', string='¥199.00')
parent = span.string.parent
print(parent.name) # span
print(parent.get('class')) # ['price']
2. 使用find_parent按条件回溯
如果直接父标签不是你想要的,比如文本外面包了好几层div,而你需要找到具备特定class的那个祖先标签,就要用find_parent()。它从当前节点开始向上搜索,返回第一个满足条件的祖先。与之配套的find_parents()(注意复数)则返回所有满足条件的祖先列表。
html = '''
<div class="wrap">
<ul class="list">
<li>
<a href="/item/1001">机械键盘</a>
</li>
</ul>
</div>
'''
soup = BeautifulSoup(html, 'html.parser')
a_tag = soup.find('a', string='机械键盘')
# 从a标签的字符串向上找class为list的ul
ul = a_tag.string.find_parent('ul', class_='list')
print(ul.name) # ul
# 找所有div祖先
divs = a_tag.string.find_parents('div')
print(len(divs)) # 1
3. 配合find_all批量处理
实际爬虫里更常见的是批量场景:列表页有几十个商品,每个商品都有价格文本,需要逐个取父标签。这时把find_all和find_parent组合起来即可:
html = '''
<div class="goods">
<p class="item"><span class="price">99元</span></p>
<p class="item"><span class="price">129元</span></p>
<p class="item"><span class="price">259元</span></p>
</div>
'''
soup = BeautifulSoup(html, 'html.parser')
for span in soup.find_all('span', class_='price'):
p = span.find_parent('p', class_='item')
print(p.span.string)
这里建议从span出发回溯,而不是直接遍历p标签再下钻,因为父标签的条件往往更稳定,页面改版时span的结构变了,只要p的class不变,代码依然可用,健壮性更好。
三、string与text参数的区别及常见坑
BeautifulSoup 4.4之后,string逐步取代了早期的text参数,两者目前都可以用,但推荐统一写string。需要特别注意的是精确匹配问题:string='¥199.00'要求整个字符串节点完全相等,如果页面上的文本前后有空格或者夹杂了换行,就会匹配不到。
解决方式是传入正则表达式或函数。正则适合模糊包含的场景,函数则最灵活,可以写任意判断逻辑:
import re
html = '<p> 当前库存:仅剩3件 </p>'
soup = BeautifulSoup(html, 'html.parser')
# 精确匹配会失败
print(soup.find('p', string='当前库存:仅剩3件')) # None
# 用正则实现包含匹配
p = soup.find('p', string=re.compile('仅剩3件'))
print(p.string.strip())
# 用函数自定义过滤
def has_stock(s):
return s is not None and '库存' in s
print(soup.find(string=has_stock).parent.name) # p
另一个容易踩的坑是标签内含多个字符串的情况。如果一个标签下还有子标签,tag.string会返回None,因为BeautifulSoup无法确定你要哪个字符串。这时应改用get_text()拿合并后的全部文本,或者用find_all(string=...)逐个处理子字符串节点。
四、实战:定位注释与特殊节点的父标签
除了可见文本,注释Comment也是NavigableString的子类,同样可以定位并回溯。这个技巧在分析某些把数据藏在注释里的页面时特别有用,比如部分网站把JSON配置写在HTML注释中,渲染时再取出来用。
from bs4 import BeautifulSoup
from bs4 import Comment
html = '''
<div id="data-box">
<!-- {"itemId": 1001, "stock": 32} -->
</div>
'''
soup = BeautifulSoup(html, 'html.parser')
comment = soup.find(string=lambda s: isinstance(s, Comment))
box = comment.find_parent(id='data-box')
print(box['id']) # data-box
# 注释内容本身就是合法JSON,可以直接解析
import json
data = json.loads(comment)
print(data['stock']) # 32
综合来看,定位字符串再回溯父标签的核心思路就三步:先用string参数或函数过滤找到文本所在位置,再通过parent或find_parent向上拿到目标标签,最后从父标签上读取属性或进一步解析。把这些方法组合熟练后,面对结构不规范的页面也能从容应对,写出的爬虫也会更加简洁可靠。
BeautifulSoupfind_parent字符串定位修改时间:2026-09-12 02:00:38