返回

BeautifulSoup findAll 返回空列表?5大原因和解决办法

python

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 等),没有找到任何匹配的元素。导致这种情况的原因有不少,常见的主要有这么几类:

  1. 目标元素压根儿就不在 HTML 源码里

    • 动态加载内容 :这是最常见的原因之一。很多网站使用 JavaScript 在页面初步加载完成后,再异步请求数据并渲染到页面上。你用 requests 获取到的只是页面的初始 HTML 骨架,那些由 JS 生成的内容还没“出生”呢,BeautifulSoup 自然找不到。
    • 需要登录或特定条件才能看到的内容 :有些内容可能需要用户登录、设置特定的 Cookie 或者满足其他条件才能显示。直接请求 URL 可能拿不到包含这些内容的 HTML。
  2. 你的选择器写错了

    • 标签名、类名 (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'),或者只指定其中一个足够独特的类名。
  3. 网站有反爬虫机制

    • User-Agent 校验 :服务器检测到请求头里的 User-Agent 是 Python requests 的默认值,或者干脆没有,就直接拒绝返回正常内容,或者返回一个“请升级浏览器”之类的假页面。
    • IP 限制或频率限制 :短时间内来自同一 IP 的请求太多,网站把你暂时或永久“拉黑”了,返回的也不是真实数据页面。
    • 更复杂的反爬措施 :比如验证码、JS 混淆、动态生成选择器名称等。
  4. 网络请求问题或响应内容不完整

    • requests.get(url) 请求可能因为网络问题、超时或者服务器端错误,并没有成功获取到完整的 HTML 页面。检查 response.status_code 是不是 200,以及 response.textresponse.content 的内容是否符合预期很重要。
  5. HTML 解析器选择的影响

    • BeautifulSoup 支持多种解析器('html.parser', 'lxml', 'html5lib')。不同的解析器在处理不规范的 HTML 时,行为可能略有差异。有时候换个解析器就能解决问题,尤其是 lxml,它通常更快、容错性更好。

了解了可能的原因,我们就能“对症下药”了。

对症下药:搞定 findAll() 返回空列表的几种方法

下面列出几种解决 findAll() 返回空列表问题的常用策略和步骤。

方案一:检查 HTML 内容,确保目标元素真的存在

这是最基础也是最重要的一步。在你开始怀疑选择器或代码逻辑之前,先确认 requests 请求回来的 HTML 里到底有没有你想要的东西。

原理与作用
验证网络请求是否成功获取了包含目标数据的、完整的 HTML 源码。如果源码里就没有,那 BeautifulSoup 再神通广大也变不出来。

操作步骤

  1. 打印响应状态码和内容

    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}")
    
  2. 人工检查 HTML 文件
    用浏览器或者文本编辑器打开 mouthshut_page.html 文件。搜索你想要抓取的元素的关键词,比如类名 'more reviewdata''icon-rating rated-star'。看看它们是否存在于这个 HTML 文件中。
    同时,使用浏览器的开发者工具(通常按 F12 打开)检查实际页面加载完成后的元素结构,对比一下和你保存的 HTML 文件有何不同。

额外建议

  • 检查网站的 robots.txt 文件(通常在 域名/robots.txt),了解网站对爬虫的规则限制。虽然它没有强制约束力,但遵守它是良好爬虫公民的表现。

方案二:换个“姿势”选元素——尝试不同的选择器

如果 HTML 内容确认无误,目标元素确实存在,那很可能是你的选择器不够精准或者写错了。

原理与作用
HTML 结构可能比你预想的复杂或有所不同。换用更稳定、更准确的选择器(比如 ID、更具体的 class 组合,或者 CSS 选择器),或者放宽选择条件再逐步筛选,可以提高找到元素的成功率。

操作步骤与代码示例

  1. 使用浏览器开发者工具

    • 在目标网页上,右键点击你想要抓取的元素(比如评论内容或评分星星),选择“检查”或“Inspect Element”。
    • 开发者工具会高亮显示该元素的 HTML 代码。仔细观察它的标签名、id、class 属性。注意 class 名是不是有多个,或者和你代码里写的是不是完全一致。
    • 尝试复制开发者工具提供的 CSS Selector 或 XPath,虽然它们有时会过于冗长和脆弱,但可以作为起点。
  2. 尝试不同的 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.")
      

进阶使用技巧

  • 结合正则表达式: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 为例)

  1. 安装 Selenium 和 WebDriver

    • 安装 Selenium 库:pip install selenium
    • 下载 WebDriver:你需要下载对应你浏览器(如 Chrome, Firefox)的 WebDriver 可执行文件,并将其路径配置到系统 PATH,或者在代码里指定路径。例如,ChromeDriver 下载地址可以在网上搜到。
  2. 使用 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 playwrightplaywright 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 页面的写法不太标准,不同的解析器处理方式不同。

原理与作用
lxmlhtml5lib 解析器通常比 Python 内置的 html.parser 更能容忍(或者说,更能按浏览器标准处理)一些“有瑕疵”的 HTML 代码。lxml 速度快,html5lib 最接近浏览器行为但较慢。

操作步骤与代码示例

  1. 安装 lxmlhtml5lib

    • pip install lxml
    • pip install html5lib
  2. 修改 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() 返回空列表,不要慌,一步步排查下来,问题通常都能解决。希望这些方法能帮你抓到想要的数据!