k-skill 之 danawa-price-search:基于 다나와 公开比价表面的只读价格对比 Skill 实战解析
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
本文以 danawa-price-search/instruction.md 为主体,完整讲解 k-skill 仓库中 다나와(Danawa)价格对比 Skill 的能力边界、三个公开 HTTP 表面、三条 CLI 命令及其 JSON 输出契约,并结合 danawa-price-search/scripts/danawa_search.py 源码与 scripts/test_danawa_price_search.py 测试,深入解析「결제조건(支付条件)徽章白名单」与total_price单标准排序的实现原理,帮助读者掌握一个可复制、可验证的登录无关(login-free)商品比价 Agent 技能。
1. Skill 定位与能力边界
该 Skill 属于 k-skill(한국인을 위한 스킬 모음집 — "把 Agent 变成韩国人"的技能集合)中retail类别、ko-KR语区的技能,元信息见 danawa-price-search/skill.json 与 danawa-price-search/SKILL.md(category: retail、locale: ko-KR、phase: v1,profile 为action:commerce)。
它只做一件事:以只读方式调用 다나와의 로그인 없는 공개 검색/가격비교 표면(다나와 的无登录公开搜索/价格对比表面),实现韩国各商城价格的比较。具体能力包括:
- 通过商品名/检索词找到 다나와 的商品候选与
pcode(商品编号); - 查询选定商品在各商城(쇼핑몰별)的 offer 列表;
- 不止商品价,还把含运费的实付价(배송비 포함 실구매가)、是否免费配送、卡片折扣价、无息分期(무이자 할부)文案一并整理;
- 明确不做:购买、登录、购物车、收藏(찜)、下单等任何写操作。
适用与不适用场景(When to use / When not to use)
适用的典型用户请求(文档原文列举):
- "다나와에서 에어팟 최저가 찾아줘"(在 다나와 上找 AirPod 最低价)
- "다나와 가격비교로 쇼핑몰별 가격 비교해줘"(用 다나와 价格对比做商城间价格比较)
- "무료배송인지, 카드 할인까지 보면 어디가 제일 싸?"(看免费配送、卡片折扣后哪里最便宜?)
- "무이자 할부 붙은 최저가도 같이 봐줘"(把带无息分期的最低价也一起看看)
明确不适用的场景:
- 需要实际购买/下单/支付/登录的场景;
- 需要确认会员专属优惠券、个性化积分、App 专属福利的场景;
- 需要大批量监控或高频爬取的场景;
- 出现 CAPTCHA、访问封锁,需要绕过 fingerprint 的场景。
必需输入
唯一必需输入是商品名或检索词。当检索词过于宽泛时,Agent 应主动追问品牌、型号、容量、颜色、自购机/运营商合约机等限定条件。文档给出的推荐提问模板:
찾을 다나와 상품명이나 모델명을 알려주세요. 예: 갤럭시 S25 울트라 256GB 자급제, 에어팟 프로 2세대 USB-C (请告诉我您要查找的 다나와 商品名或型号。例:Galaxy S25 Ultra 256GB 自购机、AirPod Pro 2 代 USB-C)
2. 三个公开表面(Public surfaces)
当前实现只使用无认证(인증 없음)的公开表面,共三个端点:
| 用途 | 端点 |
|---|---|
| 搜索页 | https://search.danawa.com/dsearch.php?query=... |
| 商品详情页 | https://prod.danawa.com/info/?pcode=... |
| 价格对比 AJAX | https://prod.danawa.com/info/ajax/getAllPriceCompareMallList.ajax.php |
其中 AJAX 端点返回 HTML 片段(fragment),helper 从其中解析.diff_item、商城 logo 的alt属性、em.prc_c/em.prc_t价格元素、配送文案、支付条件徽章(.ico.cash/.ico.point/.ico.coupon/.ico.discount/.ico.card/.ico.membership等)、卡片折扣行、无息分期图层,以及 다나와 桥接链接(bridge link)。
源码层的请求细节
从 danawa_search.py 的fetch()实现(L29-L43)可以看到几个关键实现事实:
- 固定桌面 Chrome 的
User-Agent与Accept-Language: ko-KR请求头; - POST 请求(即 AJAX 价格列表调用)会附加
Content-Type: application/x-www-form-urlencoded、X-Requested-With: XMLHttpRequest,并带上详情页Referer(见offers()中fetch(..., method="POST", data=data, referer=meta["source_url"]),L186)——这与浏览器真实 AJAX 行为一致,是该端点可匿名访问的关键前提; - 统一 25 秒超时,解码失败用
replace容错。
search()(L73-L108)解析搜索页的li.prod_item列表项,从#min_price_{pid}输入框的value取值作为最低标价(取不到时回退.price_sect strong/.prod_pricelist strong),并从li的id(productItem{pcode})中剥离出pcode。product_meta()(L130-L152)则用正则js_value()从详情页 HTML 中抽取内联 JS 变量(nCategoryCode1~4、powerLinkKeyword、nMinPrice、sNaPm、sProductName、makerCode/makerName等),再作为后续 AJAX 调用的表单字段——从源码结构看,AJAX 端点要求携带这些从详情页抓取的上下文字段才能正确返回商城列表。
3. 三条 CLI 命令
命令在技能目录下通过 k-skill CLI 执行(npx需要 Node.js 18+;SKILL.md也说明了npx -y @nomadamas/k-skill@0 instruct danawa-price-search获取完整说明、files danawa-price-search列出随附 helper 的用法):
npx -y @nomadamas/k-skill@0 exec danawa-price-search scripts/danawa_search.py -- search "에어팟 프로 2세대" --limit 8 npx -y @nomadamas/k-skill@0 exec danawa-price-search scripts/danawa_search.py -- offers 28208783 --limit 10 npx -y @nomadamas/k-skill@0 exec danawa-price-search scripts/danawa_search.py -- compare "에어팟 프로 2세대" --limit 5 --offers 5helper只输出 JSON。查看结果后,再向用户以韩文表格 + 简短结论的形式总结。
结合源码main()的参数定义(L324-L350),各子命令的完整参数如下:
| 子命令 | 位置参数 | 选项 | 默认值(源码) |
|---|---|---|---|
search | query检索词 | --limit候选数,正整数(>=1) | 10 |
offers | pcode商品编号 | --limit返回 offer 数,正整数 | 20 |
--include-shipping布尔开关(对应 AJAX 表单bPostPriceYN=Y/N) | 关闭 | ||
compare | query检索词 | --limit搜索候选数 /--offers每候选的 offer 数 | 5 / 5 |
错误处理:任何异常会以{"error": "..."}形式输出到 stderr 并返回退出码 2(L348-L350),调用方可据此判定失败。
4. 输出契约(Output shape)
4.1search输出
{ "query": "...", "source_url": "...", "count": 0, "items": [] }items[]的主要字段(源码 L94-L105 构造):
pcode— 商品编号,后续offers的输入;title— 商品名;price/price_text— 最低标价(整数 / 千分位韩元文案);mall_text— 商城文案(.prod_pricelist .memory_sect或.meta_item);url— 商品详情页链接(abs_url()会把//前缀补全为 https、把站内绝对路径挂到https://prod.danawa.com下);image_url— 缩略图(优先data-original懒加载属性);spec— 前 10 个.spec_list a/span的规格文本拼接,截断至 300 字符。
4.2offers输出
{ "pcode": "...", "title": "...", "source_url": "...", "count": 0, "normal_count": 0, "conditional_count": 0, "offers": [], "meta": { "sort": "total_price" } }核心排序约定:offers[]按含运费实付价total_price升序单一标准排序。count/normal_count/conditional_count统计的是应用limit之后实际返回的offers[]窗口。带支付条件(现金/优惠券/积分/折扣/特定卡片/会员限定)的行不参与分组或过滤,直接参加同一排序——最便宜就排第一。支付条件通过行内字段暴露,由调用方按用户支付手段自行判断。
offers[]各字段(与源码 L243-L271 一一对应):
mall— 商城名(取自.d_mall img的alt);price/price_text— 商品标价;shipping— 配送文案原文;is_free_shipping— 文案含 "무료" 即视为免费配送;shipping_fee— 运费数值(免费为 0,解析失败为 null);total_price/total_price_text—price + shipping_fee的实付价;card_price/card_price_text— 卡片适用价(.card_line .card_prc);card_name— 卡片名(.card_line .txt);card_discount/card_discount_text— 标价与卡片价的差额;installment/installment_detail— 无息分期按钮文案(.btn_foi .txt)与分期图层详情(.foi_layer .ly_cont);payment_badges— 다나와 在价格旁展示的支付条件徽章的显示标签列表;即使徽章文本为空、只有.ico.cash这类纯类名,也会按白名单合成显示标签(如["현금"]、["포인트"]、["쿠폰"]、["카드"]、["할인"]、["멤버십"]);payment_condition_types— 白名单徽章归一化后的条件类型列表(cash/point/coupon/card/discount/membership);payment_condition_label— 面向用户的支付条件标签(如현금、할인、멤버십,多条件时현금, 할인);cash_only/point_only/coupon_only— 现金专用价 / 积分抵扣价 / 优惠券适用价;card_only_badge/discount_badge/membership_badge— 特定卡片限定 / 折扣条件 / 会员条件徽章曝光价;is_conditional_price—payment_condition_types非空即为 True,表示这不是普通支付价,普通卡片支付时价格可能不同甚至不可用;url— 다나와 桥接链接。
文档特别强调:必须同时查看免费配送、含运费实付价、卡片折扣价、无息分期文案,以及payment_badges/payment_condition_label/is_conditional_price。如果把条件价当普通价放第一位,比价结果就是误导。
4.3compare输出
compare先取搜索结果,再对每个候选商品 best-effort 地附上offers[]。源码实现(L300-L314)中,单个候选的详情调用抛异常时不会中断整体流程,而是把异常记入该行的offers_error字段(meta.detail_extraction标记为"best-effort")。当搜索结果含糊(同名不同规格混排)时,应先展示前 3~5 个候选的标题与pcode,请用户选择。
5. 支付条件徽章解析:白名单设计与源码级原理
这是该 Skill 最有技术含量的部分。다나와 的 offer 行里会混排多种图标(快速配送、商品提示、评论入口等),其中只有支付条件类徽章才影响价格语义。offers()中的处理逻辑(L206-L242):
- 只扫描支付条件徽章区域:选择器限定为
.prc_line .ico, .d_dsc .ico,源码注释明确"其他 ico(빠른배송、안내、상품리뷰 等)是噪声,予以排除"; - 类名 + 文本双通道匹配:白名单表
payment_condition_labels为cash→현금、point→포인트、coupon→쿠폰、card→카드、discount→할인、membership→멤버십。只要元素的 CSS 类包含类型名(如.ico.cash)或文本包含韩文标签,即命中——因此"类名有、文本空"和"文本有、类名无"两种形态都能被捕获; - 统一派生四个层面:
payment_condition_types(归一化类型)、payment_badges(显示标签)、行内布尔标志(cash_only等)、is_conditional_price。
这一设计的验证在 scripts/test_danawa_price_search.py 中相当完整,测试通过 mockproduct_meta与fetch,注入构造好的.diff_itemHTML 片段:
test_class_only_payment_badges_synthesize_display_labels:六种纯类名徽章(<span class="ico cash"></span>等)均正确合成显示标签、类型、布尔字段与is_conditional_price=True;test_payment_badges_are_deduped_with_canonical_class_order:<span class="ico cash"></span>+<span class="ico">현금</span>+<span class="ico card">카드</span>三个徽章去重后得到["현금", "카드"],标签为현금, 카드;test_non_payment_ico_is_not_captured:<span class="ico quick">빠른배송</span>不会被误判为条件价(normal_count=1);test_cli_json_includes_normalized_payment_fields:端到端验证 CLI 输出的 JSON 中包含归一化字段。
运行入口:测试按文件路径加载danawa-price-search/scripts/danawa_search.py(ROOT = parents[1]即仓库根),可用python -m unittest scripts/test_danawa_price_search.py方式执行。
排序实现的源码证据
rows.sort(key=...)(L276-L281)的排序键为:total_price为 None 的行排后 →total_price(缺省回退price)→price→ 商城名。这与文档"total_price오름차순 단일 기준(升序单一标准)"完全对应;meta.sort固定为"total_price"。normal_count/conditional_count在截断到limit之后统计(L287-L289),因此它们反映的是最终返回窗口的构成,而非全量结果。
6. 响应风格:面向用户的表格与结论
在 Discord/Telegram/聊天场景下优先使用表格(instruction.md 原文示例):
| 순위 | 판매처 | 상품가 | 결제조건 | 배송 | 실구매가 | 카드할인가 | 무이자 | 링크 | |---:|---|---:|---|---|---:|---:|---:|---| | 1 | 킴스클럽 | 979,000원 | **현금 전용** | 유/무료 | 979,000원 | - | - | 보기 | | 2 | 롯데ON | 1,073,890원 | 일반 | 무료배송 | 1,073,890원 | - | - | 보기 | | 3 | G마켓 | 1,089,590원 | 일반 | 무료배송 | 1,089,590원 | - | 최대 24개월 | 보기 | | 4 | 옥션 | 1,121,780원 | **쿠폰 적용가** | 무료배송 | 1,121,780원 | 우리카드 303,720원 | 최대 24개월 | 보기 |排序与呈现规则:
- 单一标准
total_price升序;带支付条件的行同样参与排序,最便宜就列第一,不分组、不过滤,只在"결제조건"列按行标注(payment_condition_label优先显示,否则"일반"。具体映射:cash→"현금 전용"、coupon→"쿠폰 적용가"、point→"포인트 적용가"、card→卡片名/卡片条件、discount→"할인 조건"、membership→"멤버십 조건"); - 若
card_price存在且按卡片计价会改变胜者,则在表下另写一行"카드 기준 최저가(按卡片计的最低价)"; - 无息分期可能改变支付条件,须注明"以 다나와 曝光文案为准";
- 第 1 名是条件价时,在摘要中简短补充支付手段限定,并同时给出"卡片支付口径的最低价",让用户一次比完。
摘要示例:
최저 실구매가: G마켓 217,950원 / 무료배송 카드 기준 최저가: 옥션 우리카드 303,720원 무이자: G마켓·옥션 최대 24개월 표기若页面上没有卡片折扣 markup,应写"카드 할인가 표기 없음(无卡片折扣标示)",而不能据此断言结账时不存在任何折扣。
7. 工作流(Workflow)
文档定义的完整执行序列:
- 确认检索词;
- 用
search "<检索词>" --limit 5查看候选; - 候选明确则用对应
pcode执行offers; - 候选含糊则展示前 3~5 个的商品名/价格/
pcode,请用户选择; - offer 按
total_price升序单标准排序(不做支付条件分组),条件价列第一也照实列第一,只在表格列与行级标志中标注; - 有卡片折扣价时另写"卡片口径最低价";第 1 名是条件价时,摘要中一并给出"카드 결제 기준 최저가";
- 附注"以查询时点为准,价格/配送/卡片权益可能变动"。
8. 失败模式与降级策略(Failure modes)
- 搜索 0 结果:把检索词改具体(加品牌、型号、容量等);
- 다나와 HTML/AJAX 结构变化:selector 失效会导致
offers为空或字段缺失; - 白名单需随站方演化维护:若다나와引入新的支付条件徽章类名或文案,必须同步更新徽章白名单(
cash/point/coupon/discount/card/membership类名 +현금/포인트/쿠폰/할인/카드/멤버십文本关键词)以及payment_condition_types/payment_condition_label映射——这也是第 5 节白名单设计的维护面; - 搜索结果价格与 offer AJAX 价格可能不一致(刷新时点、卡片价、联盟链接口径差异);
- 卡片折扣与无息文案只有在 다나와 实际曝光时才确定性地呈现;
- 基于公开表面,高频请求应自行加入 throttling/backoff;
- 出现访问封锁或 CAPTCHA:不尝试绕过,按失败模式上报。
9. 完成判据与合规边界
Done when(完成判据):已确认检索词/型号;至少返回 1 个商品候选,或说明了失败原因;已按时点口径整理各商城的商品价、运费、实付价、卡片折扣价与无息文案;用户响应为表格形式;全程未越出登录/购买/绕封锁的边界。
合规方面,danawa-price-search/SKILL.md 列出了三条硬性规则(即使没有 CLI 也必须遵守):未经用户在操作前明确批准,绝不执行支付、消息/邮件发送、最终提交、取消或公开发布;绝不索取、打印或存储明文凭据;绝不绕过 CAPTCHA、身份验证、电子签名等边界。完整的韩国法务声明(含大法院判例与法条引用)见 danawa-price-search/references/DISCLAIMER.md,商标声明见 danawa-price-search/references/TRADEMARK-LEGAL-STATEMENT.md。此外,本 Skill 与任何被提及的第三方商标持有人或服务运营方均无官方关系,自动化采集仅限个人、非组织用途的查询,不得用于系统性/批量爬取、建库或绕过访问控制。
10. 小结:这个 Skill 的工程要点
从 instruction.md 与源码的对照可以看出,该 Skill 的设计要点可以归纳为四条:
- 表面最小化:只依赖 3 个无认证公开端点,POST AJAX 通过复刻浏览器请求头(
X-Requested-With、Referer、详情页内联 JS 变量表单字段)实现匿名调用; - 单一排序标准 + 行级条件标注:
total_price升序不分組,支付条件全部下放到行内字段(payment_badges/payment_condition_label/is_conditional_price及六个布尔标志),把"哪个价最便宜"和"这个价对我不适用"的判断分别交给排序与展示层; - 白名单驱动的条件徽章解析:类名/文本双通道匹配 + 纯类名徽章合成标签,配合单元测试锁定行为,是应对站方 HTML 结构漂移的可维护设计;
- 只读与失败透明:不执行任何写操作,异常走 stderr JSON + 退出码 2,
compare的详情失败降级为行级offers_error,CAPTCHA/封锁直接上报而非绕过。
更多实现背景可参考 docs/features/danawa-price-search.md(场景、实现表面、本地运行与字段说明)以及 danawa-price-search/skill.json 中的技能元数据。适用前提提醒:该实现针对当前다나와公开页面结构,若站方改版,selector 与徽章白名单需按第 8 节所述同步维护;所有价格结论均应以查询时点为准。
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考