基于Playwright的大众点评数据爬取实战:应对动态加载与反爬策略
1. 项目概述与核心价值最近在做一个本地生活服务的数据分析项目需要大量、真实的用户评价数据作为基础。大众点评作为国内最大的本地生活信息及交易平台其海量的UGC评价内容无疑是金矿。但稍微尝试过就知道点评的页面反爬机制相当成熟传统的requestsBeautifulSoup组合面对其动态加载、复杂加密和频繁验证的页面几乎寸步难行。这正是我选择Playwright这个现代浏览器自动化工具的原因。它不像Selenium那样笨重又比PuppeteerNode.js原生对Python生态更友好最关键的是它能完美模拟真人操作浏览器从根本上绕过基于请求特征的反爬。这个项目就是带你用Playwright从零开始构建一个稳定、高效、能应对大众点评动态加载和反爬策略的爬虫。我们不止要“能爬”更要“爬得好”、“爬得稳”。我会详细拆解从环境搭建、页面分析、数据提取到反反爬策略的每一个环节并分享我在实战中踩过的坑和总结的技巧。无论你是想学习Playwright在复杂场景下的应用还是迫切需要获取点评数据这篇内容都能给你一套可直接复现的解决方案。2. 技术选型与环境搭建2.1 为什么是 Playwright面对大众点评这类重度依赖JavaScript渲染、交互复杂的现代Web应用技术选型直接决定了项目的成败。我对比过几种主流方案Requests BeautifulSoup / lxml最轻量但完全无法处理动态内容。点评的评论列表、店铺信息都是通过XHR/Fetch请求异步加载的直接请求HTML得到的是空壳。Selenium老牌自动化工具功能强大社区成熟。但其驱动管理麻烦执行速度相对较慢且默认的浏览器特征容易被识别为自动化脚本。PyppeteerPython版的Puppeteer直接控制Chromium性能好。但项目活跃度已下降且异步编程模型对新手有一定门槛。Playwright微软出品支持Chromium、Firefox、WebKit三大引擎。它生来就是为了测试和自动化现代Web应用提供了更简洁强大的API自动等待机制完善能生成更接近真人的浏览器指纹并且执行速度非常快。核心优势自动等待page.click()、page.fill()等操作会自动等待元素可交互无需手动写sleep或WebDriverWait代码更健壮。多浏览器支持一套代码可跑在多个浏览器引擎上方便测试兼容性也提供了更多反爬策略选择例如使用WebKit引擎可能指纹更独特。强大的选择器支持CSS、XPath、Text等多种定位方式还内置了如page.get_by_role()、page.get_by_text()等语义化选择器定位元素更直观。网络拦截与模拟可以轻松监听和修改网络请求这对于分析数据接口、屏蔽无用资源如图片、广告提升爬取速度至关重要。无头模式与真人模式无缝切换开发时用有头模式调试上线时用无头模式运行非常方便。注意任何爬虫行为都应遵守robots.txt协议并尊重网站服务压力。大众点评的robots.txt通常对爬虫有严格限制。本项目仅用于技术学习与研究请务必控制请求频率避免对目标服务器造成过大压力。商业用途需获得官方授权。2.2 环境搭建详细步骤我们创建一个干净的Python虚拟环境来管理依赖这是避免包冲突的最佳实践。步骤1创建并激活虚拟环境# 在项目目录下 python -m venv venv # 激活虚拟环境 # Windows (PowerShell) .\venv\Scripts\Activate.ps1 # Windows (CMD) .\venv\Scripts\activate.bat # macOS / Linux source venv/bin/activate激活后命令行提示符前会出现(venv)标识。步骤2安装 Playwright 核心库pip install playwright这会安装Playwright的Python客户端库。步骤3安装浏览器驱动Playwright需要对应的浏览器二进制文件才能工作。playwright install chromium这里我选择安装Chromium因为它最轻量且兼容性最好。你也可以安装firefox或webkit。这一步会下载几百MB的浏览器文件请确保网络通畅。步骤4安装数据处理辅助库可选但推荐pip install pandas openpyxlpandas用于后续的数据清洗、分析和存储为Excel/CSVopenpyxl是pandas写入Excel文件所需的引擎。至此核心环境就准备好了。你可以通过一个简单脚本测试安装是否成功import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: browser await p.chromium.launch(headlessFalse) # 有头模式方便看效果 page await browser.new_page() await page.goto(https://www.baidu.com) print(await page.title()) await browser.close() asyncio.run(main())如果成功打印出“百度一下你就知道”说明环境一切正常。3. 页面分析与爬虫策略设计3.1 目标页面结构与数据流分析大众点评的店铺页面例如https://www.dianping.com/shop/ 店铺ID是数据的主要载体。我们需要抓取的数据通常包括店铺基础信息店名、评分、人均消费、地址、电话、营业时间等。用户评价数据用户名、用户等级、评分、评价内容、评价时间、点赞数、图片/视频等。打开浏览器开发者工具F12切换到“网络”(Network)选项卡然后刷新店铺页面或滚动评论列表。你会发现关键数据并非直接存在于初始HTML中而是通过一系列XHR或Fetch请求异步加载的。这些请求的URL通常包含/shopinfo/、/review/、/ugc/等路径响应是JSON格式这正是我们需要的结构化数据。策略选择API直取 vs. 页面渲染API直取直接找到加载评价列表的JSON接口用requests模拟请求。优点是效率极高。但难点在于接口参数通常有加密如token,sign且可能绑定会话逆向分析成本高容易被封。页面渲染使用Playwright完全模拟浏览器打开页面滚动触发加载然后从渲染后的DOM树中提取数据。优点是完全绕过前端加密行为更像真人。缺点是速度相对慢资源消耗大。对于大众点评由于其接口加密复杂且变化频繁采用“页面渲染为主辅助网络拦截”的混合策略更为稳健。即用Playwright加载页面同时监听网络请求如果能直接捕获到清晰的JSON数据接口则优先使用如果接口复杂则从渲染后的页面中提取。3.2 核心反爬机制与应对策略大众点评部署了多层防御我们的爬虫需要“全副武装”User-Agent检测Playwright启动的浏览器默认有自动化特征。我们需要覆盖默认的UA并保持一致性。context await browser.new_context( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 )WebDriver检测通过navigator.webdriver属性。Playwright可以通过添加启动参数来隐藏它。browser await p.chromium.launch( headlessFalse, args[--disable-blink-featuresAutomationControlled] ) # 更彻底的方法在页面加载前执行JS删除或覆盖相关属性 await page.add_init_script( Object.defineProperty(navigator, webdriver, { get: () undefined }); )IP频率限制与验证码这是最严峻的挑战。单个IP高频请求必然触发验证码滑块、点选等或直接封禁。应对策略降低频率在关键操作如翻页、点击后随机等待一段时间例如await page.wait_for_timeout(random.uniform(2000, 5000))。使用代理IP池这是应对IP封锁的核心。可以为每个请求或每个会话BrowserContext配置不同的代理。context await browser.new_context( proxy{server: http://your-proxy-ip:port} )识别与处理验证码一旦出现验证码简单的爬虫很难自动通过。策略可以是暂停爬取更换IP或者接入打码平台商业项目考虑。在代码中需要检测验证码元素是否存在并做出相应处理如记录日志、暂停任务。行为指纹鼠标移动轨迹、点击速度、滚动模式等。Playwright提供的page.mouse.move()、page.mouse.click()等API可以模拟但要做到完全拟人化很难。一个折中方案是使用playwright的slow_mo参数让所有操作慢下来看起来更自然。browser await p.chromium.launch(headlessFalse, slow_mo100) # 每个操作延迟100毫秒4. 爬虫核心代码实现4.1 初始化与页面导航我们采用异步asyncio模式编写爬虫因为Playwright的API是异步的这样能获得更好的性能。import asyncio import random import pandas as pd from playwright.async_api import async_playwright class DazhongdianpingSpider: def __init__(self): self.shop_id H2f8w7K9pZ6 # 示例店铺ID需要替换 self.base_url fhttps://www.dianping.com/shop/{self.shop_id} self.all_reviews [] self.proxy_list [ # 示例代理列表需自行维护或购买服务 http://user:passip1:port, http://user:passip2:port, ] async def init_browser(self, use_proxyFalse): 初始化浏览器和上下文应用反反爬策略 playwright await async_playwright().start() launch_options { headless: True, # 生产环境用无头模式 args: [ --disable-blink-featuresAutomationControlled, --no-sandbox, --disable-setuid-sandbox, --disable-dev-shm-usage, ] } if use_proxy and self.proxy_list: proxy random.choice(self.proxy_list) launch_options[proxy] {server: proxy} print(f使用代理: {proxy}) browser await playwright.chromium.launch(**launch_options) # 创建上下文设置更真实的浏览器环境 context await browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, localezh-CN, timezone_idAsia/Shanghai, ) # 注入JS覆盖webdriver属性 await context.add_init_script( Object.defineProperty(navigator, webdriver, { get: () undefined }); window.chrome { runtime: {} }; ) page await context.new_page() return playwright, browser, context, page async def navigate_to_shop(self, page): 导航到目标店铺主页 print(f正在访问店铺: {self.base_url}) try: # 设置超时和等待策略 await page.goto(self.base_url, wait_untilnetworkidle, timeout60000) # 等待页面核心元素出现比如店铺名称 await page.wait_for_selector(.shop-name, timeout30000) print(店铺主页加载成功。) # 随机等待模拟真人阅读 await page.wait_for_timeout(random.uniform(1000, 3000)) except Exception as e: print(f导航失败: {e}) # 可以在这里截图排查问题 await page.screenshot(pathnavigation_error.png) raise4.2 解析店铺基础信息店铺信息通常直接在初始页面的DOM中相对容易提取。async def parse_shop_info(self, page): 解析店铺基础信息 shop_info {} try: # 使用更稳健的选择器结合等待 shop_info[name] await page.locator(.shop-name).first.text_content() shop_info[avg_price] await page.locator(span.avg-price).first.text_content() shop_info[address] await page.locator(div.address).first.text_content() # 评分可能由多个部分组成 shop_info[taste_score] await page.locator(span[data-cat口味] .score).first.text_content() shop_info[env_score] await page.locator(span[data-cat环境] .score).first.text_content() shop_info[service_score] await page.locator(span[data-cat服务] .score).first.text_content() print(f解析到店铺: {shop_info[name]}) except Exception as e: print(f解析店铺信息时出错: {e}) # 部分信息缺失不影响后续抓评 return shop_info4.3 抓取评价数据核心难点评价数据是动态加载的需要模拟滚动或点击“下一页”来触发加载。这里我们采用自动滚动到底部触发加载的方式。async def scroll_and_load_reviews(self, page, max_pages10): 滚动加载评价直到没有新内容或达到最大页数限制 print(开始滚动加载评价...) loaded_count 0 current_page 0 while current_page max_pages: current_page 1 print(f正在加载第 {current_page} 页评价...) # 滚动到页面底部触发加载 await page.evaluate(window.scrollTo(0, document.body.scrollHeight)) # 等待可能的新的评价卡片出现 try: # 等待新内容加载的指示器例如“加载中...”消失或新元素出现 # 这里使用一个通用的等待等待一段时间让网络请求完成 await page.wait_for_timeout(random.uniform(3000, 6000)) # 尝试查找并点击“下一页”按钮如果存在且需要 # next_button page.locator(a.NextPage) # if await next_button.count() 0 and await next_button.is_visible(): # await next_button.click() # await page.wait_for_timeout(random.uniform(2000, 4000)) # else: # print(未找到下一页按钮可能已到末尾。) # break except Exception as e: print(f滚动/加载时发生错误: {e}) break # 解析当前已加载的所有评价卡片 new_reviews await self.parse_reviews_on_page(page) if not new_reviews: print(本次滚动未解析到新评价可能已加载完毕。) # 可以再尝试滚动一次或者检查是否有“没有更多了”的提示 no_more await page.locator(div.no-more).count() if no_more 0: print(已到达评价列表底部。) break # 防止死循环如果连续两次没新内容也退出 if loaded_count len(self.all_reviews): print(连续滚动未发现新内容停止加载。) break loaded_count len(self.all_reviews) print(f已累计加载 {loaded_count} 条评价。) # 随机等待避免请求过于密集 await page.wait_for_timeout(random.uniform(2000, 5000)) async def parse_reviews_on_page(self, page): 解析当前页面上的所有评价卡片 reviews [] # 评价卡片的公共选择器需要根据实际页面结构调整 review_cards page.locator(div.review-words) count await review_cards.count() for i in range(count): try: card review_cards.nth(i) review {} # 使用内部定位器避免元素失效 review[username] await card.locator(a.name).first.text_content() review[user_level] await card.locator(span.user-level).first.get_attribute(title) review[star] await card.locator(span.sml-rank-stars).first.get_attribute(class) # 从class中解析星数 review[content] await card.locator(div.review-words-all).first.text_content() or await card.locator(div.review-words).first.text_content() review[post_time] await card.locator(span.time).first.text_content() review[like_count] await card.locator(span.vote-count).first.text_content() or 0 # 处理可能存在的图片 pic_elements card.locator(div.review-pictures img) review[pictures] [] pic_count await pic_elements.count() for j in range(pic_count): pic_url await pic_elements.nth(j).get_attribute(src) if pic_url and default not in pic_url: review[pictures].append(pic_url) reviews.append(review) self.all_reviews.append(review) # 添加到总列表 except Exception as e: print(f解析第{i1}条评价时出错: {e}) # 继续解析下一条不中断 continue print(f本轮解析了 {len(reviews)} 条新评价。) return reviews4.4 数据存储与主流程控制def save_to_excel(self, shop_info, reviews, filenamedianping_reviews.xlsx): 将数据保存到Excel文件 # 将店铺信息作为一行数据 shop_df pd.DataFrame([shop_info]) # 将评价列表转为DataFrame reviews_df pd.DataFrame(reviews) # 使用ExcelWriter写入同一个文件的不同Sheet with pd.ExcelWriter(filename, engineopenpyxl) as writer: shop_df.to_excel(writer, sheet_name店铺信息, indexFalse) reviews_df.to_excel(writer, sheet_name评价数据, indexFalse) print(f数据已保存至 {filename}) async def run(self): 主运行函数 playwright browser context page None try: # 初始化浏览器可随机决定是否使用代理 use_proxy random.choice([True, False]) and len(self.proxy_list) 0 playwright, browser, context, page await self.init_browser(use_proxyuse_proxy) # 导航到店铺 await self.navigate_to_shop(page) # 解析店铺信息 shop_info await self.parse_shop_info(page) # 抓取评价数据 await self.scroll_and_load_reviews(page, max_pages20) # 限制最多抓20“页” # 保存数据 if self.all_reviews: self.save_to_excel(shop_info, self.all_reviews) else: print(未抓取到任何评价数据。) except Exception as e: print(f爬虫运行过程中发生严重错误: {e}) finally: # 确保资源被关闭 if page: await page.close() if context: await context.close() if browser: await browser.close() if playwright: await playwright.stop() print(爬虫运行结束资源已释放。) # 运行爬虫 if __name__ __main__: spider DazhongdianpingSpider() asyncio.run(spider.run())5. 高级技巧与性能优化5.1 并发控制与速率限制单线程爬取速度慢但并发过高极易被封。需要平衡。使用Semaphore控制并发数限制同时打开的页面Page或浏览器上下文Context数量。import asyncio semaphore asyncio.Semaphore(3) # 最多3个并发任务 async def crawl_one_shop(shop_id): async with semaphore: # 爬取单个店铺的逻辑 await asyncio.sleep(random.uniform(1, 3)) # 每个任务完成后随机等待全局请求间隔在爬虫类中维护一个上次请求的时间戳确保每次请求之间有最小间隔。5.2 利用网络监听捕获API数据如果运气好能监听到清晰的评价列表API效率将大大提升。async def handle_response(response): 网络响应监听处理函数 if /ugc/review/ in response.url and response.status 200: try: data await response.json() # 从data中提取评价列表 reviews data.get(data, {}).get(reviews, []) if reviews: print(f从API捕获到 {len(reviews)} 条评价) # 处理并存储reviews except: pass # 在创建page后添加监听 page.on(response, handle_response)监听后你甚至可能不需要滚动直接触发几次翻页请求就能拿到大量数据。但需要小心接口参数的变化。5.3 错误重试与状态管理网络不稳定、元素定位失败、验证码弹出都是常态。一个健壮的爬虫必须有重试机制。import tenacity tenacity.retry( stoptenacity.stop_after_attempt(3), # 最多重试3次 waittenacity.wait_exponential(multiplier1, min2, max10), # 指数退避等待 retrytenacity.retry_if_exception_type((TimeoutError, ElementHandleError)) # 针对特定异常重试 ) async def safe_click(page, selector): 带重试的点击操作 await page.click(selector)对于整个店铺的爬取任务可以将其状态成功、失败、原因记录到文件或数据库便于中断后续爬、问题排查和统计成功率。6. 常见问题与实战避坑指南6.1 元素定位失败这是最常见的问题原因可能是页面结构变化、元素未加载完成或选择器写错了。对策1使用更稳健的选择器优先使用>selectors [.primary-btn, button:has-text(提交), //button[typesubmit]] for selector in selectors: if await page.locator(selector).count() 0: await page.locator(selector).click() break6.2 验证码识别与处理当看到验证码弹窗时爬虫基本无法继续。预防优于处理使用代理IP池、降低请求频率、模拟真人行为随机等待、鼠标移动是根本。检测与告警在关键步骤后检查页面是否出现了验证码相关的元素如div.geetest_panel,img.captcha-img。一旦检测到立即记录日志、保存当前页面截图并暂停或切换IP。if await page.locator(div.geetest_panel).count() 0: print(检测到验证码) await page.screenshot(pathfcaptcha_{int(time.time())}.png) # 触发告警或切换到下一个代理 raise CaptchaException(验证码拦截)后处理对于必须爬的数据可以手动处理截图或者研究更高级的破解方案成本高不推荐。6.3 数据解析不完整或错乱可能因为评价内容有“展开全文”或者图片懒加载。展开全文在获取评价内容前先检查并点击“展开”链接。expand_btn card.locator(a.review-words-unfold) if await expand_btn.count() 0: await expand_btn.click() await page.wait_for_timeout(500) # 等待展开动画图片懒加载图片的src属性初始可能是占位符真实URL在>real_src await img_element.get_attribute(data-src) or await img_element.get_attribute(src)6.4 内存泄漏与浏览器崩溃长时间运行多个Playwright实例可能导致内存不足。及时清理确保每个Page、Context、Browser对象在使用后都被正确close()。复用Context对于同一站点的多个任务可以考虑复用BrowserContext只创建新的Page比每次都启动新浏览器更轻量。监控资源在长时间运行的爬虫中定期打印内存使用情况并设置任务数量上限。6.5 应对网站改版网站前端结构变化是必然的。提高代码的容错性和可维护性很重要。将选择器集中管理不要将CSS或XPath选择器硬编码在业务逻辑里。可以放在一个配置字典或类属性中方便统一修改。class Selectors: SHOP_NAME .shop-name REVIEW_CARD div.review-words NEXT_PAGE a.NextPage定期运行测试用例写一个简单的脚本定期跑一下核心的页面解析功能一旦失败就发出警报提醒你需要更新选择器了。最后也是最关键的一点保持敬畏之心。爬虫是双刃剑务必遵守法律法规和网站的使用条款将请求频率控制在合理范围不要试图拖垮对方服务器。技术是用来解决问题和创造价值的而不是制造麻烦的。在实际项目中如果数据需求量大且持续最稳妥的方式永远是寻求官方API合作。这里的全套方案更多是为你攻克类似的技术难题提供一个扎实的Playwright实战范本。