ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP爬虫遭遇Incapsula防护?一文详解JS挑战绕过与实战方案

PHP爬虫遭遇Incapsula防护?一文详解JS挑战绕过与实战方案 最近在用PHP写一个数据采集工具目标站点碰上了Incapsula防护请求发过去直接给你甩一个403页面里一串JS浏览器打开正常脚本一跑就拦。折腾了两天查了不少资料踩了不少坑总算把问题理顺了。这篇文章就把整个过程记录下来包括Incapsula的防护原理、识别特征、PHP侧的应对思路以及一些常规文档里不会写的细节。如果你正好也在用PHP写爬虫并且撞上了这类JS挑战防护这篇文章应该能帮你省下不少试错时间。即使你用的是Python或其他语言思路也是通用的。1. 先搞清楚Incapsula到底在拦什么1.1 它是什么长什么样Incapsula现在叫Imperva Incapsula是一套网站安全防护服务很多海外站点都在用国内的金融机构、跨境电商站点也偶尔能见到。它做的事情简单说就是在用户和源站之间加一道墙通过CDN节点接管所有请求再用一套规则判断请求是真人浏览器还是自动化脚本。很多刚开始写爬虫的人对它的认知就停留在“难搞”两个字上但难搞也得搞明白它为什么难搞。Incapsula的核心能力不是简单的IP封禁而是带状态的动态防护——每次浏览器访问它会下发一段JavaScript代码浏览器执行完后生成一个加密Cookie后续每一次请求都得带上这个Cookie而且是动态变化的。这就让传统的“伪造User-Agent 挂代理”方案直接失效了。1.2 防护机制的三个层次Incapsula的防护大致分三层越往后越难绕过。第一层是IP信誉检测。它会看你的IP是不是机房IP、是不是已知的爬虫来源、有没有历史攻击记录。这一层最基础但也最容易触发很多爬虫连JS挑战都没见到就先被IP信誉这一关拦下了。机房IP在这层基本是原罪家庭宽带IP会好很多。第二层是行为指纹检测。浏览器访问页面时会有很多微妙的特征——HTTP头顺序、TLS握手指纹、请求频率、鼠标轨迹、页面停留时间等。Incapsula会综合这些信息判断你是真人还是脚本。很多爬虫框架发出去的请求头顺序跟真实浏览器不一样TLS指纹更是天差地别这一层就把大部分脚本筛掉了。第三层是JS挑战下发。如果前两层没有完全判定你是爬虫它会给你返回一个包含JavaScript代码的页面。浏览器会自动执行这段JS计算出结果然后带着结果重新请求一次拿到合法的会话Cookie最后才看到真实内容。而PHP脚本默认不执行JavaScript所以会卡在这一步。理解这三层很关键因为后续的所有应对方案本质上都是围绕这三层防护在做针对性处理。2. 识别Incapsula的五个典型特征2.1 从HTTP响应看特征如果你不确定目标站点是不是用了Incapsula最直接的办法是看响应头。当请求被拦截时返回的响应头里通常会带X-Iinfo字段这个字段的值是一串经过编码的数据。另一个典型特征是Cookie名称Incapsula下发的Cookie一般以visid_incap_、incap_ses_、nlbi_开头看到这些前缀基本可以确认就是它了。响应页面本身也有特征。被拦截时返回的页面里通常包含Incapsula字样有时候会直接显示一个类似“Not Found”的占位页但源码里能看到incapsula的资源引用地址比如cdn.incapsula.com下的脚本。还有一种是返回一个带进度条的等待页几秒后通过JS跳转这种也是典型的JS挑战场景。我自己常用的方式是把响应头全部打印出来看一眼。下面这段代码是基于Guzzle写的?php require vendor/autoload.php; use GuzzleHttp\Client; $client new Client([ cookies true, headers [ User-Agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, ], ]); $response $client-get(https://example.com); $headers $response-getHeaders(); if (isset($headers[X-Iinfo][0])) { echo 检测到 Incapsula X-Iinfo: . $headers[X-Iinfo][0] . PHP_EOL; } $setCookie $headers[Set-Cookie] ?? []; foreach ($setCookie as $cookie) { echo Set-Cookie: . $cookie . PHP_EOL; }2.2 从Cookie名称判断防护强度拿到了Cookie信息后可以进一步判断当前处于哪个防护阶段。如果是visid_incap_*开头的Cookie说明只是记录了访客身份的标识不算强拦截。如果出现incap_ses_*说明已经触发了一次会话级JS挑战这个Cookie携带的是会话验证信息。如果在请求后能顺利拿到正常业务Cookie说明你已经通过了完整的验证流程。这里有一个容易踩的坑很多人看到visid_incap_*就以为Incapsula不过如此直接带着这个Cookie继续请求结果下一跳还是403。这是因为visid_incap_*只是访客ID真正决定你能不能访问的是incap_ses_*这个会话凭证。只有两个Cookie都齐了才算是正常的已验证状态。2.3 响应耗时异常也是信号还有一个不太被人注意的识别方式——响应耗时。正常请求如果是200可能在100-300毫秒内就返回了。被JS挑战拦截时响应时间通常在800毫秒以上有时候甚至超过2秒。这是因为Incapsula在返回JS页面前还在后台对你的请求做了一轮行为检测。如果你发现请求耗时忽高忽低而且高于正常水平八成是在执行额外检测。3. PHP爬虫被拦截的根本原因3.1 请求头不完整一看就是脚本很多刚入门的朋友写爬虫时代码里就三行设置一个User-Agent、发请求、打印结果。问题恰恰出在这里。真实浏览器发出去的一个GET请求光请求头就有十几个字段Accept、Accept-Language、Accept-Encoding、Referer、Origin、Sec-Fetch-Dest、Sec-Fetch-Mode、Sec-Fetch-Site等。Incapsula这类防护系统会对请求头做一致性校验如果你的请求头只有孤零零一个UA它不需要走后面的JS挑战流程直接在第一层就把你判定为异常流量。PHP里用cURL写爬虫时很多人只设置了UA和超时时间。对比一下真实浏览器和爬虫脚本的请求头差异就会发现差距非常明显。解决思路是拿浏览器开发者工具里的请求头完整Copy下来在代码里尽量1:1还原。注意请求头的顺序也最好保持一致虽然PHP的cURL会改变一些顺序但尽量接近总比大相径庭要好。3.2 Cookie是空的等于裸奔Incapsula的核心验证机制就是Cookie。浏览器第一次访问时Incapsula设置一个初始Cookie然后通过JS计算出一个新的Cookie值后续请求都得带着这个计算后的Cookie。PHP的原生cURL调用如果不做Cookie管理每次请求都是“无状态”的。也就是说即使你拿到了Cookie下一次请求没带上防护还是把你当成新访客。问题不在于你能不能拿到Cookie而在于你有没有把Cookie存下来、带上。解决方法是使用Guzzle的Cookie中间件或者手动维护Cookie文件。下面这段代码展示了用Guzzle维持Cookie会话的写法?php require vendor/autoload.php; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; $jar new CookieJar(); $client new Client([ cookies $jar, headers [ User-Agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language en-US,en;q0.5, Accept-Encoding gzip, deflate, br, Connection keep-alive, ], verify false, ]); $response $client-get(https://example.com); // 打印当前CookieJar中所有Cookie foreach ($jar-toArray() as $cookie) { echo $cookie[Name] . . $cookie[Value] . PHP_EOL; } echo HTTP状态码: . $response-getStatusCode() . PHP_EOL;3.3 请求频率太大触发了行为分析还有一个常见原因就是请求频率。有些采集脚本为了追求速度循环里连sleep都不写一秒发几十个请求产生的流量模式跟正常人完全不一样。即使你解决了JS挑战这么频繁的请求也会触发它的行为分析模块轻则让你过一会儿再访问重则直接封IP。这里给一个参考值如果是中小规模采集建议单IP请求间隔控制在5到15秒之间如果数据量比较大优先考虑分布式多IP方案而不是在一个IP上把频率拉满。控制频率不是怕封IP而是降低被行为分析标记的概率。4. 实操PHP应对Incapsula的完整方案4.1 方案一用无头浏览器拿CookiePHP负责请求这是我目前认为最稳、也最适合PHP生态的方案。思路很清晰写爬虫的PHP代码不直接访问目标站点而是先通过无头浏览器Headless Chrome完成JS挑战获取到合法的Cookie然后把Cookie交给PHP的请求客户端使用。这么做的核心原因是Incapsula的JS挑战由真实的V8引擎执行PHP本身没有像样的JS执行环境与其费劲去逆向那个JS算法不如直接用真实浏览器解决。这也是目前业界应对这类防护的主流做法。具体需要两个工具配合。第一个是Puppeteer或Playwright用于驱动无头浏览器完成挑战。第二个是PHP侧的请求库比如Guzzle或原生cURL。它们之间的数据流转就是Cookie。用Playwright的PHP绑定或者通过Node脚本单独服务都可以我更推荐的方式是写一个独立的Node脚本专门负责取CookiePHP脚本通过命令行调用它?php function getIncapsulaCookies($url) { $script __DIR__ . /get_cookie.js; $cmd node . escapeshellarg($script) . . escapeshellarg($url) . 21; $output shell_exec($cmd); $cookies json_decode(trim($output), true); if (!$cookies) { throw new Exception(获取Cookie失败: . $output); } return $cookies; } // 调用示例 $cookies getIncapsulaCookies(https://example.com); print_r($cookies);对应的get_cookie.js脚本大致长这样const puppeteer require(puppeteer); (async () { const url process.argv[2]; const browser await puppeteer.launch({ headless: new, args: [--no-sandbox, --disable-setuid-sandbox] }); const page await browser.newPage(); await page.setUserAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36); await page.goto(url, { waitUntil: networkidle2, timeout: 30000 }); // 等待JS挑战完成Cookie写入 await page.waitForTimeout(5000); const cookies await page.cookies(); console.log(JSON.stringify(cookies)); await browser.close(); })();拿到Cookie后把它序列化到文件或Redis里PHP请求时再读出来用。这样可以避免每次请求都启动一个浏览器节省资源。4.2 方案二纯PHP模拟JS挑战流程这个方案对技术能力要求比较高思路是自己去分析Incapsula下发的JS代码找到它的加密算法然后在PHP里用代码重现计算过程直接生成合法的Cookie值不需要真实浏览器执行。说实话这个方案的难度不小。Incapsula的JS代码是混淆过的而且会定期更新算法。你花一周时间逆向出来的算法可能在下次更新后就失效了。所以这个方案适合以下场景你的采集量非常大浏览器方案满足不了性能要求并且你有足够的时间和能力去持续维护逆向逻辑。如果你确实想走这条路推荐的工具是PHP V8扩展或QuickJS。PHP V8扩展可以让PHP代码直接执行JavaScript代码这样就能复用Incapsula下发的原始JS逻辑不用自己逆向算法。大致思路是把返回页面里的JS代码提取出来去掉一些重复的检测逻辑然后在V8里执行拿到结果Cookie。但这里涉及一个关键知识点Incapsula的JS执行时会发起一些额外的网络请求或者操作DOM这些在纯净的V8环境里是跑不起来的。所以还得在PHP里模拟出基本的DOM操作接口。说白了你自己在PHP里实现一个迷你浏览器环境。这个工程量非常大不推荐新手尝试。4.3 方案三Cookie池与并发请求设计解决了单次请求的Cookie问题后下一步要考虑的是规模化和并发时的稳定性。这里我分享一下自己的实践。先说Cookie池的思路。无头浏览器取出Cookie后不要用完就丢而是缓存起来设置一个合理的过期时间。Incapsula的会话Cookie通常有10到20分钟的有效期过了有效期会重新校验。所以可以做一个后台任务每隔几分钟用浏览器刷新一批新Cookie放到池子里请求时随机从池里取一个使用。这样即使其中一个Cookie失效池子里还有其他可以用的。再来看并发。PHP处理并发本身就有点吃力特别是跟常驻内存的Node或Python相比。如果一定要用PHP做并发采集建议用Swoole扩展或直接上多进程。cURL的curl_multi_*函数也可以做简易并发但控制粒度太粗容易触发防护。更合适的方案是把任务拆分成小块放到Redis队列里用多个PHP进程同时消费。每个进程负责一个独立的Cookie和独立的IP频率各自控制。我自己用过一段时间的Swoole协程方案配合Redis队列做了分布式采集。实际效果是单个进程每5秒能完成一个请求20个进程叠加就是每秒4个请求在不对目标站点造成压力的前提下这个采集效率已经足够满足大多数业务需求了。关于“爬虫并发设计到底哪个好”我个人的看法是没有最好的方案只有最合适的方案。单机低频率就够的业务没必要上分布式真到了需要分布式的量级重点考虑的不是用什么语言写爬虫而是怎么管理好Cookie、IP和任务队列这三件事。4.4 请求头完整组装示例写爬虫最忌每次“裸奔”。这里给一份我实践下来比较稳的请求头模板供参考$headers [ User-Agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language en-US,en;q0.5, Accept-Encoding gzip, deflate, br, Referer https://www.google.com/, Sec-Fetch-Dest document, Sec-Fetch-Mode navigate, Sec-Fetch-Site cross-site, Cache-Control max-age0, Connection keep-alive, ];这几个字段缺一不可吗也不完全是。但如果目标是尽量模拟真实浏览器这些字段最好都带上。特别是Sec-Fetch-*系列它们是现代浏览器的标配大量爬虫脚本没有这些字段Incapsula的指纹模型会非常敏感。4.5 验证码兜底即使做好了以上所有工作还是有概率触发验证码。Incapsula在检测到可疑但不确定的情况下会下发一个验证码页面需要用户拖动滑块或点击图片。这种情况下脚本就无能为力了只能接打码平台。国内外的打码平台很多我个人测试下来国外的2Captcha对Incapsula的支持还算可以但价格不便宜。如果你只是小规模采集遇到验证码就直接换IP、换Cookie重试成本比接平台低得多。只有采集量非常大时才值得考虑接入打码平台。5. 常见问题与排查技巧实录5.1 常见问题速查表现象直接原因解决方案请求直接返回403无任何Cookie下发IP信誉差或请求头异常更换IP完整还原浏览器请求头返回JS挑战页面但无头浏览器执行后仍是403Cookie被关联到浏览器指纹跟后续请求指纹不一致确认后续请求使用的TLS指纹与无头浏览器一致Cookie拿到了但PHP请求仍被拦截Cookie未正确传递或Cookie已过期检查CookieJar是否正常工作重新抓取Cookie请求偶尔成功偶尔失败单IP请求频率过高触发行为分析降低请求频率增加请求间隔同一Cookie短时间内有效过一会就失效Incapsula动态会话机制实现Cookie定期刷新机制页面能打开但关键数据接口返回429接口层面有独立的频率限制单独控制接口请求频率5.2 我踩过的几个坑第一个坑是只Copy了请求头忽略了TLS指纹。PHP的cURL默认使用OpenSSL的指纹特征跟Chrome完全不一样。即使你的请求头做得再像TLS层一握手就露馅了。解决方式是给cURL设置与Chrome一致的加密套件列表和TLS版本。Guzzle底层其实就是cURL所以同样适用。设置方式是在cURL选项中手动指定ciphers。第二个坑是无头浏览器被检测到。早期的Puppeteer用headless: true启动很容易被识别因为它的navigator.webdriver属性是true而且没有正常的插件加载。现在Chrome的headless: new模式已经改进了很多但为了保险我建议给Puppeteer加一些反检测参数比如隐藏webdriver标记、加载自定义插件等。第三个坑是只刷新Cookie不刷新IP。Incapsula会把Cookie和IP做绑定。如果你用A IP获取的Cookie换到B IP去用它大概率会被判定为异常。所以Cookie池要跟IP池绑定使用一组IP对应一组Cookie不能混着用。第四个坑是GC时间。PHP的垃圾回收机制在高并发请求下可能会拖慢响应导致取到的Cookie超时。建议在CLI模式下跑长任务时显式设置gc_enable()和gc_collect_cycles()并合理设置max_execution_time 0。5.3 监控是必须的做爬虫可不是发了请求就完事要有一整套监控机制。我习惯在代码里记录三个关键指标请求成功率、平均响应时间、Cookie有效时长。这三个指标能帮你快速定位问题出在哪个环节。如果成功率突然下降优先检查是不是Cookie池过期了如果响应时间变长可能触发了行为分析需要降低频率如果Cookie有效时长越来越短说明目标站点调整了防护参数需要重新审视方案。监控不一定要做得多复杂一张简单的SQLite表或者Redis里的几个计数器就够了。关键是出了问题能找到判断依据。6. 写在最后的几点体会做了一段时间对抗Incapsula的采集我的感觉是这类防护真正难的不是技术而是心态。你会不断遇到今天能跑、明天跑不了的情况。如果每次变更都从头开始逆向很快就会耗尽耐心。更务实的做法是第一保持代码模块化Cookie获取、请求发送、频率控制、数据解析这四块逻辑尽量解耦方便在某一层失效时只改那一层。第二主动降低采集频率很多防护是被高频率触发出来的慢一点反而能长期稳定运行。第三一定要重视数据源的价值如果目标站点数据是核心业务依赖考虑购买官方API或者走商务合作这比跟防护系统死磕划算得多。另外很多人在群里问“PHP是不是不适合写爬虫”每次遇到这类问题我都很感慨。语言只是工具。我用PHP写爬虫三四年了最开始的单进程脚本到后来的Swoole协程、Redis队列分布式方案PHP一样能搞定不少事情。现在网上搜爬虫教程十篇里有八篇是Python但PHP的生态也不差——Guzzle、Symfony HttpClient、Swoole再加上Composer管依赖完全够用。关键还是要理解HTTP协议、理解浏览器行为、理解目标站点的防护逻辑这些基础打牢了用什么语言都无所谓。最后分享一个小技巧如果你实在搞不定某站的Incapsula试着在目标站点的移动端页面或旧版页面找找突破口。很多站点只对主站域名启用了最强防护对子域名、移动端或API接口的防护等级会弱一些。当然前提是你得确认自己的采集行为在合规范围内别碰robots.txt里明确禁止爬取的内容。
返回列表