4、动态内容抓取与Selenium:JavaScript渲染页面原理、Selenium环境搭建、元素定位与交互、等待策略
好,咱们进入第四章。这一章聊的东西,说实话,是爬虫工程师从「入门」到「进阶」的一道分水岭。
你想想看,前面几章我们抓的页面,都是老老实实的 HTML。服务器把数据塞进标签里,我们直接拿 requests 去取就行。但现在的网站,尤其是社交媒体,谁还这么干?数据都是通过 JavaScript 动态加载的。你打开页面,看到的只是一个空壳子,真正的数据是 JS 跑起来之后,再通过 Ajax 请求拿到的。
这时候,requests 就彻底歇菜了。它只能拿到那个空壳子。那怎么办?我们需要一个真正的浏览器——Selenium。
JavaScript 渲染页面原理
先聊聊原理。为什么有些页面我们抓不到?
传统的网页,服务器返回完整的 HTML,浏览器直接渲染。这叫「服务端渲染」。但现在的单页应用(SPA),比如微博、Twitter、知乎的动态流,用的是「客户端渲染」。服务器只返回一个 index.html,里面就一个 <div id="app"></div>,然后一堆 JS 文件。浏览器下载完 JS,执行它,JS 再去调接口拿数据,最后把数据塞进那个 div 里。
所以,你用 requests 拿到的,就是那个空 div。数据在哪?在 JS 执行后的内存里。
核心结论: 要拿到动态内容,你必须让 JS 先跑起来。Selenium 就是干这个的——它模拟一个真实的浏览器环境,让 JS 完整执行,然后你再从渲染后的 DOM 里取数据。
我个人习惯把这种抓取方式叫做「浏览器自动化」。说白了,就是让程序像人一样操作浏览器。
Selenium 环境搭建
搭建环境其实不复杂,但有几个坑。我一步步说。
第一步:安装 Selenium 库
pip install selenium
嗯,就这一行。但别急,光装库没用,你还需要一个「浏览器驱动」。
第二步:下载 WebDriver
Selenium 需要通过 WebDriver 来操控浏览器。Chrome 用 ChromeDriver,Firefox 用 GeckoDriver。版本必须和你的浏览器版本匹配。
我记得有一次,项目上线前突然报错,查了半天,原来是 Chrome 自动更新了,但 ChromeDriver 没更新。从那以后,我养成了一个习惯:每次跑 Selenium 之前,先检查浏览器版本和驱动版本是否一致。
| 浏览器 | 驱动名称 | 下载地址 |
|---|---|---|
| Chrome | ChromeDriver | https://chromedriver.chromium.org/ |
| Firefox | GeckoDriver | https://github.com/mozilla/geckodriver |
| Edge | EdgeDriver | https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ |
第三步:启动浏览器
from selenium import webdriver
# 指定驱动路径(如果驱动在 PATH 环境变量中,可以省略)
driver = webdriver.Chrome(executable_path='/path/to/chromedriver')
# 打开一个页面
driver.get('https://example.com')
# 关闭浏览器
driver.quit()
小技巧: 如果你不想每次看到浏览器弹出来,可以开启「无头模式」。这样浏览器在后台运行,不显示界面,节省资源。
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
元素定位与交互
浏览器打开了,页面渲染了,接下来就是找到我们想要的数据。Selenium 提供了多种定位方式。
我个人最常用的是 find_element 系列方法。你可以根据 ID、Class Name、XPath、CSS Selector 等来定位。
| 定位方式 | 方法 | 示例 |
|---|---|---|
| ID | find_element(By.ID, 'id') | driver.find_element(By.ID, 'username') |
| Class Name | find_element(By.CLASS_NAME, 'class') | driver.find_element(By.CLASS_NAME, 'post-content') |
| XPath | find_element(By.XPATH, '//div[@class="content"]') | driver.find_element(By.XPATH, '//span[contains(text(),"关注")]') |
| CSS Selector | find_element(By.CSS_SELECTOR, 'div.content > p') | driver.find_element(By.CSS_SELECTOR, '.avatar img') |
定位到元素之后,你可以做各种操作:点击、输入文字、获取文本、获取属性等。
# 输入文字
search_box = driver.find_element(By.NAME, 'q')
search_box.send_keys('Selenium 教程')
# 点击按钮
submit_btn = driver.find_element(By.ID, 'search-btn')
submit_btn.click()
# 获取文本
title = driver.find_element(By.CSS_SELECTOR, 'h1').text
print(title)
# 获取属性
link = driver.find_element(By.TAG_NAME, 'a').get_attribute('href')
注意: 我曾经踩过一个坑——用 find_element 时,如果页面上有多个匹配的元素,它只返回第一个。如果你想获取所有匹配的元素,要用 find_elements(注意是复数)。
等待策略
这是 Selenium 抓取中最容易出问题的地方。为什么?因为页面是动态加载的。你定位一个元素时,它可能还没出现在 DOM 里。
你想想看,你打开一个社交媒体页面,数据是通过 Ajax 请求加载的。网络有延迟,JS 执行也需要时间。如果你在数据还没加载完就去定位,那肯定报错——NoSuchElementException。
怎么解决?等。
Selenium 提供了两种等待方式:
1. 隐式等待
设置一个全局的超时时间。只要在这个时间内找到了元素,就继续执行。如果超时了还没找到,就报错。
driver.implicitly_wait(10) # 最多等 10 秒
这个设置一次,对整个 driver 生命周期都有效。但有个问题:它只会在 find_element 时等待,不会等待其他条件(比如元素可点击)。
2. 显式等待
这个更灵活。你可以针对某个特定条件进行等待,比如元素可见、可点击、文本出现等。
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 等待直到某个元素可见
element = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.ID, 'my-element'))
)
# 等待直到某个元素可点击
button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, '//button[text()="加载更多"]'))
)
button.click()
我的建议: 在实际项目中,我几乎只用显式等待。隐式等待虽然方便,但不够精确。比如你等一个弹窗出现,用隐式等待可能等不到,因为弹窗是异步加载的。显式等待可以精确控制「等到什么条件满足才继续」。
常用的预期条件(Expected Conditions)包括:
presence_of_element_located:元素出现在 DOM 中(不一定可见)visibility_of_element_located:元素可见element_to_be_clickable:元素可点击text_to_be_present_in_element:元素中包含指定文本alert_is_present:弹窗出现
我曾经在抓取微博评论时,遇到一个很头疼的问题:评论是分页加载的,每次点击「加载更多」后,新评论要等 1-3 秒才出现。如果用 time.sleep(3),太慢了;如果用隐式等待,又经常报错。最后我用显式等待,等新评论的容器出现后再继续,效率提升了一倍。
知识体系图
下面这张图,是我自己梳理的本章知识结构。你可以把它当作一个「思维导图」来看。
总结一下: 动态内容抓取的核心就三件事——让浏览器跑起来(Selenium)、找到你要的东西(元素定位)、等它出现(等待策略)。这三件事做好了,大部分动态页面你都能搞定。
嗯,这一章的内容就这些。下一章我们会聊更高级的话题——如何处理反爬虫机制,比如验证码、IP 封禁、请求头伪装等。到时候再细说。
公众号:蓝海资料掘金营,微信deep3321