恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Selenium实战指南:解决JavaScript渲染页面爬取难题
首页
资讯中心
/
Selenium实战指南:解决JavaScript渲染页面爬取难题
Selenium实战指南:解决JavaScript渲染页面爬取难题
发布时间:2026/10/9 10:38:37
做爬虫做到后期基本都会撞上同一种尴尬requests写得很溜正则和XPath也都顺手结果拿到某个网站一看response.text里干干净净连数据的影子都没有。这不是你代码写错了是页面压根没把数据放在初始HTML里——所有内容都是网页里的JavaScript脚本在你打开页面的瞬间去请求接口、拼装DOM、再填进去的。我自己的习惯是先从浏览器里按F12看一眼“网络”面板如果发现大量XHR请求在页面加载之后才发起那就基本可以判断这个页面需要渲染光靠requests是搞不定的。这篇文章就围绕“怎么用Selenium搞定JavaScript渲染的网页”来说。先说原理和选型再给一套可以直接复用的代码骨架然后拆几个实际开发中高频遇到的动态处理场景最后把我和团队这两年踩过的坑整理成问题排查清单。适合已经会基础爬虫、现在正被动态页面折磨的朋友参考。1. 为什么需要处理JavaScript渲染requests的局限与动态网页的真相1.1 动态页面的真相浏览器里到底发生了什么一个现代网页在你浏览器里显示出来至少要经历这么几步浏览器先拿到一份HTML骨架然后按里面的script标签去加载JavaScript文件JS执行之后再去请求后端接口也就是XHR或fetch拿到JSON数据之后再通过DOM操作把数据渲染成你看到的文字、图片、表格。整个过程里初始的HTML可能只是一个空壳真正有价值的数据全部发生在“JS执行之后”。requests这类库的本质是什么是模拟浏览器发一个HTTP请求拿回服务器响应的原始内容。它不会执行JavaScript不会等异步回调也不会帮你做DOM渲染。所以当页面里的数据是JS动态拼出来的你requests到的HTML里自然什么都没有只能在那一堆script代码里看到几个接口地址拿不到实际内容。打个简单的比方requests是直接去饭店后厨拿菜单看一眼原材料而浏览器是等厨师做完菜、端上桌之后你再动筷子。动态页面的数据往往是“端上桌”之后才出现的你要么自己照着菜单接口列表手动做一遍模拟请求要么就让浏览器把整桌菜端出来你再直接吃渲染页面。1.2 requests的短板和哪些页面必须上Selenium在实际项目里我判断一个页面要不要上Selenium基本看三个特征。第一个是懒加载。典型表现是页面滚动到底部才加载下一批数据最直接的现象就是requests拿到的HTML里图片和列表项全是空的或者占位符。第二个是Ajax翻页点击下一页时URL没变页面局部刷新这种用requests模拟非常麻烦因为你得先逆向找接口、分析参数加密方式成本往往比写个Selenium脚本还高。第三个是高度依赖用户交互的场景比如先登录、再拖拽、再点击某个按钮才能看到的数据这种业务逻辑本身就是一种“页面流程”Selenium这种真实浏览器驱动的方式天然能覆盖。当然我并不是说Selenium是唯一解。很多情况下逆向接口才是更省资源、更高性能的方案。但如果接口参数加密复杂、验证逻辑多、或者你拿不到接口格式那就别硬刚了直接交付渲染方案。我个人的判断标准是如果逆向接口的成本预计超过一天而页面结构不复杂那就直接上Selenium。后者虽然慢但开发周期短、稳定性高、维护起来也直观。2. Selenium原理与选型让浏览器替你去“看”2.1 WebDriver的工作方式Selenium的核心机制是WebDriver。WebDriver不是Selenium自己发明的魔法而是一套浏览器自动化的协议标准相当于“遥控器”。它通过浏览器厂商提供的驱动程序比如ChromeDriver、GeckoDriver和浏览器真实通信你写Selenium代码告诉WebDriver“打开这个URL”“点击这个按钮”“把页面截图给我”WebDriver把这些指令翻译成浏览器能懂的底层命令然后浏览器真的去执行。这里面有个关键点Selenium操作的是真实的浏览器实例。也就是说页面里的JavaScript是浏览器内核自己执行的不是Selenium模拟的。你执行JS、发Ajax、改DOM、跳转页面统统都是浏览器自己的行为所以它在“对抗”动态渲染时效果等同于人工操作浏览器。现在做浏览器自动化除了Selenium还有Playwright和Puppeteer这两个选择。Puppeteer是Node.js生态的主要绑定ChromiumPlaywright是微软出的支持多浏览器和多语言API设计也更现代。为什么我这篇还坚持以Selenium为例因为它在Python爬虫生态里最普及网上资料多团队协作时找人接手最容易。如果你是从零开始且没有历史包袱也可以了解一下Playwright但如果你用的是Python、项目里又要连老代码Selenium依然是稳妥的选择。2.2 环境搭建与驱动版本匹配Selenium的环境搭建看起来简单实际上最容易出幺蛾子的就是浏览器驱动版本不匹配。简单说你的Chrome浏览器升了级旧版ChromeDriver可能就驱动不了它了运行时会直接报SessionNotCreatedException。我现在的标准做法是两步走。第一步直接用pip安装selenium库pip install selenium第二步不要手动去下载ChromeDriver直接用第三方库webdriver-manager自动处理版本匹配。这个库会检测你本机浏览器版本然后自动去下载对应的驱动还能缓存起来省得每次换版本都要折腾。from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)如果你所在的网络环境下载驱动比较慢也可以手动去网站下载对应版本然后指定路径service Service(/path/to/chromedriver) driver webdriver.Chrome(serviceservice)这里有个我在实战中总结的版本对应关系ChromeDriver的大版本号必须和Chrome浏览器的大版本号一致比如浏览器是120.x驱动也得是120.x小版本可以略有出入。还有一点如果你跑脚本在服务器上服务器通常自带的是无图形界面的Linux环境那就必须用无头模式这个我在下一节详细说。3. 手把手写一个可复用的渲染爬虫3.1 启动配置无头模式与稳定性调优很多人第一次用Selenium写出来的代码是能跑但一部署到服务器就崩。原因十有八九是没处理无头模式和资源占用。我先把一套我一直在用的通用配置贴出来再逐行解释关键点。from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) options.add_argument(--user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36) options.add_experimental_option(excludeSwitches, [enable-automation]) driver webdriver.Chrome(optionsoptions) driver.set_page_load_timeout(30) driver.set_script_timeout(10)这里面的参数逐个说。headlessnew是无头模式有的老教程写的是headless新版Chrome推荐--headlessnew兼容性更好。no-sandbox和disable-dev-shm-usage是Linux服务器上必备的否则经常报错因为容器的共享内存太小disable-gpu是为了避免GPU相关的崩溃虽然浏览器现在已经是软件渲染为主但加上这个参数在某些环境下能少很多坑。window-size很重要很多页面会判断视口宽度来决定是否加载移动端页面为了模拟桌面访问我一般固定成1920x1080。user-agent设置成真实浏览器的默认UA能挡掉一部分憨憨的爬虫检测。set_page_load_timeout是页面加载超时时间有些网站某个资源卡住了会导致整个脚本卡死设个30秒就能及时跳过。set_script_timeout是执行JS脚本的超时时间后面用execute_script的时候有用。3.2 定位元素find_element与XPath的选择拿到渲染完成的页面之后剩下的就是定位元素、提取数据。Selenium定位元素的方式很多find_element和find_elements是最常用的两个方法。前者返回第一个匹配的元素后者返回所有匹配的元素列表。选哪个看你需求批量抓列表的时候用find_elements点单个按钮的时候用find_element。定位策略我基本只用两种XPath和CSS Selector。CSS Selector比较简洁比如div.product-card .title适合结构清晰的静态页面。XPath虽然啰嗦但灵活性碾压CSS尤其是在结构混乱、语义模糊的动态页面里XPath可以通过“包含文本”“找父节点”“按位置取元素”等方式绕过很多坑。举个例子你要找页面上“加载更多”那个按钮但这个按钮的class是动态变化的只有英文文本是“Load More”稳定不变。用CSS基本没法写用XPath就很简单button driver.find_element(By.XPATH, //button[contains(text(), Load More)])contains(text(), ...)可以在XPath里做模糊匹配对付那些“总是带个数字角标”的按钮非常有效。还有一个我经常用的组合//div[contains(class, product)]//span[classprice]意思是先定位所有包含product关键词的div再在它们下面找价格span这样定位每一行商品数据非常自然。用XPath定位之后提取数据我一般优先拿元素的text属性而不是直接解析HTML字符串。因为Selenium的element.text拿到的是浏览器渲染之后“真正可见的文字”省去了清洗一堆隐藏标签的麻烦。如果你更习惯用XPath直接提取文本也可以用element.find_element(By.XPATH, .//span).text这种写法注意//前要加个点表示在当前元素内部查找。3.3 等待机制别再用死等新手最容易犯的错是定位不到元素就直接time.sleep(5)硬等。我不推荐这种做法原因很简单网络快慢是动态的固定等待要么等久了浪费大量时间要么等短了就飘。正确做法是使用Selenium的显式等待WebDriverWait。显式等待的核心思路是“轮询直到满足某个条件否则超时”。我举两个高频场景。场景一等某个元素出现from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC element WebDriverWait(driver, 20).until( EC.presence_of_element_located((By.XPATH, //div[classitem])) )场景二等某个元素可点击比如弹窗确认按钮button WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.XPATH, //button[text()确定])) ) button.click()还有一类更难搞的有的网站先是显示一个加载动画动画结束才出现真实数据。这时候如果只等元素出现可能等到的是loading占位符。我通常的做法是配合EC.invisibility_of_element_located等加载动画消失或者直接等我们真正关心的那个“内容节点”出现。判断“真正关心”的节点标准看数据是不是已经渲染进去了比如某个容器内的子元素数量大于0。用显式等待还能顺带解决页面渲染太快和太慢的矛盾等不到就超时报错等到了就立即往下执行不用人为估时间。3.4 获取渲染后的页面数据整个页面渲染完成之后如果你想自己做解析可以用driver.page_source拿到当前完整的HTML源码然后再丢给BeautifulSoup处理。这个方式的优势是可以用你熟悉的BeautifulSoup的API劣势是Selenium拿到的源码和你在浏览器开发者工具里看到的DOM可能略有差异因为页面又额外执行了脚本更新了DOM。如果你想拿的是JSON数据也可以继续用Selenium执行JS去读取某个全局变量或者接口返回的数据。我用过一种思路在页面里翻页、滚动、点击之后直接执行JS把某个接口响应缓存从全局变量里捞出来。这一步放到下一章的execute_script里细讲。另外提醒一下page_source拿到的是“此时此刻的DOM”不是初始HTML。你要是想保存“刚打开没等渲染”的版本必须在浏览器发出请求后立刻获取这个意义不大因为我们要的就是渲染后的内容。所以不要纠结源码差异重点永远放在你要提取的业务数据上。4. 处理动态内容的几种硬核实战技巧4.1 无限滚动加载现在很多信息流页面不做翻页按钮滚动到底部自动加载下一批数据。Selenium处理这种场景的思路很直白模拟滚动触发加载等待新元素出现再统计数量。last_height driver.execute_script(return document.body.scrollHeight) while True: driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(2) new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: break last_height new_height我简单说下这段代码的逻辑先记录当前页面的滚动高度然后滚动到底部触发加载等几秒让页面渲染出新的内容再重新读取滚动高度。如果高度没变说明没有新的内容加载出来了循环结束。time.sleep(2)在这里是有意的因为它要等的是一个“异步过程”不是某个元素出现所以显式等待不太好写我就用短停顿轮询。有一点要注意如果页面存在“加载更多”按钮而不仅是滚动加载你可以把滚动操作替换成按钮点击然后加上显式等待。还有一些页面滚动到最低端时会出现遮罩层和弹窗需要在滚动之后做个判断如果出现了弹窗就关掉再继续。另外滚动加载很容易触发反爬限流建议每滚动几次就随机停顿一下不要保持固定频率像人操作一样有快有慢请求频率更自然。4.2 iframe里的内容怎么抓还有一种动态页面让人抓狂数据在一个iframe子框架里你直接在driver.page_source里搜根本搜不到定位元素也报找不到。这是因为iframe是独立的文档Selenium默认操作的是主文档必须先把上下文切进去。iframe driver.find_element(By.XPATH, //iframe) driver.switch_to.frame(iframe) # 此时可以正常查找iframe里面的元素 data driver.find_elements(By.XPATH, //div[classinner]) # 用完切回主文档 driver.switch_to.default_content()这里有个细节iframe的定位和普通元素一样可以用XPath但有些iframe加载是动态的你切之前最好先等待它出现。切完之后你要清楚当前在哪个文档里如果iframe里又有嵌套iframe还得一层一层切千万别切乱了。我处理iframe的一个习惯是先花两分钟在浏览器里手动看下这个页面有哪些iframe它们的id或者class是什么。只要能确定一个稳定的定位方式代码就好写了反而最忌讳上来就写//iframe不带任何筛选条件页面里要是同时有好几个iframe跟你期待的那个根本不是同一个。4.3 弹窗和窗口切换点击按钮触发新窗口的场景也常见尤其是那种点击之后跳到一个新的Tab页展示详情而且新页面还是异步加载的。这时候如果你直接driver.find_element去新页面的元素会发现找不到因为Selenium还在旧的窗口上下文里。处理方法是先记录当前窗口句柄点击新窗口后切换到新窗口current_handle driver.current_window_handle driver.find_element(By.XPATH, //button[text()查看详情]).click() WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2)) new_handle [h for h in driver.window_handles if h ! current_handle][0] driver.switch_to.window(new_handle)number_of_windows_to_be(2)是等新窗口出现这是处理这类问题的关键。如果你不等就直接去取window_handles很可能取到的还是旧的那一个。还有一种弹窗是JavaScript的alert框它和HTML元素不一样普通定位是抓不到的要用driver.switch_to.alert处理然后可以调用它的accept或dismiss方法。老实说这种alert弹窗在现在的互联网页面上已经比较少了但偶尔在旧系统或者一些后台管理系统里还是会碰上。4.4 直接执行JS获取渲染数据Selenium有一个隐藏利器execute_script。它可以让你在浏览器的当前页面上下文中执行任意JavaScript代码这意味着你可以直接读取JS全局变量、修改DOM属性、触发自定义事件还能拿到一些常规定位方式很难获取的数据。比如有些页面把渲染数据放在一个全局变量里比如window.__INIT_STATE__里面包含了完整的JSON数据。如果用XPath去逐个提取可能得写几十行代码直接一条JS把它取出来方便很多data driver.execute_script(return window.__INIT_STATE__;)拿回来之后你会发现这往往是一段JSON字符串或者一个对象用Python的json模块或者json.loads处理一下就变成了字典列表后续想怎么清洗就怎么清洗。execute_script还有一个用途动态触发事件。有的按钮不是普通click能触发的比如React里的事件绑定比较复杂你直接.click()没反应这时候用element.click()配合execute_script(arguments[0].click();, element)往往就能顺利触发。这个技巧在对付某些用框架写的后台页面时非常管用我把它列为“爬虫必会的救命招数”之一。5. 高频踩坑问题与排查实录5.1 元素找不到先分清楚In Web和In DOM的差异大家最常遇到的就是NoSuchElementException。不少人的第一反应是加等待时间但我要说的是先别急按照下面这个顺序排查。第一步确认你要找的元素是不是在iframe里如果是先切框架。第二步确认元素是不是在新窗口里如果是先切窗口。第三步确认元素是不是隐藏状态的有些元素要等展开后才存在第四步复制控制台里元素的XPath在Selenium代码里试试。最后还要检查一个非常隐蔽的问题页面里有没有多个相同文本的元素。XPath的text()匹配到多个值时find_element默认只取第一个往往不是你要的那个。这种情况建议把XPath写得更精确一点比如加上层级限定或者直接用find_elements遍历筛选。我自己的习惯是在写定位脚本之前先花几分钟用浏览器开发者工具确认一下页面结构而不是急着写代码。页面结构看得越清楚后面调脚本的时间就越少。5.2 等待失效和超时别把锅全甩给Selenium有些时候你已经用了WebDriverWait也设置了20秒但元素就是一直不出来。这时候不一定是等待机制的问题可能是这个页面的加载策略和你预期不同。一种常见情况是页面初加载特别慢甚至白屏好几秒钟这时候你就算怎么等元素也还没生成。解决办法是启动参数里加上set_page_load_timeout让页面加载阶段不要卡死同时把等待条件换成“页面某个基准元素先出现”再基于它等目标元素。另一种情况是页面元素一直存在但被遮罩层盖住导致点击不了。这种用element_to_be_clickable等待时Selenium会检查元素是否可见和可点击如果被挡住就会一直等。这种时候可以先判断页面有没有遮罩有就先把遮罩关掉或者用execute_script强制触发点击。还有注意显式等待的超时时间不要设得太长。我一般主体逻辑用15到20秒超过这个时间基本可以判定页面结构变了而不是渲染太慢。宁可让它超时报错也不要让单页等待两三分钟拖垮整个采集任务。5.3 资源占用与稳定性防止脚本跑着跑着就崩Selenium开着真实浏览器内存和CPU的消耗比普通请求大得多。如果爬虫要跑大量页面长期不退出浏览器实例内存会被占满最后进程被系统杀掉或者浏览器无响应。这里我有一个实用的管理方式每处理完一批URL就主动清理页面缓存并且定期重启驱动。# 定期清理缓存 driver.delete_all_cookies() driver.execute_script(window.localStorage.clear();) driver.execute_script(window.sessionStorage.clear();)如果任务本身跑得很久可以考虑在一个采集任务中循环创建新的浏览器实例比如每抓100个页面就退出重启一次。虽然创建实例有开销但稳定性提升非常明显。而且很多网站对长时间同一个浏览器实例的采样行为比较敏感定期换新实例反而能降低被识别和限制的概率。另外无头模式也不是一定就不会崩。如果是在Linux服务器上跑建议加上--disable-dev-shm-usage参数如果容器内存本身就小尽量把浏览器并发实例控制在两三个以内不要贪多。我以前就遇到过开了8个Chrome实例直接吃满服务器内存的情况后来限制成3个任务总时长反而因为稳定性上去了。5.4 自动化特征识别做一个低调的“访客”有些网站会检测你是不是自动化工具常见的特征包括浏览器窗口里显示了“Chrome正在受到自动测试软件的控制”这种提示条、window.navigator.webdriver属性为true、检测你的鼠标轨迹是否太规律等等。我能分享的规避思路是合规范围内的常规做法。一是启动参数里排除自动化提示options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False)二是设置一个正常的User-Agent。三是尽量模拟人类操作节奏点击、滚动之间加一点随机间隔不要每次间隔都一样。至于更加复杂的手段比如打补丁、改指纹这些我不建议也不展开聊一方面这些操作可能违反使用条款另一方面也会增大被反制的风险。其实更重要的还是心态爬虫这件事技术只是其中一环合规才是底线。大规模采集前一定要先看清楚目标网站的服务条款和robots协议控制请求频率不要给目标服务器造成压力。做爬虫是为了获取信息、提升效率不是为了打一场攻防战。收尾最后还是分享一个我个人的体会Selenium这类工具入门容易精通难难就难在“页面永远在变化”。今天你写好的XPath下周网站改版可能就全部失效所以代码结构一定要留好维护余地和日志记录。我自己的做法是把定位策略尽量写得“宽容”一些比如能用contains的就不要用绝对文本匹配能用相对路径的就不要用绝对路径这样页面小改动时脚本还能继续跑。另外如果你的爬虫任务量特别大Selenium并不是最优选择它更适合中小体量、强交互逻辑的场景真到了大规模采集那一步还是要去分析接口、做分布式爬虫。但作为一整套动态页面的处理方案Selenium依然是我现在遇到“什么都抓不到”的页面时最想先试的那把锤子。希望这篇文章能让你少踩几个坑把时间花在更有价值的数据分析上。