基于Playwright的京东联盟高佣商品自动化采集方案

📅 2026/6/30 1:52:39
基于Playwright的京东联盟高佣商品自动化采集方案
1. 项目概述与核心价值最近在和一些做电商选品和内容营销的朋友聊天发现他们普遍面临一个痛点从京东联盟京粉这类CPS平台手动筛选高佣金商品效率极低且容易遗漏。每天花几个小时在后台翻页、对比佣金率、查看优惠券不仅枯燥还常常错过一些短期爆发的“神券”商品。作为一个常年和自动化工具打交道的开发者我第一反应就是这事儿完全可以交给代码来做。于是我花了些时间用 Playwright 搭建了一套自动化采集京东联盟高佣商品信息的脚本。Playwright 可能有些朋友还不太熟悉简单说它是一个由微软开源的现代化浏览器自动化测试库。相比老牌的 Selenium它的优势在于速度快、API 设计更人性化并且原生支持无头模式Headless下的高性能运行对于需要模拟真实用户点击、滚动、填表等操作的网页数据采集场景简直是“神器”。而京东联盟的页面恰恰是一个动态加载、交互复杂的典型SPA单页应用传统的简单 HTTP 请求很难搞定Playwright 这类能驱动真实浏览器的工具就成了不二之选。这个项目的核心目标很明确自动化、定时、精准地抓取京东联盟中符合特定条件如高佣金比例、有优惠券、特定品类的商品信息并将结果结构化保存如 CSV、JSON 或直接入库为后续的选品分析、内容创作或社群分发提供数据基础。它适合有一定 Python 基础并且对电商数据有需求的运营、博主或开发者。即使你之前没接触过 Playwright跟着这篇实操记录走一遍也能快速上手搭建起属于自己的“商品雷达”。2. 技术选型与环境搭建思路为什么是 Playwright 而不是别的这里需要拆解一下京东联盟页面的特点和技术难点。2.1 目标页面分析京东联盟的“反爬”与动态加载京东联盟后台的商品列表页例如“联盟精选”、“高佣商品”等数据并非直接写在 HTML 里。当你滚动页面时商品数据是通过 JavaScript 异步请求加载的。更“麻烦”的是页面元素使用了复杂的 CSS 类名且可能带有随机哈希值单纯用 XPath 或 CSS Selector 定位可能会因为页面微调而失效。此外京东对频繁的、非人行为的访问会有一定的风控机制虽然不如搜索页严格但直接用脚本疯狂请求接口也容易被限制。因此我们的方案必须能执行 JavaScript模拟滚动触发数据加载。等待元素出现智能等待动态内容加载完成避免因网络或脚本执行速度导致抓取失败。处理用户交互可能需要登录、点击筛选按钮如“佣金比率从高到低”。模拟人类行为加入随机延迟、模拟鼠标移动降低被识别为机器人的风险。2.2 Playwright 的优势所在多浏览器支持Chromium, Firefox, WebKit 一网打尽我们通常用 Chromium最稳定。自动等待Playwright 的操作如click,fill内置了智能等待会等到元素可操作时才执行省去了自己写time.sleep和轮询的麻烦。强大的选择器支持文本选择器text、CSS、XPath还能根据元素属性如[data-spm]定位应对动态类名更从容。网络请求拦截与模拟可以直接监听页面发出的 XHR/Fetch 请求有时直接解析接口返回的 JSON 数据比解析 DOM 更高效。不过对于京东联盟我们这次主要走 DOM 解析的路线更通用。无头模式与抗检测Playwright 的无头模式已经很难被普通脚本检测还可以通过add_init_script注入脚本覆盖navigator.webdriver等属性进一步伪装。2.3 环境搭建实操注意以下操作基于 Python 环境。请确保已安装 Python (建议 3.8)。首先安装 Playwrightpip install playwright安装完成后需要安装浏览器驱动。Playwright 管理了一个自己的浏览器版本以保证 API 兼容性。playwright install chromium这条命令会下载 Chromium 浏览器速度取决于你的网络。如果下载慢可以考虑设置环境变量PLAYWRIGHT_DOWNLOAD_HOST为国内镜像源但通常直接安装也可行。为了编写和调试脚本我强烈建议使用支持 Playwright 智能提示的编辑器如 VS Code并安装 Python 和 Playwright 的官方插件体验会好很多。一个常见的坑是公司网络可能有严格代理导致playwright install失败。如果遇到可以尝试在终端设置临时的 HTTP/HTTPS 代理环境变量后再执行安装命令。3. 核心流程设计与关键步骤拆解整个自动化采集流程可以抽象为以下几个核心环节我将逐一拆解其中的技术细节和注意事项。3.1 流程总览启动与配置浏览器以无头模式启动设置视窗大小、加入随机 User-Agent 等。处理登录状态这是最大的难点之一。京东联盟需要登录。方案有两种一是用 Playwright 模拟输入账号密码登录可能遇到验证码二是复用已有的浏览器 Cookies更稳定。我们采用第二种。导航至目标页面打开商品列表页例如“高佣商品”频道。模拟页面交互与滚动滚动页面以触发加载更多商品直到满足条件如滚到底部或抓够指定数量。提取商品信息在每次滚动加载后从当前 DOM 中解析出商品卡片提取标题、价格、佣金、佣金比例、优惠券、商品链接等信息。数据去重与存储将提取的数据进行去重防止滚动时重复抓取并保存到文件或数据库。资源清理关闭浏览器释放资源。3.2 登录状态持久化Cookie 复用直接模拟登录要处理验证码成功率不稳定。更可靠的方法是先手动登录一次然后保存登录后的 Cookies后续脚本直接加载 Cookies。手动获取 Cookies你可以先写一个简单的脚本用playwright启动一个非无头浏览器手动登录京东联盟然后脚本将 Cookies 保存为 JSON 文件。from playwright.sync_api import sync_playwright import json with sync_playwright() as p: browser p.chromium.launch(headlessFalse) # 显示浏览器方便手动操作 context browser.new_context() page context.new_page() page.goto(https://union.jd.com/) # 京东联盟首页 input(请手动登录登录完成后按回车键继续...) # 保存 Cookies cookies context.cookies() with open(jd_cookies.json, w) as f: json.dump(cookies, f) print(Cookies 已保存。) browser.close()运行此脚本浏览器打开后你手动完成扫码或账号密码登录。登录成功后在终端按回车Cookies 就会被保存到jd_cookies.json文件。加载 Cookies 自动登录在正式的采集脚本中启动浏览器上下文时加载这个文件。from playwright.sync_api import sync_playwright import json with sync_playwright() as p: browser p.chromium.launch(headlessTrue) # 正式运行用无头模式 context browser.new_context() # 加载之前保存的 Cookies with open(jd_cookies.json, r) as f: cookies json.load(f) context.add_cookies(cookies) page context.new_page() # 此时访问页面应该已是登录状态 page.goto(https://union.jd.com/) # 可以通过检查页面是否包含“我的联盟”等登录后元素来验证 if page.locator(text我的联盟).is_visible(): print(登录状态验证成功)重要提示Cookies 有有效期。如果脚本某天突然失效提示未登录很可能就是 Cookies 过期了需要重新运行一次手动登录脚本获取新的 Cookies。3.3 页面导航与滚动加载策略登录成功后导航到高佣商品页面。京东联盟的页面地址可能会变需要你实际去联盟后台找到对应页面的链接。# 示例链接实际请以京东联盟后台为准 high_commission_url https://union.jd.com/goods/highCommission page.goto(high_commission_url) page.wait_for_load_state(networkidle) # 等待网络基本空闲接下来是核心滚动加载。商品列表是滚动分页的。import time def scroll_page_to_bottom(page, max_scrolls50, scroll_delay1.5): 模拟滚动到页面底部 :param page: playwright page 对象 :param max_scrolls: 最大滚动次数防止无限循环 :param scroll_delay: 每次滚动后的延迟模拟人类阅读/网络加载时间 last_height page.evaluate(document.body.scrollHeight) scroll_attempts 0 while scroll_attempts max_scrolls: # 滚动到底部 page.evaluate(window.scrollTo(0, document.body.scrollHeight)) # 等待新内容加载 page.wait_for_timeout(int(scroll_delay * 1000)) # 等待固定时间也可用 wait_for_selector 等待特定元素出现 new_height page.evaluate(document.body.scrollHeight) if new_height last_height: # 高度未变可能已到底部或加载完成 # 可以再检查一下“没有更多了”之类的元素 break last_height new_height scroll_attempts 1 # 加入随机延迟更拟人 time.sleep(scroll_delay random.uniform(-0.5, 0.5))这里有几个关键点wait_for_timeoutvswait_for_selectorwait_for_timeout是固定等待简单但可能低效等久了浪费时间等短了数据没加载完。更优的做法是在滚动后等待一个代表“新商品卡片已加载”的特定元素出现例如page.wait_for_selector(.goods-item:last-child, stateattached, timeout5000)。这需要你分析页面结构。随机延迟在循环中加入random.uniform(-0.5, 0.5)这样的随机扰动避免过于规律的请求间隔。终止条件除了判断滚动高度是否变化还应结合业务逻辑比如“当商品卡片数量达到1000时停止”或者检测“已加载全部”的提示元素。3.4 商品信息解析与数据提取滚动加载过程中或完成后我们需要从页面中提取商品信息。首先要用浏览器开发者工具F12仔细分析商品卡片的 HTML 结构。假设我们分析出每个商品卡片有一个类名为.goods-item的容器实际类名可能不同可能是动态的。def extract_goods_info(page): 从当前页面提取所有商品信息 goods_items page.locator(.goods-item).all() # 获取所有商品卡片元素 goods_list [] for item in goods_items: try: # 提取标题注意可能有多行文本或促销标签 title_elem item.locator(.goods-title).first title title_elem.inner_text().strip() if title_elem.count() 0 else N/A # 提取价格 - 通常有原价和券后价 price_elem item.locator(.price).first price price_elem.inner_text().strip() if price_elem.count() 0 else N/A # 提取佣金和佣金比例 - 这是核心 # 佣金可能是一个单独元素也可能是“佣金比例”文本的一部分 commission_elem item.locator(.commission).first commission_text commission_elem.inner_text().strip() if commission_elem.count() 0 else # 解析 commission_text例如“佣金 ¥12.34 (20%)” import re commission_pattern r佣金\s*¥?(\d\.?\d*).*?(\d\.?\d*)% match re.search(commission_pattern, commission_text) commission_amount match.group(1) if match else 0 commission_rate match.group(2) if match else 0 # 提取商品链接联盟链接 link_elem item.locator(a.goods-link).first goods_url link_elem.get_attribute(href) if link_elem.count() 0 else # 链接可能是相对路径需要补全 if goods_url and not goods_url.startswith(http): goods_url https: goods_url if goods_url.startswith(//) else https://union.jd.com goods_url # 提取优惠券信息 coupon_elem item.locator(.coupon-info).first coupon coupon_elem.inner_text().strip() if coupon_elem.count() 0 else 无 goods_list.append({ title: title, price: price, commission_amount: commission_amount, commission_rate: f{commission_rate}%, coupon: coupon, url: goods_url, snapshot_time: time.strftime(%Y-%m-%d %H:%M:%S) }) except Exception as e: print(f解析商品条目时出错: {e}) continue # 跳过当前出错商品继续下一个 return goods_list实操心得页面结构是最大的变数。京东联盟的前端可能会改版导致选择器失效。因此选择器的健壮性至关重要。优先使用>import json import time import random import csv from playwright.sync_api import sync_playwright def main(): with sync_playwright() as p: # 1. 启动浏览器加载 Cookies browser p.chromium.launch(headlessTrue, args[--disable-blink-featuresAutomationControlled]) context browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... # 设置一个常见的UA ) try: with open(jd_cookies.json, r) as f: cookies json.load(f) context.add_cookies(cookies) except FileNotFoundError: print(未找到 cookies 文件请先运行登录脚本。) browser.close() return page context.new_page() page.add_init_script( Object.defineProperty(navigator, webdriver, { get: () undefined }); ) # 注入脚本隐藏 webdriver 属性 # 2. 访问高佣页面 page.goto(https://union.jd.com/goods/highCommission, wait_untilnetworkidle) time.sleep(3) # 初始页面加载等待 # 3. 验证登录状态 if not page.locator(text我的联盟).is_visible(timeout10000): print(登录状态失效请更新 Cookies。) browser.close() return all_goods [] seen_ids set() # 用于去重假设我们可以从URL或标题生成一个唯一ID # 4. 滚动加载并采集 max_scrolls 30 for i in range(max_scrolls): print(f第 {i1} 次滚动...) # 滚动前提取一次当前屏商品 current_goods extract_goods_info(page) for goods in current_goods: # 生成一个简单ID用于去重例如商品链接的某个参数 goods_id goods[url].split(/)[-1].split(.)[0] if goods[url] else goods[title] if goods_id not in seen_ids: seen_ids.add(goods_id) all_goods.append(goods) # 执行滚动 page.evaluate(window.scrollTo(0, document.body.scrollHeight)) # 等待新内容加载 - 更优的方式是等待特定元素 try: # 假设新加载的商品会有某个特定的加载类加载完成后类名消失 page.wait_for_selector(.loading-more, statehidden, timeout10000) except: # 如果等待失败使用固定延迟 page.wait_for_timeout(2000 random.randint(0, 1000)) # 判断是否到底部 is_at_bottom page.evaluate( () { return (window.innerHeight window.scrollY) document.body.scrollHeight - 100; } ) if is_at_bottom: print(已滚动到页面底部。) break print(f共采集到 {len(all_goods)} 个去重后的商品。) # 5. 数据保存 (以CSV为例) if all_goods: keys all_goods[0].keys() with open(jd_high_commission_goods.csv, w, newline, encodingutf-8-sig) as f: dict_writer csv.DictWriter(f, fieldnameskeys) dict_writer.writeheader() dict_writer.writerows(all_goods) print(f数据已保存至 jd_high_commission_goods.csv) # 6. 关闭浏览器 browser.close() if __name__ __main__: main()4.2 性能与稳定性优化点并发与异步上述是同步 (sync_api) 版本简单直观。对于大规模采集可以考虑使用 Playwright 的异步 API (async_api)甚至启动多个浏览器上下文或页面并行处理不同分类但需注意账号风控。请求拦截与缓存通过page.route可以拦截网络请求例如屏蔽图片、CSS 等不必要资源大幅提升加载速度。async def route_handler(route): if route.request.resource_type in [image, stylesheet, font]: await route.abort() else: await route.continue_() await page.route(**/*, route_handler)错误重试与断点续传网络不稳定或页面偶尔加载失败是常态。需要在关键步骤如page.goto,wait_for_selector添加try-except和重试逻辑。对于长时间采集可以考虑将已采集的商品ID定期保存到文件脚本重启时读取实现断点续传。内存管理长时间运行且滚动加载大量内容的页面可能导致内存增长。可以定期如每滚动10次尝试通过page.evaluate(window.scrollTo(0, 0);)回到顶部并移除一些早期的 DOM 元素如果页面结构允许或者直接关闭当前页面 (page.close()) 然后打开一个新页面继续但这会丢失滚动位置需要设计好状态管理。5. 常见问题排查与实战技巧在实际运行中你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的排查清单。5.1 登录状态失效或无法访问现象脚本打开页面后跳转到登录页或提示“请登录”。排查检查 Cookies 文件首先确认jd_cookies.json文件存在且内容有效。可以打印一下加载的 Cookies看是否有关键的pt_key,pt_pin京东的典型 Cookie 名。检查 Cookies 过期这是最常见原因。手动用浏览器访问京东联盟看是否需要重新登录。如果需要就重新运行一次手动登录脚本。检查域名匹配context.add_cookies(cookies)时Cookies 中的domain字段必须包含你将要访问的域名如.jd.com,union.jd.com。确保你保存 Cookies 的页面和采集页面域名一致。使用 Persistent ContextPlaywright 支持创建持久化上下文浏览器数据包括 Cookies、LocalStorage会保存在磁盘上更接近真实用户会话。context browser.new_context(user_data_dir./user_data) # 首次需要手动登录之后就会自动保持状态5.2 元素定位失败抓不到数据现象page.locator(.goods-item)返回空列表或者wait_for_selector超时。排查确认页面加载完成在定位元素前确保页面已经加载到你期望的状态。多用page.wait_for_load_state(networkidle)或page.wait_for_selector(某个关键元素)。检查选择器是否正确页面可能已经改版。使用 Playwright 的录制工具可以快速生成可靠的选择器。在终端运行playwright codegen https://union.jd.com然后在打开的浏览器中操作它会自动生成代码。处理 iframe如果目标元素在 iframe 内你需要先定位到 iframe然后切换到它的上下文。frame page.frame_locator(iframe[namexxx]).first element_inside_frame frame.locator(.my-element)增加超时时间网络慢时默认的 30 秒超时可能不够可以设置timeout参数例如locator.wait_for(timeout60000)。使用更宽松的定位器如果 CSS 类名是动态的尝试用 XPath 结合文本内容定位或者用:has-text()伪类。5.3 被识别为自动化脚本访问受限现象页面返回验证码、空白页或访问频繁提示。应对策略启用无头模式抗检测launch参数中已经设置了args[--disable-blink-featuresAutomationControlled]并注入了隐藏webdriver的脚本这是基础。模拟真人行为随机延迟在操作间加入随机等待时间time.sleep(random.uniform(1, 3))。随机移动鼠标使用page.mouse.move(x, y)在页面上随机移动。随机滚动不一定每次都滚到底部可以随机滚动一小段距离。使用代理 IP如果采集频率非常高考虑使用住宅代理 IP 池轮换 IP。Playwright 启动浏览器时可以指定代理服务器。browser p.chromium.launch(proxy{ server: http://your-proxy-server:port, username: user, password: pass })降低采集频率这是最有效的方法。不要试图在几分钟内抓取成千上万个商品。将任务分散到一天的不同时段模拟正常用户的浏览节奏。5.4 数据提取不准确或格式混乱现象佣金金额和比例提取错误标题包含多余换行和空格。处理技巧精细化文本处理使用.inner_text().strip()后再用正则表达式精确提取数字和百分比。对于复杂的文本块可以先split(\n)分行再逐行分析。多方案备选一个信息可能有多个展示位置或元素。编写提取逻辑时可以设计一个优先级例如先尝试用 A 选择器提取如果失败或为空再尝试 B 选择器。数据清洗管道提取后的数据不要直接存储先经过一个清洗函数处理空值、统一格式如价格统一为浮点数佣金比例统一为小数。保存原始快照对于重要的采集任务可以在提取失败时将当前页面的 HTML 片段或截图保存下来供后续离线分析和调试。page.locator(...).screenshot(pathdebug.png)非常有用。最后自动化采集是一个“道高一尺魔高一丈”的过程。京东联盟的页面结构、风控策略都可能变化。最宝贵的经验是将你的脚本模块化。把登录、滚动、解析、存储等环节写成独立函数并做好日志记录。这样当某个环节出错时你能快速定位和修复。定期比如每周手动运行一下脚本检查其是否依然有效比等到真正需要数据时才发现失效要好得多。这套基于 Playwright 的方案其核心思路——模拟浏览器、处理动态内容、管理状态——同样适用于其他复杂的、需要登录的电商或内容平台希望它能为你打开自动化数据获取的一扇门。