
简介这份工具用于自动化比对唯品会与得物平台上的同款商品价格既适合日常购物比价用户也适合想学习网络爬虫与浏览器自动化的开发者。压缩包共38个文件体积约13.56MB包含主程序ComparePrice.exe、配置文件、使用说明txt以及17个dll运行库和14个xml说明文档结构清晰。其中汇集了WebDriver浏览器驱动、Newtonsoft.Json与System.Text.Json解析库、RestSharp请求封装、unvell.ReoGrid表格控件等组件可支撑商品信息的抓取、解析、对比和展示。已有4441人学习下载。利用该资源可直接运行程序体验跨平台比价也可结合代码与配置学习Selenium自动操作、JSON接口对接、HTTP通信及桌面表格呈现等综合技能对研究电商数据采集与C#桌面开发很有参考价值。1. 唯品会得物商品比价工具同一双鞋两边的到手价能差出半个钱包我是在准备买一双跑鞋时意识到这个问题的唯品会打完折不到四百得物上同样的货号要四百六但得物页面还标着“包鉴定”一时分不清哪个更值。手动切了两个 APP 比来比去又费时间又容易漏于是我想干脆做个“唯品会得物商品比价工具”让两个平台的商品自己站出来比价。这个工具的核心不是把两个价格摆在一起那么简单而是要先解决平台之间商品编码不统一、价格模型不同、到手价要算服务费和运费的问题。它适合经常网购比价的人也适合想快速判断商品行情、做代购选品的从业者。2. 比价工具的选型与数据链路先搞清楚两个平台“卖”的到底是不是同一个价格2.1 唯品会与得物的价格模型差异一口价、预售、求购价、闪电直发唯品会走的是品牌特卖模式价格基本是一口价页面显示多少钱结算时最多叠加优惠券。它的库存波动大尺码一缺就是“已抢完”但价格模型非常简单很适合程序直接读取。得物就不一样。得物的商品详情页至少能看到几种价格形态商家预售、商家现货、闪电直发甚至还有买家挂出的求购价。预售便宜但要等现货贵一些但发货快闪电直发又是另一个卖家渠道。求购价代表“有人愿意出这个钱买”不是我们能直接下单买入的价格。如果比价工具一律取页面上的第一个价格很容易拿“求购价”去对比唯品会的“可买价”得出一个错的结论。所以我一般会把两个平台的价格口径拉平。对于唯品会取“当前默认可卖尺码的最低现价”加上优惠券估算。对于得物取“尺码选择后的商家最低可买价预售和现货都算但分开标记”再加上平台服务费和预计运费。这样比出来的才是“同一件商品在两个平台买到的总成本”而不是裸价。下面这张表是我在搭建工具时整理的对比维度唯品会得物价格形态一口价品牌特卖预售 / 现货 / 闪电直发 / 求购价匹配关键商品货号有时带后缀商品货号较规整支付附加运费一般满包邮买家服务费 / 鉴定费 / 运费动态程度活动期变化快受买卖供需实时影响页面结构相对静态适合直接解析有渲染逻辑需要等加载这张表直接影响后面的代码怎么写尤其是得物那栏必须把“服务费”当成参数而不是假设为零。2.2 技术选型requests 还是 Playwright为什么我选后者常见的做法是先用 requests BeautifulSoup 去抓商品页。唯品会的 Web 端页面结构相对简单商品详情页在登录态下可以直接拿到价格和货号。但得物的情况要复杂一些商品数据大多是异步加载的部分接口有签名校验直接 requests 去请求经常会撞上风控返回一个空的 JSON。另一个选择是 Selenium能模拟浏览器但启动慢、需要额外维护浏览器驱动社区维护也在逐步转向更新方案。我实际更推荐 Playwright它同样能模拟真实浏览器而且自带等待机制能有效处理页面异步渲染的问题另外它对 JavaScript 渲染出来的价格字段能自然捕获省去逆向接口签名的功夫。这里做一个选型对比方便你判断哪种适合自己方案上手难度抗反爬能力资源占用适合场景requests BeautifulSoup低弱容易被风控极低单次抓取快速验证Selenium中中高需要兼容老代码Playwright中较好中动态渲染页面长期跑比价我最终选择 Playwright 还有一个实际原因比价工具需要处理两个平台编码逻辑尽量统一。用 Playwright 抓取页面两边都能走同一套“打开页面 → 等待元素 → 读出字段”的套路少写一套接口解析。代价是内存占用比 requests 高但在定时任务里控制好并发数量和页面关闭逻辑完全可以接受。一句话总结选型思路先把目标锁定在“能用浏览器看到的价格”而不是“能从接口读到的价格”。这样可以绕开大部分签名校验把精力放在比价规则上。3. 实现步骤用 Playwright 抓取商品页按货号匹配价格3.1 搭一个最小工程目录结构、依赖与日志我习惯把整个比价工具拆成几个独立模块搜索、解析、价格归一化、配置。这样如果某一天得物改版只需要改它自己的解析文件其他部分不动。先列一个最小目录price_compare/ ├── requirements.txt ├── config.json ├── compare.py ├── parsers/ │ ├── __init__.py │ ├── weipinhui.py │ └── dewu.py └── utils/ └── logger.py依赖文件 requirements.txt 里只有三样东西playwright1.44.0 pandas2.2.2 schedule1.2.1安装依赖后还需要执行一行命令让 Playwright 下载浏览器内核playwright install chromium这里我用了固定版本号避免更新后行为不一致。实际项目里你也可以去掉版本号但那样会在某一天升级后遇到新问题所以我个人倾向锁版本。3.2 搜出商品链接两个平台的搜索逻辑比价工具的第一步是输入一个模糊关键词比如“空军一号 男鞋”然后分别在两个平台搜索出商品列表拿到详情页 URL。我先写一个基于 Playwright 的搜索函数它接收搜索词返回商品链接和标题。注意这段代码只是一个基础模板你需要根据实际页面结构调整选择器。# compare.py 片段 from playwright.sync_api import sync_playwright def search(platform, keyword): search_urls { weipinhui: https://www.weipinhui.com/search?kw{}.format(keyword), dewu: https://www.dewu.com/search?q{}.format(keyword), } with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(search_urls[platform], timeout30000) # 等待商品卡片出现 page.wait_for_selector(.product-item a, timeout10000) links page.locator(.product-item a) results [] for i in range(min(5, links.count())): items links.nth(i) link items.get_attribute(href) title items.inner_text().strip() results.append({link: link, title: title}) browser.close() return results逻辑说明这个函数用 Playwright 的同步 API 启动一个无头浏览器打开搜索页面等待商品卡片选择器出现后再抽取链接和标题。参数headlessTrue表示不显示窗口适合跑在服务器上timeout控制页面加载的最长等待时间。这里的选择器.product-item a是示例真实页面里需要按浏览器 F12 去确认。我给商品卡片数量设了上限 5 个是为了避免一次搜太多触发风控实际运行中你可以把min(5, links.count())改成可配置参数。得物的搜索 URL 和选择器可能不同甚至搜索页本身就是动态路由。一个更稳妥的做法是直接打开首页用页面内的搜索框输入关键词再回车def search_dewu(page, keyword): page.goto(https://www.dewu.com/, timeout30000) page.locator(input[placeholder*搜索]).fill(keyword) page.keyboard.press(Enter) page.wait_for_selector(.product-card, timeout10000) links page.locator(.product-card a) return [links.nth(i).get_attribute(href) for i in range(min(5, links.count()))]这两种写法展示了一个差别前者依赖 URL 拼接如果平台有跳转参数就容易失效后者通过浏览器交互逻辑上更贴近用户但速度慢一些。我一般在搜索阶段用第二种因为搜索是低频操作不至于对性能产生压力。3.3 解析详情页并抽取货号、价格、运费拿到详情页 URL 后需要抽取的关键字段有品牌、商品标题、货号、当前可买价格、运费。货号是后续比价的锚点。下面这段代码负责从唯品会详情页解析字段# parsers/weipinhui.py def parse_weipinhui_detail(page): page.wait_for_selector(.product-name, timeout15000) name page.locator(.product-name).inner_text().strip() brand page.locator(.brand-name).inner_text().strip() price page.locator(.current-price).inner_text().strip() sku page.locator(.product-sku).inner_text().strip() return { platform: weipinhui, name: name, brand: brand, price: float(price.replace(¥, )), sku: sku, url: page.url, }关键参数说明wait_for_selector里的超时时间我设了 15 秒因为唯品会详情页有些模块加载很慢如果等不到就直接报错宁可加延时也不拿空数据。inner_text拿到的价格可能带着各种符号比如“¥”或文字“到手价”需要做清洗。得物页面复杂一些。它默认展示的价格是当前所选尺码对应的价格而尺码如果不选可能显示一个区间甚至显示“请选择尺码”。所以解析更有必要的做法是先尝试逐个点击尺码标签每次点击后读取价格记录下最低价和对应尺码。# parsers/dewu.py def parse_dewu_detail(page): page.wait_for_selector(.size-selector, timeout15000) size_buttons page.locator(.size-selector .size-item) min_price 99999 size_selected None for i in range(size_buttons.count()): size_buttons.nth(i).click() page.wait_for_selector(.price-now, timeout5000) price_text page.locator(.price-now).inner_text() price float(price_text.replace(¥, ).strip()) if price min_price: min_price price size_selected size_buttons.nth(i).inner_text() sku page.locator(.product-sku).inner_text().strip() if page.locator(.product-sku).count() else return { platform: dewu, name: page.locator(.product-name).inner_text().strip(), price: min_price, size: size_selected, sku: sku, url: page.url, }这段代码是实测中最容易翻车的地方。因为每次点击尺码按钮后得物会重新请求一次库存和价格如果网络慢price-now可能还停留在上一个尺码的值。我在这里加了一行wait_for_selector但更可靠的方案是记录点击前后价格的文本变化如果不变就再等 500 毫秒。后面避坑章节会专门讨论这个问题。3.4 价格归一化与比价算法解析完两个平台的商品数据后下一步就是用货号去匹配。货号在唯品会可能叫“款号”在得物可能叫“商品编号”。两边不一定完全一致所以匹配前需要做一次清洗去掉空格、横线、中文单位等。def normalize_sku(sku): return sku.replace( , ).replace(-, ).replace(款号, ).upper()然后定义一个价格数据结构和一个比价函数from dataclasses import dataclass dataclass class ProductPrice: platform: str sku: str name: str price: float shipping: float 0.0 extra_fee: float 0.0 property def total_price(self): return self.price self.shipping self.extra_fee def compare(weipinhui_item, dewu_item): sku1 normalize_sku(weipinhui_item[sku]) sku2 normalize_sku(dewu_item[sku]) if sku1 ! sku2: return None w ProductPrice(weipinhui, sku1, weipinhui_item[name], weipinhui_item[price]) d ProductPrice(dewu, sku2, dewu_item[name], dewu_item[price]) diff d.total_price - w.total_price cheaper weipinhui if diff 0 else dewu return { sku: sku1, weipinhui_total: w.total_price, dewu_total: d.total_price, diff: abs(diff), cheaper: cheaper, }这里的extra_fee是后面用来承载得物服务费和唯品会运费的关键字段。暂时设为 0只做裸价对比。等到第 4 章把配置加进来后这个字段才会被赋值。这段代码的核心是“先归一化货号再匹配”否则平台间的小差异会让匹配率极低。匹配成功后再算总价并用diff输出差多少。实际输出到表格或者控制台时可以用 pandasimport pandas as pd results [compare(w, d) for w, d in zip(weipinhui_list, dewu_list)] valid_results [r for r in results if r is not None] df pd.DataFrame(valid_results) df.to_csv(compare_result.csv, indexFalse, encodingutf-8-sig)参数说明encodingutf-8-sig很关键。如果默认用 utf-8生成的 CSV 用 Excel 打开时中文会乱码加上 BOM 才正常。这一点是很多第一次写比价脚本的人会踩到的。4. 把“到手价”算明白优惠券、鉴定费、运费与会员权益4.1 唯品会优惠券与满减规则用配置而不是硬编码只对比商品裸价很容易得出一个错误结论。比如唯品会显示一件衣服 199得物显示 229看上去唯品会便宜 30但唯品会可能不包邮得物却免运费。又比如唯品会有一张“满 300 减 40”的券只有凑单时才能用单商品比价时不该把券算进去。我的做法是把这些规则写进配置文件。每次运行比价前工具先读配置再决定是否应用优惠。配置文件 config.json 里维护满减规则{ weipinhui: { free_shipping_threshold: 199, shipping_fee: 10, coupons: [ { threshold: 300, discount: 40 } ] }, dewu: { buyer_service_rate: 0.05, service_fee_min: 5, shipping_fee: 12, appraisal_fee: 0 } }这里的参数我不写成代码常量而是做成可配置是因为平台的活动会变优惠券也时刻在变。比价工具的维护成本不在代码而在参数更新。然后写一个工具函数把唯品会的优惠券规则应用到商品价格上def apply_weipinhui_rule(price, shipping_fee, coupon): total price shipping_fee if total coupon[threshold]: total - coupon[discount] return total逻辑说明这个函数先计算是否达到满减门槛如果达到就直接减掉优惠面额。注意它把运费也计入了“满减门槛”因为平台实际结算时多数满减是看结算总金额不是只看商品原价。实际使用中我用一个包含“最低可凑单价”的字段来优化满减计算。举个例子券是满 300 减 40单件商品只有 199是否要算便宜 40这取决于你是否愿意凑单。我会在配置里加一个can_combine: true来标记可以凑单这样工具就会按“199 不够再加一件 101 的其他商品分摊下来单件减 26.7”计算。这个问题很细节但直接决定了比价结果是否真实。4.2 得物的服务费与鉴定费为什么比价时要额外加一笔得更物上的价格不是最终结算价。得物作为平台会向买家收取一定的服务费或鉴定费而且费率可能随时间变化。不同类目的费率还不一样鞋子、衣服、数码产品各有各的标准。因此抓完商品价后必须再按规则加一笔费用。我在配置里用了rate min_fee的方式对应实际收费“按比例计算但不足某个金额时按下限收取”的模式def apply_dewu_service_fee(price, rate, min_fee): fee price * rate return max(fee, min_fee)逻辑说明比如一台手机卖 5000费率 1% 就算出 50一条项链卖 100按 5% 算只有 5但实际平台可能最少收 8 块这时max(fee, min_fee)会返回 8。这个函数可以同时覆盖两种情况。然后总的到手价这样算dewu_total dewu_price apply_dewu_service_fee(dewu_price, cfg[dewu][buyer_service_rate], cfg[dewu][service_fee_min]) cfg[dewu][shipping_fee]这段代码体现了比价工具中“个人信息差”的问题得物页面展示的可能是裸发布价只有进入订单确认页才能看到服务费。我们做比价工具不可能每次都走到下单那一步所以必须用配置费率去估算。我建议你在第一次使用工具时用真实下单流程核对一次费率然后修正配置里的数值后面才能信任输出结果。另外得物还有会员运费券和平台积分这些通常和账号绑定。比价工具要支持多账号模式配置文件里可以加一个dewu_user_level字段简单点的做法是先按无会员计算再根据自己实际的会员权益手动调整运费参数。4.3 按商品类目调整比价规则一张表解决比价规则不是全局统一的。鞋类和数码类在得物上的服务费率可能不同唯品会对不同品类的包邮规则也有差异。所以配置文件里应该按类目组织参数而不是平铺在一个节点下。{ categories: { shoes: { weipinhui: {free_shipping_threshold: 199, shipping_fee: 10, coupons: []}, dewu: {buyer_service_rate: 0.05, service_fee_min: 8, shipping_fee: 12} }, digital: { weipinhui: {free_shipping_threshold: 99, shipping_fee: 8, coupons: []}, dewu: {buyer_service_rate: 0.03, service_fee_min: 20, shipping_fee: 0} } } }这个配置让我每次比价前可以先问“我要比的是哪个类目”。代码里就增加一个参数category shoes cfg full_config[categories][category]这样做的好处是以后平台调整费率我只需要改 JSON不用改代码。这也是比价工具能长期维护的基础。现在很多比价失灵的问题不是程序跑不通而是费率没跟上平台规则变化。5. 避坑指南从“价格一样”到“真正买到”之间隔了五个坑5.1 现象同一件商品两边货号匹配不上我遇到过几次明明是同款鞋唯品会保存的货号是“AQQ001-400”得物显示的是“AQQ001400”后四位还带了个“400”。直接做字符串相等判断永远匹配失败。原因唯品会和得物的货号命名规则不同唯品会经常带后缀表示颜色或者尺码区间得物更干净一些。另外还有空格和横线的问题。解决匹配前统一做归一化处理去掉所有非字母数字字符并统一转成大写。我在normalize_sku函数里就是这么做的。如果你的商品还有更复杂的规则可以维护一个货号别名表把两个平台的货号映射到同一个内部编码里。这是最笨但也最稳的办法。5.2 现象得物详情页价格字段一开始是空的抓回来的是上一件商品的价格我在调试时发现用 Playwright 打开得物商品页立即读取价格会拿到一个空字符串。等几秒再读价格又正常了。如果连续抓多个商品由于第二件商品页面还没完全渲染脚本就去读取结果读到了上一页缓存的文本。原因得物详情页的尺码和价格是通过异步请求动态渲染的请求完成前页面上根本没有价格节点但浏览器里仍保留上一页的 DOM 状态。解决每次导航后都要先page.wait_for_load_state(networkidle)再等待价格节点出现。不能只wait_for_selector就完事因为 selector 在上一页也可能存在。我在代码里加了一个“价格文本是否发生变化”的循环检查直到值稳定后才继续。5.3 现象搜索关键词正常但平台返回了验证码页面有一次我连续跑了一个多小时再去打开唯品会搜索页弹出了滑块验证。这不是代码逻辑错误而是请求频率过高触发了风控。原因 Playwright 虽然模拟浏览器但没有真实用户的行为特征比如点击速度、鼠标轨迹、页面停留时间。平台能通过请求频率识别脚本。解决给每次搜索和详情页访问之间增加随机延时比如 5 到 8 秒并且不要在一个浏览器实例里连续打开太多页面。我通常每访问 20 个页面重启一次浏览器。如果验证码依然频繁可以在配置里把headless设置为False让浏览器窗口可见这样风控概率会明显降低。5.4 现象最低价比出来是得物便宜实际上得物没有货工具报告得物某个尺码价格低我点进去准备买结果发现“库存不足”。这是因为得物的价格是动态的最低价对应的尺码可能只有一两件库存那一两件刚被别人买走页面价格就变了。原因商品库存变化比页面渲染快比价工具读取的是一个瞬时状态无法保证下单时仍然成立。解决在比对结果里同时输出“价格对应的尺码”和“时间戳”。我后面在结果 CSV 里加入了这两列看到“最低价对应尺码 41 码”和“价格采集于 10:23”至少能判断数据的新鲜度。如果你需要更准确的结果可以针对得物增加一个“库存检测”步骤选中该尺码后读取库存数字再决定是否把这条结果列入比价报告。5.5 现象唯品会打开商品详情页价格明明看到但 inner_text 抓到的是一堆乱码数字有几次我从页面抓到的价格是“4,889.00”这类带千分位逗号的字符串直接用float()转换会报错。还有的时候文本前面有“新人价”三个字也跟着一起被抓进来。原因页面上的价格是经过格式化后的展示文本不是结构化数据。不同模板的文本前缀也可能不同。解决在解析层统一做价格清洗正则只提取数字、小数点、负号。我写了一个clean_price函数会在后续代码里复用。不要相信页面文本是干净的所有字段都要经过清洗后再进入比价逻辑。这类看似小的问题实际上消耗了我最多的调试时间。6. 让比价工具真正省时间定时任务、通知和“历史低价”验证比价工具跑通一次之后我还把它升级成了每天定时运行的任务这样每天早上能收到一份今天值得买的清单。这里分享一个我自用的做法用 Python 的schedule库设置每天早晨 8 点运行一次再把结果写到 CSV 里最后通过企业微信机器人推送到手机上。定时任务的核心代码很短import schedule import time def job(): run_compare() send_notification() schedule.every().day.at(08:00).do(job) while True: schedule.run_pending() time.sleep(60)send_notification()内部可以是任意的推送 API原理就是 HTTP POST。比起自己搭手机客户端这种消息推送方式最省事。注意不要在任务里频繁发通知最好只推送当天与上次相比降价超过 5% 的商品否则容易变成噪音。我一直有记录历史价格的习惯所以会在每次运行结束时把compare_result.csv追加到一份历史文件里。这样积累一个月后能用 pandas 画出商品价格曲线。验证“当前价格是不是最近 30 天最低”比单次比价更有价值因为平台活动有周期性现在的低价可能只是临时降价。history pd.read_csv(history.csv) trend history[history[sku] current_sku] lowest_price trend[dewu_total].min()这段代码的作用简单直接从历史记录里捞出同一货号的得物总价算最低值。如果当前比价结果里的得物总价低于历史最低就标记为“今日好价”。这个逻辑同样适用于唯品会。我自己的使用习惯是每周跑一次全量的热门商品扫描每天只跑关注列表。全量扫描会非常消耗平台资源频率太高容易触发风控关注列表通常保持在 50 个以内每次运行大约 10 分钟频率可控。后来我又把“是否降价超过 5%”作为通知条件整个工具才算真正进入稳定状态而不是每天推送一堆没变化的数据。这套方案走到现在最大的收获不是省了几十块差价而是让我摸清了两个平台的价格模型。比价工具真正困难的地方不在爬虫而在“什么价格才是可以下单的价格”。如果你也照着这个方向做记得把服务费、运费和货号归一化优先处理好再开始优化抓取速度。希望帮到你。本文还有配套的精品资源点击获取