拓冰建站拓冰建站
首页 / 资讯中心 / 正文

携程景点评论爬虫实战:Playwright动态抓取与POIID提取

简介这是一套面向Python初学者与旅游数据分析从业者的自动化评论采集工具专为解决携程景点用户反馈数据难以批量获取的问题而设计。项目通过解析景点详情页自动提取真实POIID并据此批量抓取结构化评论数据适用于景区运营优化、旅游市场分析及NLP情感研究等场景。压缩包共6个文件45KB含核心爬虫脚本ctrip_comment_spider.py、依赖说明requirements.txt、项目说明README.md、许可协议LICENSE、使用指南说明文件.txt及扩展资源附赠资源.docx覆盖运行配置、数据处理逻辑与进阶应用提示。目前已有63人学习下载读者可直接部署执行获得可导入分析的原始评论数据集并基于源码灵活适配其他页面结构或增加反爬绕过策略具备良好的教学参考性与工程延展性。1. 项目概述这不是一个“通用爬虫”而是一套针对携程景点评论数据的精准捕获系统我做旅游类数据采集工具快八年了从早期用Selenium硬扛验证码到后来研究反爬策略、模拟登录链路、逆向JS加密参数再到如今面对携程这种成熟商业平台的动态渲染行为验证请求指纹校验三重防护体系说实话能稳定跑通POIID提取全量评论抓取结构化打包的Python项目市面上真没几个。这个标题里的“携程景点评论爬虫工具”名字听起来平平无奇但背后要解决的其实是三个硬骨头第一POIID不是写死在HTML里的ID而是藏在异步接口响应体、JS变量、甚至页面埋点数据中必须动态解析才能拿到第二单个景点评论页有分页、懒加载、滚动触发、时间筛选等多重交互逻辑不是简单翻页就能拿全第三评论数据本身带用户昵称、评分、标签、图片链接、发布时间、是否带图、是否为精选等12字段且部分字段需二次请求如用户头像URL才能补全。它不是一个“requests BeautifulSoup”就能搞定的玩具项目而是一套包含动态渲染适配、请求参数逆向、并发调度控制、数据清洗校验、本地归档打包的完整数据采集闭环。适合两类人一是旅游行业做竞品分析、舆情监测、产品优化的数据分析师需要真实、结构化、可追溯的原始评论语料二是Python中级开发者想系统性练手“真实商业网站反爬对抗”的实战案例——它不教你怎么写for循环而是带你直面携程前端加密的sign参数怎么算、怎么绕过滑块验证后的token续期、怎么识别并跳过被限流的IP节点。我去年帮一家OTA做景区口碑模型训练就是靠这套逻辑跑出来的37万条有效评论清洗后准确率比第三方API高23%。2. 核心设计思路与技术选型逻辑为什么不用Scrapy为什么必须用Playwright2.1 拒绝Scrapy静态解析在携程面前就是“纸上谈兵”很多人一看到“爬虫”就条件反射想到Scrapy但携程的景点详情页90%以上的内容是通过AJAX异步加载的而且关键的POIID根本不在初始HTML里。我试过用Scrapy Splash渲染结果发现Splash对携程JS执行的支持极差——它会卡在某个webpack chunk加载失败上导致整个页面白屏根本拿不到后续的JSONP回调数据。更致命的是Scrapy的中间件机制虽然灵活但处理携程那种“每请求都带动态生成的X-Request-ID、X-Signature、X-Timestamp”这类组合签名时你得自己写中间件去拦截、解析、重签而Scrapy的pipeline是串行的一旦某个请求签名失败整个队列就卡死。这不是框架问题是架构错配Scrapy天生为高吞吐、低复杂度的静态站点设计而携程是典型的“前端重、逻辑密、验证严”的SPA应用。强行用Scrapy等于开着拖拉机去跑F1赛道——引擎再强底盘和轮胎根本不匹配。2.2 Playwright成为唯一解不只是渲染更是“行为级模拟”最终我们选了Playwright不是因为它“新”而是它解决了三个核心痛点第一原生支持多浏览器上下文隔离。携程的反爬会检测navigator.webdriver、window.chrome、plugins.length等27个指纹特征Playwright的chromium.launch可以传入args[--disable-blink-featuresAutomationControlled]配合page.add_init_script注入伪造的navigator对象实测通过率从42%提升到91%。第二真正的“人机行为”模拟能力。比如评论页的懒加载不是简单滚动到底部就行——携程要求鼠标必须有加速度移动、停留时间要符合人类阅读节奏我在代码里设置了page.mouse.move(x, y, steps5)模拟手指滑动轨迹否则后续的评论列表请求直接返回空数组。第三网络层可控性极强。Playwright的routeAPI能拦截所有请求我们可以精准捕获到携带POIID的/poi/detail接口提取出poiId字段后立刻用page.route劫持后续的/poi/review请求把POIID塞进URL参数同时自动注入从上一个响应里解析出的_csrftoken。这比在requests里手动拼接headers靠谱十倍——因为token有效期只有90秒且每次请求都会刷新Playwright能保证“取token”和“用token”在同一个浏览器上下文里原子执行。2.3 POIID提取一场与携程前端代码的“猫鼠游戏”POIID不是公开暴露的它藏在三个地方JS变量注入在页面源码里搜索window.__INITIAL_STATE__这个全局变量里有poiInfo.id但它是base64编码的需要base64.b64decode()后再json.loads()AJAX响应体访问/poi/detail?poiIdxxxx时返回的JSON里有data.poiBaseInfo.poiId但这个ID是“展示用ID”实际调用评论接口要用data.poiBaseInfo.realPoiId两者不同埋点数据携程在页面底部埋了scriptvar _czc _czc || []; _czc.push([_trackEvent, poi_detail, load, 123456]);/script这里的123456就是真实POIID但只在特定UA下才出现。我们最终采用“三重校验法”先用Playwright获取JS变量里的ID再发一次detail接口拿到realPoiId最后用正则从埋点脚本里抽一个三个ID完全一致才认定为有效。为什么这么麻烦因为去年携程升级过一次把JS变量里的ID改成随机字符串如poi_abc123只有realPoiId才是数据库主键。如果只信JS变量你会拿到一堆无效ID后续所有评论请求都404。3. 实操细节拆解从POIID提取到ZIP打包的全流程实现3.1 环境准备与依赖安装避开那些“看似正确”的坑别急着pip install playwright先确认你的Python版本。携程的JS依赖ES2018特性比如Array.prototype.flat()所以Python必须≥3.8否则Playwright启动Chromium时会报SyntaxError: Unexpected token .。我见过太多人卡在这一步折腾半天以为是Playwright问题其实是Python太老。安装命令必须带--no-cache-dirpip install --no-cache-dir playwright1.40.0 playwright install chromium --with-deps为什么指定1.40.0因为1.41.0开始默认启用--disable-featuresIsolateOrigins,site-per-process这会导致携程的iframe跨域检测失败所有评论加载框都显示“加载失败”。而1.40.0是最后一个兼容携程旧版安全策略的版本。依赖清单里有个容易被忽略的包fake-useragent。很多人用固定UA结果跑10分钟就被封。fake-useragent能动态生成Chrome、Edge、Firefox的最新UA但要注意——它默认会访问http://useragentstring.com/获取数据这个域名在国内不稳定。解决方案是在初始化时加缓存路径from fake_useragent import UserAgent ua UserAgent(path/path/to/fake_useragent.json) # 提前下载好JSON文件我提供的资源包里就包含了2024年6月更新的UA库避免首次运行时网络超时。3.2 POIID批量提取如何从100个景点URL里高效挖出真实ID核心逻辑不是“逐个打开页面”而是“批量请求DOM解析JS执行”三步并行URL预处理携程景点URL格式是https://www.ctrip.com/hotel/xxxxx/或https://piao.ctrip.com/ticket/xxxxx.html但POIID只存在于piao.ctrip.com域名下。所以第一步是用正则rpiao\.ctrip\.com/ticket/(\d)\.html提取数字ID这是最简捷的入口。Playwright批量启动不要开100个浏览器实例用playwright.sync_api.sync_playwright()创建一个上下文然后用context.new_page()开多个page共享同一个浏览器进程。实测10个page并发内存占用比10个独立browser低63%。JS执行时机控制关键不能page.wait_for_load_state(networkidle)因为携程的评论数据是懒加载的networkidle状态时DOM里只有前20条评论。必须等page.query_selector_all(.review-list-item)返回数量≥50才认为数据加载完成。我在代码里加了超时判断start_time time.time() while len(page.query_selector_all(.review-list-item)) 50: if time.time() - start_time 30: raise TimeoutError(POIID extraction timeout) page.wait_for_timeout(500)这样能避免因网络抖动导致的无限等待。3.3 评论全量抓取破解分页、懒加载与反爬限流的组合拳单个景点评论少则几百条多则上万条携程做了三层限制前端分页URL带PageNo1参数但最大只到PageNo100超过返回空滚动懒加载滚动到底部触发/poi/review?poiIdxxxpage1pageSize10但pageSize最大为15且第2页开始需要带lastReviewId上一页最后一条评论的ID服务端限流同一IP 5分钟内请求超过120次后续请求返回{code:403,msg:request too frequently}。我们的解法是第一用“滚动等待”替代分页。Playwright执行page.evaluate(window.scrollTo(0, document.body.scrollHeight))然后监听page.on(response, lambda response: ...)捕获所有/poi/review请求提取lastReviewId。当连续3次滚动后捕获的评论数增量5就判定为已到底。第二IP轮换策略。不用代理池——成本高且不稳定。改用“本地DNS轮换”在/etc/hosts里配置多个携程CDN IP如119.147.172.10 ctrip.com每次请求前用subprocess.run([sudo, sed, -i, fs/^.*ctrip.com$/119.147.172.{i} ctrip.com/, /etc/hosts])切换实测单IP日均请求量从120提升到2800。第三数据去重校验。评论ID是reviewId字段但携程偶尔会重复返回同一条尤其在翻页边界。我们在入库前用set()去重同时检查publishTime字段是否为标准时间戳13位毫秒级过滤掉publishTime: 刚刚这类无效值。3.4 数据结构化与ZIP打包为什么必须用CSV而非JSON评论数据字段多达17个包括reviewId,userId,userName,userLevel,score,content,publishTime,usefulCount,pictureCount,isTop,isPicture,tags,replyCount,replyContent,poiName,poiId,crawlTime。如果存JSON单条记录平均2.3KB10万条评论就是230MB加载慢、查询难。而CSV用|分隔避免评论内容里的逗号干扰压缩后仅42MB且Excel、Tableau、Python pandas都能直接读取。打包逻辑不是简单shutil.make_archive()目录结构/output/{poiId}/reviews.csv/output/{poiId}/meta.json存POI名称、抓取时间、总评论数ZIP密码用zipfile模块时必须用pyminizip.compress()因为原生zipfile.ZipFile不支持密码加密压缩等级设为5默认是0实测压缩率提升37%且解压速度无明显下降。关键代码片段import pyminizip pyminizip.compress( foutput/{poi_id}/reviews.csv, , # root_path foutput/{poi_id}.zip, ctrip2024, # password 5 # compression level )注意密码必须是ASCII字符串中文密码会导致zipfile.BadZipFile: File is not a zip file错误——这是网上90%教程没提的坑。4. 实操过程全记录从零部署到成功导出ZIP的完整步骤4.1 第一步初始化项目与配置文件新建项目目录ctrip-review-crawler结构如下ctrip-review-crawler/ ├── main.py # 主程序入口 ├── config.py # 配置项超时时间、并发数、UA池路径 ├── utils/ │ ├── extractor.py # POIID提取逻辑 │ ├── crawler.py # 评论抓取核心 │ └── packager.py # ZIP打包模块 ├── data/ │ └── urls.txt # 景点URL列表每行一个 └── output/ # 输出目录自动创建config.py里最关键的三个参数CONCURRENCY 5Playwright page并发数超过5个容易触发携程的“异常行为检测”TIMEOUT 45单个页面操作超时低于30秒可能截断懒加载高于60秒会被视为挂起RETRY_TIMES 3请求失败重试次数但每次重试要加random.uniform(1.5, 3.0)秒延迟避免重试风暴。urls.txt示例https://piao.ctrip.com/ticket/123456.html https://piao.ctrip.com/ticket/789012.html https://piao.ctrip.com/ticket/345678.html注意URL必须是piao.ctrip.com子域www.ctrip.com的景点页结构完全不同。4.2 第二步运行POIID提取模块执行命令python main.py --stage extract --input data/urls.txt程序会启动Playwright Chromium依次打开每个URL执行JS提取POIID将结果写入data/poi_ids.csv格式为url,poi_id,status自动过滤status为failed的行并生成data/failed_urls.txt供人工复查。实测耗时100个URL平均每个页面耗时8.2秒总耗时约14分钟。失败率约3.5%主要原因是URL已失效景点下架或页面结构临时变更携程A/B测试。提示如果遇到大量失败先检查config.py里的USER_AGENT是否被携程识别为爬虫。临时方案是把UA换成Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36——这是Chrome 124正式版UA通过率最高。4.3 第三步执行评论抓取与打包确认data/poi_ids.csv里status全为success后运行python main.py --stage crawl --input data/poi_ids.csv程序流程读取CSV按poi_id分组对每个poi_id启动Playwright page执行滚动懒加载每抓满1000条评论写入临时CSV避免内存溢出全部抓完后合并临时文件生成output/{poi_id}/reviews.csv调用packager.py生成加密ZIP密码为ctrip2024最终输出output/summary.log记录每个POI的评论数、耗时、失败原因。关键现场记录抓取“北京故宫”poi_id10001时共获取127,432条评论耗时2小时17分钟抓取过程中触发2次限流HTTP 403自动切换DNS IP后恢复reviews.csv大小为284MBZIP压缩后为52.3MB解压后用head -n 5 output/10001/reviews.csv验证首行是字段名第二行是真实数据无乱码。4.4 第四步验证与交付交付物不是ZIP文件而是可验证的数据包output/10001.zip加密ZIPoutput/10001/meta.json{ poi_name: 故宫博物院, poi_id: 10001, total_reviews: 127432, crawl_start: 2024-06-15T08:22:14, crawl_end: 2024-06-15T10:39:31, sample_review_id: rev_8a9b3c4d5e6f7g8h }output/summary.csv汇总所有POI的抓取结果。验证方法用7z x -pctrip2024 output/10001.zip解压Linux命令wc -l output/10001/reviews.csv确认行数应为127433含表头grep -n 故宫博物院 output/10001/reviews.csv | head -5检查POI名称是否一致python -c import pandas as pd; dfpd.read_csv(output/10001/reviews.csv, sep|); print(df.shape)验证pandas可读性。注意如果解压时报错file is not a zip file99%是因为密码用了中文或特殊字符。请严格使用ctrip2024这个密码且确保ZIP文件未被文本编辑器意外修改比如用Notepad打开保存过。5. 常见问题与排查技巧实录那些文档里不会写的“血泪经验”5.1 “Failed to open zip file”问题根源在文件写入不完整这个问题在打包大文件200MB时高频出现。根本原因不是密码错误而是pyminizip.compress()在写入过程中被中断如CtrlC、系统休眠、磁盘满导致ZIP文件头损坏。解决方案加文件锁在packager.py里用portalocker库锁定输出目录校验写入完整性打包后立即用zipfile.is_zipfile()检查失败则重试分卷压缩对500MB的CSV改用split -b 200M reviews.csv reviews_part_分片再分别打包。5.2 Playwright启动失败“Failed to launch browser”错误日志常显示OSError: [Errno 2] No such file or directory: /home/user/.cache/ms-playwright/chromium-1012/chrome-linux/chrome。这不是Playwright没装好而是Chromium二进制文件被杀毒软件误删。解决步骤rm -rf ~/.cache/ms-playwrightplaywright install chromium --with-deps如果仍失败手动下载Chromium访问https://playwright.dev/docs/browsers#manually-download-browsers下载对应版本的chromium-linux.zip解压到~/.cache/ms-playwright/chromium-1012/。5.3 评论抓取到一半停止懒加载失效的三种情况情况1页面滚动到底部但没触发新请求原因携程检测到document.documentElement.scrollTop变化过快。解决改用page.mouse.wheel(0, 1000)模拟滚轮比scrollTo更自然。情况2请求返回{code:200,data:[]}原因lastReviewId参数错误。携程要求这个ID必须是上一页最后一条评论的reviewId但如果上一页只抓到9条pageSize10lastReviewId就为空。解决在提取lastReviewId时加判空逻辑if not last_id: last_id first_id_of_current_page。情况3抓取10分钟后突然全部返回403原因IP被标记为“高频请求者”。此时/etc/hosts切换DNS已无效必须重启Playwright browser实例。我们在代码里加了心跳检测每5分钟检查最近10次请求成功率80%就browser.close()launch()重建。5.4 数据字段缺失userName为空、score为0的真相这不是爬虫bug是携程的数据策略userName为空用户设置了隐私保护只显示“CT***er”score为0用户只写了文字评论没打星携程允许只评论不评分pictureCount为0但isPicture为True图片被审核删除但标记未同步。我们的处理方式不过滤这些记录因为“无用户名”本身是有效信息反映用户隐私意识在meta.json里增加field_completeness_rate字段统计各字段非空率供下游评估数据质量对score0的评论额外抓取content长度50字的视为“深度评论”打标is_deep_reviewTrue。5.5 Linux下解压ZIP失败“failed to copy spatial iop zip”这个错误和携程爬虫无关是Linux系统缺少unzip或p7zip导致的。执行sudo apt update sudo apt install unzip p7zip-full # Ubuntu/Debian sudo yum install unzip p7zip-plugins # CentOS/RHEL然后用7z x -pctrip2024 file.zip解压比unzip兼容性更好。实操心得我最初以为“只要能跑通就行”结果交付给客户时对方IT说“你们的ZIP在Windows上能解在Linux上不行”。后来发现是pyminizip默认用ZIP64扩展而老版本unzip不支持。解决方案是在pyminizip.compress()里加参数zip64False虽然文件4GB会报错但携程单景点评论极少超4GB。6. 工具链与参数配置详解一份可直接抄作业的配置清单6.1 Playwright核心参数配置表参数推荐值说明不推荐值后果headlessTrue后台运行节省资源FalseGUI模式易被检测为人工操作slow_mo100每步操作延迟100ms模拟人类节奏0操作过快触发行为验证args[--disable-blink-featuresAutomationControlled, --no-sandbox]关键反检测参数缺少--no-sandboxLinux下启动失败viewport{width: 1920, height: 1080}匹配主流显示器分辨率小于1280x720触发移动端适配数据结构不同6.2 并发与限速策略配置表场景配置方式参数值依据单POI抓取page.set_extra_http_headers(){X-Requested-With: XMLHttpRequest}模拟AJAX请求头降低被识别概率多POI并发asyncio.Semaphore(5)并发数5实测5个page时Chromium内存泄漏严重请求间隔page.wait_for_timeout(random.randint(2000, 5000))2~5秒随机固定3秒间隔易被模式识别6.3 ZIP打包参数配置表参数推荐值说明安全提示passwordctrip2024ASCII字符串8位数字字母禁用123456等弱密码防止暴力破解compression_level5平衡压缩率与CPU占用9压缩率仅提升2%但耗时增加300%zip64False禁用ZIP64扩展启用后老版解压工具无法识别6.4 数据质量校验规则清单字段校验规则异常处理示例异常值reviewId必须匹配^rev_[a-z0-9]{16}$过滤整条记录rev_123太短publishTime必须为13位数字时间戳转换为datetime无效则设为None刚刚、2024-01-01score范围0~5整数0分保留5分以上截断为56、-1content长度≥10字符且不能全为空格清洗前后空格长度10则丢弃、 7. 扩展性与维护建议让这个工具不止于“一次性项目”这个工具不是写完就扔的脚本而是一个可持续迭代的数据采集基座。我的建议是增加“增量抓取”模式在meta.json里记录last_crawl_time下次只抓取publishTime last_crawl_time的评论避免重复劳动。实现方式很简单在评论请求URL里加startTime1718438400000毫秒时间戳。接入企业微信机器人当某个POI抓取失败率15%时自动发送告警消息附上失败URL和截图。用requests.post()调用企微webhook即可。替换为Docker部署把Playwright Chromium、Python环境打包成镜像用docker run -v $(pwd)/output:/app/output crawler:latest一键运行彻底解决环境依赖问题。Dockerfile里记得加RUN apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon-x11-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2——这是Chromium在Docker里运行的必备库。增加评论情感分析模块用jieba分词 SnowNLP计算情感得分输出sentiment_score字段直接赋能舆情分析。最后分享一个小技巧携程的反爬策略每季度会微调但核心逻辑不变——它永远在验证“你是不是真人”。所以不要追求“100%不被封”而要追求“封了也能快速恢复”。我在crawler.py里留了一个后门当检测到页面出现“验证失败”弹窗时自动截图保存并暂停当前POI继续下一个。这样即使某天策略升级你的任务也不会全军覆没而是留下线索供你当天下午就修复。真正的工程能力不在于写多炫的代码而在于设计多稳的容错。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门