BeautifulSoup findAll 返回空列表?5大原因和解决办法
2025-03-28 10:10:10
BeautifulSoup 抓取网页,findAll() 返回空列表?原因和解决办法都在这儿
写爬虫的时候,用 Python 的 BeautifulSoup 库来解析 HTML 是个挺常见的操作。但有时候,明明感觉选择器写对了,可 findAll()
或者 find_all()
函数(它俩现在是等价的)就是任性地返回一个空列表 []
,啥也抓不到。就像下面这位朋友遇到的情况:
想抓取 Mouthshut 网站上评论的标题、日期、评分和具体内容。但是页面标题下面的东西,比如 class 为 'more reviewdata' 的 div 里的 p 标签内容,就完全抓不到。试了 Stack Overflow 上类似问题的好几个代码片段,用
find()
/findAll()
去找特定的 class,结果还是空的列表。
from bs4 import BeautifulSoup import requests url = 'https://www.mouthshut.com/product-reviews/berger-paints-reviews-925712965-page-4' response = requests.get(url) soup = BeautifulSoup(response.content, 'html.parser') # 尝试提取评论内容 for div in soup.find_all('div', class_='more reviewdata'): for p in div.find_all('p'): print(p.text) # 这里没输出 # 尝试提取评分 soup.find_all('i', class_='icon-rating rated-star') # 这里也没输出 # 甚至连普通的 div 都找不到? soup.findAll('div') # 可能也是空列表或者不包含预期内容
谁能给点建议,怎么才能让
findAll()
不返回空列表,抓到我想要的 class 数据?
这个问题其实挺典型的,不少人在用 BeautifulSoup 时都可能碰到。别急,咱们来分析分析为啥会这样,再看看有哪些解决办法。
为啥 findAll() 会“罢工”?扒一扒可能的原因
findAll()
返回空列表,本质上意味着 BeautifulSoup 在你提供的 HTML 文档里,根据你指定的条件(标签名、class、id 等),没有找到任何匹配的元素。导致这种情况的原因有不少,常见的主要有这么几类:
-
目标元素压根儿就不在 HTML 源码里 :
- 动态加载内容 :这是最常见的原因之一。很多网站使用 JavaScript 在页面初步加载完成后,再异步请求数据并渲染到页面上。你用
requests
获取到的只是页面的初始 HTML 骨架,那些由 JS 生成的内容还没“出生”呢,BeautifulSoup 自然找不到。 - 需要登录或特定条件才能看到的内容 :有些内容可能需要用户登录、设置特定的 Cookie 或者满足其他条件才能显示。直接请求 URL 可能拿不到包含这些内容的 HTML。
- 动态加载内容 :这是最常见的原因之一。很多网站使用 JavaScript 在页面初步加载完成后,再异步请求数据并渲染到页面上。你用
-
你的选择器写错了 :
- 标签名、类名 (class) 或 ID 拼写错误 :这是“手滑”型错误,比如
class_
写成了class
,或者类名多了个空格、少了个字母。注意,BeautifulSoup 里指定 class 时用class_
是因为class
是 Python 的。 - HTML 结构理解错误 :你以为某个元素是另一个元素的子元素,但实际上它们是兄弟元素,或者嵌套层级比你预想的更深或更浅。
- 类名包含空格 :如果一个元素的
class
属性包含多个类名(用空格隔开),比如<div class="classA classB">
,直接find_all('div', class_='classA classB')
可能不行(取决于 BeautifulSoup 版本和解析器,但最好别这么依赖)。通常需要用 CSS 选择器select('div.classA.classB')
,或者只指定其中一个足够独特的类名。
- 标签名、类名 (class) 或 ID 拼写错误 :这是“手滑”型错误,比如
-
网站有反爬虫机制 :
- User-Agent 校验 :服务器检测到请求头里的 User-Agent 是 Python
requests
的默认值,或者干脆没有,就直接拒绝返回正常内容,或者返回一个“请升级浏览器”之类的假页面。 - IP 限制或频率限制 :短时间内来自同一 IP 的请求太多,网站把你暂时或永久“拉黑”了,返回的也不是真实数据页面。
- 更复杂的反爬措施 :比如验证码、JS 混淆、动态生成选择器名称等。
- User-Agent 校验 :服务器检测到请求头里的 User-Agent 是 Python
-
网络请求问题或响应内容不完整 :
requests.get(url)
请求可能因为网络问题、超时或者服务器端错误,并没有成功获取到完整的 HTML 页面。检查response.status_code
是不是 200,以及response.text
或response.content
的内容是否符合预期很重要。
-
HTML 解析器选择的影响 :
- BeautifulSoup 支持多种解析器(
'html.parser'
,'lxml'
,'html5lib'
)。不同的解析器在处理不规范的 HTML 时,行为可能略有差异。有时候换个解析器就能解决问题,尤其是lxml
,它通常更快、容错性更好。
- BeautifulSoup 支持多种解析器(
了解了可能的原因,我们就能“对症下药”了。
对症下药:搞定 findAll() 返回空列表的几种方法
下面列出几种解决 findAll()
返回空列表问题的常用策略和步骤。
方案一:检查 HTML 内容,确保目标元素真的存在
这是最基础也是最重要的一步。在你开始怀疑选择器或代码逻辑之前,先确认 requests
请求回来的 HTML 里到底有没有你想要的东西。
原理与作用 :
验证网络请求是否成功获取了包含目标数据的、完整的 HTML 源码。如果源码里就没有,那 BeautifulSoup 再神通广大也变不出来。
操作步骤 :
-
打印响应状态码和内容 :
import requests url = 'https://www.mouthshut.com/product-reviews/berger-paints-reviews-925712965-page-4' headers = { # 加个常见的浏览器 User-Agent 试试,后面会细说 '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' } try: response = requests.get(url, headers=headers, timeout=10) # 设置超时 response.raise_for_status() # 如果状态码不是 2xx,会抛出异常 print(f"Status Code: {response.status_code}") # 打印前 500 个字符看看大概内容 # print(response.text[:500]) # 或者,直接把完整内容保存到文件里查看 with open('mouthshut_page.html', 'w', encoding='utf-8') as f: f.write(response.text) print("HTML content saved to mouthshut_page.html") except requests.exceptions.RequestException as e: print(f"Request failed: {e}")
-
人工检查 HTML 文件 :
用浏览器或者文本编辑器打开mouthshut_page.html
文件。搜索你想要抓取的元素的关键词,比如类名'more reviewdata'
或'icon-rating rated-star'
。看看它们是否存在于这个 HTML 文件中。
同时,使用浏览器的开发者工具(通常按 F12 打开)检查实际页面加载完成后的元素结构,对比一下和你保存的 HTML 文件有何不同。
额外建议 :
- 检查网站的
robots.txt
文件(通常在域名/robots.txt
),了解网站对爬虫的规则限制。虽然它没有强制约束力,但遵守它是良好爬虫公民的表现。
方案二:换个“姿势”选元素——尝试不同的选择器
如果 HTML 内容确认无误,目标元素确实存在,那很可能是你的选择器不够精准或者写错了。
原理与作用 :
HTML 结构可能比你预想的复杂或有所不同。换用更稳定、更准确的选择器(比如 ID、更具体的 class 组合,或者 CSS 选择器),或者放宽选择条件再逐步筛选,可以提高找到元素的成功率。
操作步骤与代码示例 :
-
使用浏览器开发者工具 :
- 在目标网页上,右键点击你想要抓取的元素(比如评论内容或评分星星),选择“检查”或“Inspect Element”。
- 开发者工具会高亮显示该元素的 HTML 代码。仔细观察它的标签名、id、class 属性。注意 class 名是不是有多个,或者和你代码里写的是不是完全一致。
- 尝试复制开发者工具提供的 CSS Selector 或 XPath,虽然它们有时会过于冗长和脆弱,但可以作为起点。
-
尝试不同的 BeautifulSoup 选择方法 :
- 用 CSS 选择器 (
select()
方法) :通常更灵活,尤其处理复合类名或层级关系时。from bs4 import BeautifulSoup import requests # 假设 response.text 已经包含了正确的 HTML 内容 # soup = BeautifulSoup(response.text, 'lxml') # 推荐使用 lxml 解析器 # 尝试用 CSS 选择器找评论内容 (假设它确实在 p 标签里) # 注意 .more.reviewdata 中间的点表示同一个元素需要同时拥有这两个 class (如果确实是这样的话) # 但更常见的是 div class="more reviewdata", CSS 选择器是 div.more.reviewdata 或者 div[class='more reviewdata'] # 这里我们假设原问题的 class 是 "more reviewdata",可能需要选择器是 .more.reviewdata 或者 .reviewdata # 经过实际查看Mouthshut页面(假设),评论内容可能在一个特定ID下的某个结构里 # 例如,评论可能在 id="ctl00_ctl00_ContentPlaceHolderFooter_ContentPlaceHolderBody_litReviewDetails" 内部 # 我们需要实际检查后调整下面的选择器 # 示例:假设评论内容在 <div class="reviewdata"> 下的 <p> 标签 (注意:这里根据实际情况调整!) reviews = soup.select('div.reviewdata p') if not reviews: print("Using 'div.reviewdata p' selector didn't find anything. Trying original approach again with select...") # 回到原始的 class='more reviewdata',用 CSS Selector reviews = soup.select('div.more.reviewdata p') # 尝试 .more 和 .reviewdata 同时存在 if reviews: for review in reviews: print(review.get_text(strip=True)) else: print("Could not find review text paragraphs using various selectors.") # 尝试用 CSS 选择器找评分星星 (假设 i 标签确实有 'icon-rating' 和 'rated-star' 两个类) ratings = soup.select('i.icon-rating.rated-star') if ratings: for rating_icon in ratings: # 可能需要从 i 标签的兄弟节点或父节点获取具体评分数字 # 这里仅打印找到的 i 标签自身,具体怎么提取评分值需要看 HTML 结构 print(f"Found rating icon: {rating_icon}") # 示例:尝试获取父元素的某个属性或文本 parent_div = rating_icon.find_parent('div', class_='rating') # 假设评分在一个叫 rating 的 div 里 if parent_div: # 找表示分数的 span 或其他标签 score_span = parent_div.find('span', class_='rating-text') # 假设有这样一个 span if score_span: print(f" Rating text found: {score_span.get_text(strip=True)}") else: # 可能需要根据星星数量计算分数,如 len(parent_div.find_all('i', class_='rated-star')) rated_stars_count = len(parent_div.find_all('i', class_='rated-star')) print(f" Rated stars count: {rated_stars_count}") else: print("Could not find rating icons using 'i.icon-rating.rated-star'.")
- 放宽条件,逐步查找 :先找一个肯定存在的父级元素,再在它的子孙元素里查找。
# 假设所有评论都在一个 ID 为 'reviews-container' 的大 div 里 container = soup.find('div', id='reviews-container') # 假设 ID 存在 if container: # 在这个容器内查找评论 review_divs = container.find_all('div', class_='more reviewdata') for div in review_divs: p_tags = div.find_all('p') for p in p_tags: print(p.text) else: print("Could not find the main reviews container.")
- 用 CSS 选择器 (
进阶使用技巧 :
- 结合正则表达式:
find_all(re.compile("^p"))
查找所有 p 开头的标签。find_all(class_=re.compile("review"))
查找 class 包含 "review" 的元素。 - 使用 lambda 表达式进行更复杂的筛选:
find_all(lambda tag: tag.name == 'div' and tag.get('class') == ['more', 'reviewdata'])
。
方案三:对付动态加载内容——引入 Selenium 或 Playwright
如果检查 HTML 源码发现确实缺少目标内容,很可能就是 JavaScript 动态加载搞的鬼。这时候光靠 requests
+ BeautifulSoup 就不够了,需要能执行 JavaScript 的工具。
原理与作用 :
Selenium 和 Playwright 是浏览器自动化工具,它们可以驱动一个真实的浏览器(或无头浏览器)加载页面,执行 JavaScript,等待动态内容加载完成后,再把渲染好的页面 HTML 提供给你。这样 BeautifulSoup 就能解析到完整的 DOM 了。
操作步骤与代码示例 (以 Selenium 为例) :
-
安装 Selenium 和 WebDriver :
- 安装 Selenium 库:
pip install selenium
- 下载 WebDriver:你需要下载对应你浏览器(如 Chrome, Firefox)的 WebDriver 可执行文件,并将其路径配置到系统 PATH,或者在代码里指定路径。例如,ChromeDriver 下载地址可以在网上搜到。
- 安装 Selenium 库:
-
使用 Selenium 获取渲染后的 HTML :
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from bs4 import BeautifulSoup import time url = 'https://www.mouthshut.com/product-reviews/berger-paints-reviews-925712965-page-4' # 配置 WebDriver (这里用 Chrome 示例,需要 chromedriver 在 PATH 或指定路径) options = webdriver.ChromeOptions() options.add_argument('--headless') # 无头模式,不打开浏览器窗口 options.add_argument('--disable-gpu') options.add_argument('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') # 设置 User-Agent driver = None # 初始化 driver 变量 try: # 指定 ChromeDriver 路径 (如果不在 PATH 中) # driver_path = '/path/to/chromedriver' # driver = webdriver.Chrome(executable_path=driver_path, options=options) driver = webdriver.Chrome(options=options) # 如果 chromedriver 在 PATH 中 driver.get(url) # 等待关键元素加载完成 (选择一个动态加载后肯定会出现的元素) # 比如,等待评论的容器 div 出现,最多等 20 秒 wait = WebDriverWait(driver, 20) # 注意:这里的选择器需要根据实际情况调整! # 可能是评论区ID,或者第一条评论的某个特征 class target_element_locator = (By.CSS_SELECTOR, 'div.more.reviewdata') # 用 CSS 选择器定位 # 或者你可以等待更明确的元素,例如评分星星 # target_element_locator = (By.CSS_SELECTOR, 'i.icon-rating.rated-star') try: wait.until(EC.presence_of_element_located(target_element_locator)) print("Target element likely loaded.") except Exception as e: print(f"Timed out waiting for page element: {e}") # 即使超时,也尝试获取页面源码,可能部分内容已加载 pass # 继续执行,尝试解析已有内容 # 获取渲染后的页面源码 html_content = driver.page_source # 现在可以用 BeautifulSoup 解析这个完整的 HTML 了 soup = BeautifulSoup(html_content, 'lxml') # 推荐 lxml # 再次尝试之前的查找逻辑 review_texts = [] for div in soup.find_all('div', class_='more reviewdata'): for p in div.find_all('p'): review_texts.append(p.get_text(strip=True)) print("Found reviews:", review_texts[:5]) # 打印前5条看看 rating_icons = soup.find_all('i', class_='icon-rating rated-star') print(f"Found {len(rating_icons)} rating icons.") except Exception as e: print(f"An error occurred with Selenium: {e}") finally: if driver: driver.quit() # 关闭浏览器进程
(Playwright 的用法类似,安装
pip install playwright
和playwright install
,然后使用其 API 进行页面加载和等待。)
额外建议与安全考量 :
- 性能开销 :Selenium/Playwright 比
requests
慢得多,资源消耗也更大。只在必要时使用。 - 增加等待时间/更智能的等待 :简单的
time.sleep()
不可靠,优先使用WebDriverWait
等待特定元素出现或可见。 - 处理弹窗和交互 :如果页面需要点击按钮、滚动等操作才能加载内容,Selenium/Playwright 也能模拟。
- 伦理与法律 :自动化浏览器可能对网站服务器造成更大压力。务必遵守网站的爬取政策,避免过于频繁的请求。
方案四:伪装成“正常用户”——设置请求头 (User-Agent)
有些服务器比较敏感,一看请求不像来自普通浏览器,就可能不返回真实数据。
原理与作用 :
在 HTTP 请求中加入 User-Agent
请求头,模拟是某个常见的浏览器(如 Chrome, Firefox)在访问页面,可以绕过一些基础的反爬虫检查。
操作步骤与代码示例 :
import requests
from bs4 import BeautifulSoup
url = 'https://www.mouthshut.com/product-reviews/berger-paints-reviews-925712965-page-4'
# 定义一个常见的浏览器 User-Agent
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'
# 可以考虑添加其他请求头, 如 'Accept-Language': 'en-US,en;q=0.9'
}
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'lxml') # 使用 lxml
# ... 后续的解析代码 ...
reviews = soup.select('div.more.reviewdata p')
ratings = soup.select('i.icon-rating.rated-star')
if reviews:
print(f"Found {len(reviews)} review paragraphs after setting User-Agent.")
else:
print("Still couldn't find reviews even with User-Agent.")
if ratings:
print(f"Found {len(ratings)} rating icons after setting User-Agent.")
else:
print("Still couldn't find rating icons even with User-Agent.")
except requests.exceptions.RequestException as e:
print(f"Request failed with User-Agent: {e}")
额外安全建议 :
- 使用真实且多样的 User-Agent :可以准备一个 User-Agent 列表,每次请求随机选择一个。
- 尊重
robots.txt
和服务条款 (ToS) :即使伪装成功,也要遵守网站规则,避免滥用。 - 频率控制 :配合
time.sleep()
控制请求频率,模仿人类浏览行为,减少被封禁的风险。
方案五:换个 HTML 解析器试试
虽然不总是问题的根源,但有时 HTML 页面的写法不太标准,不同的解析器处理方式不同。
原理与作用 :
lxml
和 html5lib
解析器通常比 Python 内置的 html.parser
更能容忍(或者说,更能按浏览器标准处理)一些“有瑕疵”的 HTML 代码。lxml
速度快,html5lib
最接近浏览器行为但较慢。
操作步骤与代码示例 :
-
安装
lxml
或html5lib
:pip install lxml
pip install html5lib
-
修改 BeautifulSoup 初始化代码 :
# ... 获取 response 的代码 ... # 使用 lxml 解析器 soup_lxml = BeautifulSoup(response.content, 'lxml') # 然后用 soup_lxml 进行 find_all/select # 或者使用 html5lib 解析器 # soup_html5lib = BeautifulSoup(response.content, 'html5lib') # 然后用 soup_html5lib 进行 find_all/select
在你的代码中,只需要把
BeautifulSoup(response.content, 'html.parser')
改成'lxml'
或'html5lib'
即可。通常推荐优先尝试lxml
。
一些额外的建议
- 耐心和调试是关键 :爬虫开发经常需要反复尝试和调试。多用
print()
输出中间结果(比如response.status_code
,response.text
的片段,soup.prettify()
输出格式化后的 HTML,某个find()
的结果),逐步定位问题所在。 - 处理异常 :网络请求、页面解析都可能出错。使用
try...except
块捕获requests.exceptions.RequestException
,AttributeError
(比如对None
调用find
时) 等异常,让你的爬虫更健壮。 - 理解目标网站 :花点时间在浏览器里研究目标网站的结构和行为,特别是网络请求(Network tab in DevTools),看看数据是怎么加载的,有没有隐藏的 API 接口可以直接调用(这通常比解析 HTML 更稳定高效)。
遇到 findAll()
返回空列表,不要慌,一步步排查下来,问题通常都能解决。希望这些方法能帮你抓到想要的数据!