Skill: spzwin/pharmacy-price-monitor (version 1.0.0)

--- name: pharmacy-price-monitor description: 药品电商价格监控与版本识别系统。监控京东、淘宝、拼多多等电商平台药品售价，识别低于标准价的违规商家及非授权版本（海外版、港版等），自动生成 Markdown 分析报告与多期趋势对比。使用 OpenClaw Browser 工具进行数据抓取，绕过反爬机制。适用于：价格体系维护、渠道风险监控、市场调研、代理商合规检查等场景。触发词：监控价格/查价格/价格监控/药品价格/电商比价/渠道监控。 --- # 药品电商价格监控 Skill ## 核心能力 - **价格合规性监控**：识别低于标准价的违规店铺 - **非授权版本识别**：检测海外版、港版、代购等非官方渠道商品 - **多平台监控**：支持京东、淘宝、拼多多 - **趋势分析**：多期数据对比，追踪违规变化 - **报告生成**：自动生成 Markdown 结构化报告 - **数据库存储**：SQLite 本地持久化，支持历史查询和趋势分析 ## 适用场景 - 监控指定药品的电商平台售价是否低于公司 MAP 价 - 识别非授权销售渠道（海外版、港版等） - 定期价格巡检，生成合规报告 - 分析各平台价格分布和违规趋势 --- ## 工作流程 ### 第一步：收集需求参数 必需参数： - **药品名称**：完整药品名称（如："阿莫西林胶囊"） - **标准价格**：公司规定的统一零售价（如：33 元） 可选参数： - **规格**：药品规格（如："0.25g*24粒"） - **监控平台**：京东、淘宝、拼多多（默认全部） 示例： ``` 监控"黛力新 0.5mg:10mg 20片/盒"的价格，标准价50元，查京东和淘宝 ``` --- ### 第二步：数据抓取（Browser 工具） **推荐方法：使用 Browser 工具直接抓取** - 不依赖 Cookie导出 - 不受登录态过期影响 - 翻页更可靠 #### 2.1 京东抓取流程（✅已验证 2026-04-17） **分页机制（2024年京东改版后）：** - 底部显示 `"1/10 共10页 上一页 1 2 3 4 5 6 7 下一页"` - URL **不变**，内容通过 AJAX 无刷新加载 - ❌ 点击"下一页"文字链接 → 无效（分页器不更新） - ✅ 点击数字按钮（如"2"）→ 触发 AJAX 加载 **数据提取选择器（2024年京东新结构）：** ``` 商品卡片：document.querySelectorAll('[data-sku]') 标题：item.querySelector('[class*="title"]').innerText 价格：item.querySelector('[class*="price"]').innerText（需清洗换行） 店铺：item.querySelector('[class*="name"]').innerText ``` **完整操作流程：** ``` 1. 打开搜索页 browser action:open url:"https://search.jd.com/Search?keyword=<关键词>" 2. 等待8秒让JS渲染 browser action:act kind:wait timeMs:8000 3. 提取第1页数据（JS evaluate） browser action:act kind:evaluate fn:<提取JS> → 返回 'clicked page X' 表示成功 → 返回 'no more pages' 表示已到最后一页，停止 4. 执行翻页JS（京东用"下一页"按钮，淘宝用数字按钮） browser action:act kind:evaluate fn:<翻页JS> 5. 等待6秒 browser action:act kind:wait timeMs:6000 6. 提取当前页数据 → 重复步骤4-6直到返回 'no more pages' 7. 合并所有页数据，去重 ``` **⚠️ 重要：必须翻到"no more pages"才停止！** - 京东有10-20页，必须逐页翻完 - 中途停止会丢失后面页面的数据 **停止条件：** - 翻页JS返回 `'no more pages'` → 已到最后一页 - 翻页后商品数量不变 → 可能已卡在最后一页，停止 **提取数据 JS（每页执行）：** ```javascript () => { const results = []; const items = document.querySelectorAll('[data-sku]'); for (const item of items) { const titleEl = item.querySelector('[class*="title"]'); const priceEl = item.querySelector('[class*="price"]'); const shopEl = item.querySelector('[class*="name"]'); const title = titleEl ? titleEl.innerText.trim() : ''; let price = priceEl ? priceEl.innerText.trim().replace(/\s/g, '') : ''; const shop = shopEl ? shopEl.innerText.trim() : ''; if (title && price) { results.push({ title, price, shop, platform: '京东' }); } } return JSON.stringify(results); } ``` **翻页 JS（京东 - 推荐方案）：** ```javascript // 京东翻页 - 直接点击"下一页"按钮 // 适用于京东搜索页（2024年改版后验证） () => { const nextBtns = document.querySelectorAll('[class*="next"]'); for (const btn of nextBtns) { const txt = btn.innerText ? btn.innerText.trim() : ''; if (txt === '下一页') { btn.click(); return 'clicked next page'; } } return 'no more pages'; } ``` **翻页 JS（备选方案 - 动态找数字按钮）：** ```javascript // 如果"下一页"按钮失效，使用数字按钮翻页 () => { const allElements = document.querySelectorAll('*'); let maxPage = 0; let nextBtn = null; for (const el of allElements) { try { const txt = el.innerText ? el.innerText.trim() : ''; if (txt.isdigit() && el.childNodes.length === 1) { const num = parseInt(txt); const parent = el.parentElement; if (parent && parent.innerText && parent.innerText.includes('下一页')) { if (num > maxPage) { maxPage = num; nextBtn = el; } } } } catch(e) {} } if (nextBtn) { nextBtn.click(); return 'clicked page ' + maxPage; } return 'no more pages'; } ``` **⚠️ 注意事项：** - 翻页后必须等待 6 秒（AJAX 加载需要时间） - 用 `networkidle` 判断加载完成会永远超时（京东有持续追踪请求） - 翻页失败特征：点击后商品数量不变 → 停止翻页 --- #### 2.2 淘宝抓取流程（✅已验证 2026-04-17） **已知限制：** - URL 的 page 参数会被服务器强制重定向回 page=1 - ❌ 直接访问 `&page=2` 的 URL → 回到第1页 - ✅ 必须用点击数字按钮翻页 **分页机制：** - 显示"上一页 当前第1页 /5 下一页" - 点击数字按钮"2"可以正常翻页（不是URL参数） **提取数据 JS：** ```javascript () => { const results = []; const seen = new Set(); const links = document.querySelectorAll('a[href*="detail.tmall.com"], a[href*="item.htm"]'); for (const link of links) { const text = link.innerText || ''; if (text.includes('黛力新') && text.includes('¥') && text.includes('片')) { const priceMatch = text.match(/¥\s*([\d.]+)/); const dealsMatch = text.match(/(\d+[\d万]*)\s*人付款/); const price = priceMatch ? priceMatch[1] : ''; const deals = dealsMatch ? dealsMatch[1] : ''; let title = '商品标题'; // 提取店铺名 const lines = text.split('\n').filter(l => l.trim()); let shop = ''; for (let j = lines.length - 1; j >= 0; j--) { const l = lines[j].trim(); if (l.includes('旗舰店') || l.includes('大药房') || l.includes('医药专营')) { shop = l; break; } } if (price && !seen.has(price)) { seen.add(price); results.push({ title, price, shop, deals, platform: '淘宝' }); } } } return JSON.stringify(results); } ``` **翻页 JS（淘宝通用 - 动态版本）：** ```javascript // 淘宝翻页 - 自动点击下一个数字按钮 () => { const allElements = document.querySelectorAll('*'); let maxPage = 0; let nextBtn = null; for (const el of allElements) { try { const txt = el.innerText ? el.innerText.trim() : ''; if (txt.isdigit() && el.childNodes.length === 1) { const num = parseInt(txt); const parent = el.parentElement; if (parent && parent.innerText && parent.innerText.includes('下一页')) { if (num > maxPage) { maxPage = num; nextBtn = el; } } } } catch(e) {} } if (nextBtn) { nextBtn.click(); return 'clicked page ' + maxPage; } return 'no more pages'; } ``` --- #### 2.3 拼多多抓取流程（✅已验证 2026-04-20） **关键技术发现（⚠️ 重要修正 2026-04-20）：** - `window.rawData.stores.store.data.ssrListData.list` 只在**首屏 SSR 时注入一次** - 滚动后 AJAX 加载的新商品**不在** `window.rawData` 中，翻页后它仍然是 20 条 - ✅ 正确方法：**滚动完成后，从 DOM 提取所有商品文本**，再解析结构 - 商品文本特征：同时包含 `黛力新` + `¥` + `片` + 长度 30~800 字符的 DOM 元素 - 价格字段在 DOM 中被拆成多行（如 `¥\n42\n.6`），需要清洗合并 - 翻页机制：**瀑布流无限滚动**，`scrollHeight` 会随加载不断扩大 - 停止条件：连续 3 次滚动后 DOM 中 drug items 数量不变（建议上限 20 次滚动） **操作流程：** ``` 1. browser action:open url:"https://mobile.yangkeduo.com/search_result.html?search_key=<关键词>" 2. 等待8秒（SSR渲染完成） 3. 循环（最多20次）： a. 执行滚动 JS：window.scrollBy(0, 800) b. 等待3秒（让 AJAX 加载） c. 检查 drug items 数量 d. 若数量不变次数 >= 3，停止滚动 4. 执行 DOM 提取 JS（见下方） 5. Python 解析价格/标题/销量，生成报告 ``` **滚动 JS（拼多多）：** ```javascript () => { window.scrollBy(0, 800); return 'scrolled, scrollH=' + document.body.scrollHeight; } ``` **DOM 提取 JS（滚动完成后执行，全量提取）：** ```javascript // 从 DOM 提取所有商品（用于滚动完成后一次性提取） () => { var all = document.querySelectorAll('*'); var rawItems = []; for (var i = 0; i < all.length; i++) { var txt = all[i].innerText || ''; if (txt.indexOf('黛力新') === -1) continue; if (txt.indexOf('片') === -1) continue; if (txt.indexOf('¥') === -1) continue; if (txt.length < 30 || txt.length > 800) continue; rawItems.push(txt); } // 按前50字符去重 var seen = {}; var unique = []; for (var j = 0; j < rawItems.length; j++) { var key = rawItems[j].substring(0, 50); if (!seen[key]) { seen[key] = true; unique.push(rawItems[j]); } } return 'unique:' + unique.length + ' samples:' + JSON.stringify(unique.slice(0,2)).substring(0, 300); } ``` **Python 解析脚本（处理 DOM 原始文本）：** ```python import re def parse_pinduoduo_items(raw_texts): """解析拼多多 DOM 原始文本，提取结构化商品数据""" results = [] for txt in raw_texts: lines = txt.split('\n') title = '' price = '' sales = '' for li, line in enumerate(lines): l = line.strip() # 取第一个含药品名的行作为标题 if not title and ('黛力新' in l or '氟哌噻吨' in l or '加乐舒' in l): title = l # ¥ 符号后面跟着数字/小数点是价格 if l == '¥' and li + 1 < len(lines): p1 = lines[li+1].strip() p2 = lines[li+2].strip() if li+2 < len(lines) else '' # 处理 ¥ 42 .6 格式（价格被拆成多行） if p2 and re.match(r'^\.[0-9]$', p2): price = p1 + p2 else: price = p1 # 销量 if '本店已拼' in l or '全店总售' in l: sales = l break if title and price: results.append({'title': title[:80], 'price': price, 'sales': sales, 'platform': '拼多多'}) return results ``` **⚠️ 注意事项：** - ❌ 不要依赖 `window.rawData` 取滚动后的数据（它不更新！） - ✅ 必须从 DOM 提取：`querySelectorAll` + 关键词过滤 - 等待时间：首次加载 8 秒，每次滚动后等待 3 秒 - 价格清洗：`¥\n42\n.6` → `42.6` - 拼多多无限滚动：热门药品搜索可达数百条，建议设滚动上限（如 20 次） --- ### 第三步：数据库存储 #### 3.1 数据库初始化 数据库路径：`~/.openclaw/skills/pharmacy-price-monitor/price_monitor.db` **数据库架构：** ```sql -- 药品表（按药品名称去重） CREATE TABLE IF NOT EXISTS drugs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, spec TEXT, standard_price REAL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 抓取记录表（每次抓取一条记录） CREATE TABLE IF NOT EXISTS crawl_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, drug_id INTEGER NOT NULL, platform TEXT NOT NULL, total_items INTEGER, min_price REAL, max_price REAL, avg_price REAL, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (drug_id) REFERENCES drugs(id) ); -- 商品详情表 CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, crawl_record_id INTEGER NOT NULL, drug_id INTEGER NOT NULL, platform TEXT NOT NULL, title TEXT, price REAL NOT NULL, shop TEXT, deals TEXT, url TEXT, is_low_price BOOLEAN DEFAULT 0, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (crawl_record_id) REFERENCES crawl_records(id), FOREIGN KEY (drug_id) REFERENCES drugs(id) ); ``` #### 3.2 数据存储脚本 每次抓取完成后，将数据存入数据库： ```python import sqlite3, json from datetime import datetime DB_PATH = "~/.openclaw/skills/pharmacy-price-monitor/price_monitor.db" def init_db(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS drugs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, spec TEXT, standard_price REAL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''') c.execute('''CREATE TABLE IF NOT EXISTS crawl_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, drug_id INTEGER NOT NULL, platform TEXT NOT NULL, total_items INTEGER, min_price REAL, max_price REAL, avg_price REAL, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (drug_id) REFERENCES drugs(id))''') c.execute('''CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, crawl_record_id INTEGER NOT NULL, drug_id INTEGER NOT NULL, platform TEXT NOT NULL, title TEXT, price REAL NOT NULL, shop TEXT, deals TEXT, url TEXT, is_low_price BOOLEAN DEFAULT 0, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (crawl_record_id) REFERENCES crawl_records(id), FOREIGN KEY (drug_id) REFERENCES drugs(id))''') conn.commit() return conn def save_crawl(drug_name, spec, standard_price, platform, products): conn = init_db() c = conn.cursor() # 插入或获取药品ID c.execute('''INSERT OR IGNORE INTO drugs (name, spec, standard_price) VALUES (?, ?, ?)''', (drug_name, spec, standard_price)) c.execute('SELECT id FROM drugs WHERE name = ?', (drug_name,)) drug_id = c.fetchone()[0] # 计算价格统计 prices = [float(p['price'].replace('¥','').replace(',','')) for p in products] min_p, max_p, avg_p = min(prices), max(prices), sum(prices)/len(prices) # 插入抓取记录 c.execute('''INSERT INTO crawl_records (drug_id, platform, total_items, min_price, max_price, avg_price) VALUES (?, ?, ?, ?, ?, ?)''', (drug_id, platform, len(products), min_p, max_p, avg_p)) crawl_record_id = c.lastrowid # 插入商品 for p in products: price_val = float(p['price'].replace('¥','').replace(',','')) is_low = 1 if standard_price and price_val < standard_price else 0 c.execute('''INSERT INTO products (crawl_record_id, drug_id, platform, title, price, shop, deals, is_low_price) VALUES (?, ?, ?, ?, ?, ?, ?, ?)''', (crawl_record_id, drug_id, platform, p.get('title',''), price_val, p.get('shop',''), p.get('deals',''), is_low)) conn.commit() conn.close() print(f"已存储 {len(products)} 条商品数据到数据库") # 使用示例： # save_crawl("黛力新", "0.5mg:10mg 20片/盒", 50.0, "京东", jd_products) # save_crawl("黛力新", "0.5mg:10mg 20片/盒", 50.0, "淘宝", taobao_products) ``` #### 3.3 历史查询 ```python def query_history(drug_name, platform=None, limit=10): conn = sqlite3.connect(DB_PATH) c = conn.cursor() if platform: c.execute('''SELECT cr.crawl_time, cr.platform, cr.total_items, cr.min_price, cr.max_price, cr.avg_price FROM crawl_records cr JOIN drugs d ON cr.drug_id = d.id WHERE d.name = ? AND cr.platform = ? ORDER BY cr.crawl_time DESC LIMIT ?''', (drug_name, platform, limit)) else: c.execute('''SELECT cr.crawl_time, cr.platform, cr.total_items, cr.min_price, cr.max_price, cr.avg_price FROM crawl_records cr JOIN drugs d ON cr.drug_id = d.id WHERE d.name = ? ORDER BY cr.crawl_time DESC LIMIT ?''', (drug_name, limit)) results = c.fetchall() conn.close() return results # 查询示例：query_history("黛力新", platform="京东") ``` --- ### 第四步：数据处理 #### 4.1 价格清洗 京东价格含换行符，需清洗： ```python price = price.replace('¥', '').replace('\n', '').replace(' ', '') # "¥\n50" → "50" ``` #### 4.2 去重 多页数据按 title+price 去重： ```python seen = set() unique = [] for p in results: key = p['title'][:30] + p['price'] if key not in seen: seen.add(key) unique.append(p) ``` --- ### 第五步：报告生成 生成 Markdown 报告，结构： ```markdown # 药品电商价格监控报告 ## 一、调研目标 - 药品：XXX - 规格：XXX - 标准价格：XXX 元 - 平台：京东、淘宝 ## 二、数据概览 | 平台 | 商品数 | 价格区间 | |------|--------|----------| | 京东 | X | ¥X - ¥X | | 淘宝 | X | ¥X - ¥X | ## 三、低价商品清单 | 平台 | 店铺 | 价格 | 备注 | |------|------|------|------| ## 四、购买建议 | 场景 | 推荐 | 价格 | |------|------|------| ``` --- ## 快速开始 **抓取京东全部数据（动态翻页）：** ``` 1. browser action:open url:"https://search.jd.com/Search?keyword=黛力新+0.5mg+10mg+20片" 2. 等待8秒 3. 执行提取JS → 保存第1页 4. 执行翻页JS → 返回 'clicked page X' 5. 等待6秒 → 执行提取JS → 保存第X页 6. 重复步骤4-5，直到返回 'no more pages' 7. 合并所有页数据，去重 ``` **抓取淘宝数据：** ``` 1. browser action:open url:"https://s.taobao.com/search?q=黛力新+0.5mg+10mg+20片" 2. 等待8秒 3. 执行提取JS → 保存 4. 执行翻页JS(2) → 等待6秒 → 执行提取JS 5. 重复直到翻页失败 6. 合并数据，去重 ``` **抓取拼多多数据：** ``` 1. browser action:open url:"https://mobile.yangkeduo.com/search_result.html?search_key=黛力新+0.5mg+10mg+20片" 2. 等待8秒 3. 执行提取JS → 保存数据 4. 执行滚动JS → 等待5秒 5. 检查 lastPage： - lastPage === false → 重复步骤3-5 - lastPage === true → 停止 6. 合并所有数据，去重 ``` --- ## 重要注意事项 ### 1. 翻页核心区别 | 平台 | 翻页方式 | URL变化 | 关键 | |------|----------|---------|------| | 京东 | 点击数字按钮 | 不变 | ❌ 不能用"下一页"文字链接 | | 淘宝 | 点击数字按钮 | 可能被重定向 | ✅ 以页面文字为准 | | 拼多多 | `window.scrollBy(0,800)` + DOM提取 | 不变 | ✅ 滚动完成后从 DOM 提取（`window.rawData` 不更新！） | ### 2. 等待时间 - 京东首次加载：8秒 - 京东翻页后：6秒 - 淘宝每次翻页：6秒 - 拼多多每次滚动后：3秒 ### 3. 防封注意 - 每页间隔 1-2 秒随机延迟 - 避免短时间内大量请求 - 优先使用 Browser 工具（保持登录态） - 拼多多：✅ 已验证可从 DOM 提取（`window.rawData` 在滚动后不更新，必须用 querySelectorAll + 关键词过滤） ### 4. 各平台验证时间 - 京东：✅ 已验证 2026-04-17 - 淘宝：✅ 已验证 2026-04-17 - 拼多多：✅ 已验证 2026-04-20

path: db_storage.py
content: #!/usr/bin/env python3
"""
药品价格监控数据库存储模块
数据库路径：~/.openclaw/skills/pharmacy-price-monitor/price_monitor.db
"""

import sqlite3
import os
from datetime import datetime

DB_DIR = os.path.expanduser("~/.openclaw/skills/pharmacy-price-monitor")
DB_PATH = os.path.join(DB_DIR, "price_monitor.db")


def get_db_path():
    return DB_PATH


def init_db():
    """初始化数据库，创建表结构"""
    os.makedirs(DB_DIR, exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()

    c.execute('''CREATE TABLE IF NOT EXISTS drugs (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL UNIQUE,
        spec TEXT,
        standard_price REAL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')

    c.execute('''CREATE TABLE IF NOT EXISTS crawl_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        drug_id INTEGER NOT NULL,
        platform TEXT NOT NULL,
        total_items INTEGER,
        min_price REAL,
        max_price REAL,
        avg_price REAL,
        crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (drug_id) REFERENCES drugs(id))''')

    c.execute('''CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        crawl_record_id INTEGER NOT NULL,
        drug_id INTEGER NOT NULL,
        platform TEXT NOT NULL,
        title TEXT,
        price REAL NOT NULL,
        shop TEXT,
        deals TEXT,
        url TEXT,
        is_low_price BOOLEAN DEFAULT 0,
        notes TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (crawl_record_id) REFERENCES crawl_records(id),
        FOREIGN KEY (drug_id) REFERENCES drugs(id))''')

    conn.commit()
    return conn


def parse_price(price_str):
    """解析价格字符串为浮点数"""
    if not price_str:
        return 0.0
    return float(str(price_str).replace('¥', '').replace(',', '').replace(' ', ''))


def save_crawl(drug_name, spec, standard_price, platform, products, conn=None):
    """
    保存一次抓取数据到数据库

    参数:
        drug_name: 药品名称
        spec: 规格
        standard_price: 标准价格
        platform: 平台（京东/淘宝/拼多多）
        products: 商品列表，每项包含 title, price, shop, deals, url
        conn: 可选的数据库连接（用于批量操作）
    """
    should_close = False
    if conn is None:
        conn = init_db()
        should_close = True

    c = conn.cursor()

    # 插入或获取药品ID
    c.execute('''INSERT OR IGNORE INTO drugs (name, spec, standard_price)
                 VALUES (?, ?, ?)''', (drug_name, spec, standard_price))
    c.execute('SELECT id FROM drugs WHERE name = ?', (drug_name,))
    row = c.fetchone()
    drug_id = row[0] if row else None

    if drug_id is None:
        print("错误：无法获取药品ID")
        return None

    # 计算价格统计
    prices = [parse_price(p.get('price', '0')) for p in products if p.get('price')]
    if prices:
        min_p, max_p, avg_p = min(prices), max(prices), sum(prices) / len(prices)
    else:
        min_p, max_p, avg_p = 0, 0, 0

    # 插入抓取记录
    c.execute('''INSERT INTO crawl_records
                 (drug_id, platform, total_items, min_price, max_price, avg_price)
                 VALUES (?, ?, ?, ?, ?, ?)''',
              (drug_id, platform, len(products), min_p, max_p, avg_p))
    crawl_record_id = c.lastrowid

    # 插入商品
    for p in products:
        price_val = parse_price(p.get('price', '0'))
        is_low = 1 if (standard_price and price_val < standard_price) else 0
        c.execute('''INSERT INTO products
                     (crawl_record_id, drug_id, platform, title, price, shop, deals, url, is_low_price)
                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)''',
                  (crawl_record_id, drug_id, platform,
                   p.get('title', ''), price_val, p.get('shop', ''),
                   p.get('deals', ''), p.get('url', ''), is_low))

    conn.commit()
    print(f"[{platform}] 已存储 {len(products)} 条商品数据到数据库 (记录ID: {crawl_record_id})")

    if should_close:
        conn.close()

    return crawl_record_id


def query_history(drug_name, platform=None, limit=10):
    """查询历史抓取记录"""
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()

    if platform:
        c.execute('''SELECT cr.id, cr.crawl_time, cr.platform, cr.total_items,
                            cr.min_price, cr.max_price, cr.avg_price
                     FROM crawl_records cr
                     JOIN drugs d ON cr.drug_id = d.id
                     WHERE d.name = ? AND cr.platform = ?
                     ORDER BY cr.crawl_time DESC LIMIT ?''',
                  (drug_name, platform, limit))
    else:
        c.execute('''SELECT cr.id, cr.crawl_time, cr.platform, cr.total_items,
                            cr.min_price, cr.max_price, cr.avg_price
                     FROM crawl_records cr
                     JOIN drugs d ON cr.drug_id = d.id
                     WHERE d.name = ?
                     ORDER BY cr.crawl_time DESC LIMIT ?''',
                  (drug_name, limit))

    results = c.fetchall()
    conn.close()
    return results


def query_low_price_products(drug_name, platform=None, standard_price=None):
    """查询低于标准价的商品"""
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()

    if platform:
        c.execute('''SELECT p.platform, p.shop, p.price, p.title, p.deals, p.is_low_price
                     FROM products p
                     JOIN drugs d ON p.drug_id = d.id
                     WHERE d.name = ? AND p.platform = ? AND p.is_low_price = 1
                     ORDER BY p.price ASC''',
                  (drug_name, platform))
    else:
        c.execute('''SELECT p.platform, p.shop, p.price, p.title, p.deals, p.is_low_price
                     FROM products p
                     JOIN drugs d ON p.drug_id = d.id
                     WHERE d.name = ? AND p.is_low_price = 1
                     ORDER BY p.price ASC''',
                  (drug_name,))

    results = c.fetchall()
    conn.close()
    return results


def get_latest_crawl_id(drug_name, platform):
    """获取最新一次抓取记录ID"""
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''SELECT cr.id
                 FROM crawl_records cr
                 JOIN drugs d ON cr.drug_id = d.id
                 WHERE d.name = ? AND cr.platform = ?
                 ORDER BY cr.crawl_time DESC LIMIT 1''',
              (drug_name, platform))
    row = c.fetchone()
    conn.close()
    return row[0] if row else None


def get_products_by_crawl(crawl_record_id):
    """获取某次抓取记录的所有商品"""
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''SELECT title, price, shop, deals, url, is_low_price
                 FROM products WHERE crawl_record_id = ? ORDER BY price ASC''',
              (crawl_record_id,))
    results = c.fetchall()
    conn.close()
    return results


def print_history_table(drug_name, platform=None):
    """格式化打印历史记录"""
    rows = query_history(drug_name, platform)
    if not rows:
        print(f"暂无 '{drug_name}' 的历史抓取记录")
        return

    print(f"\n{'='*80}")
    print(f"药品: {drug_name}")
    if platform:
        print(f"平台: {platform}")
    print(f"{'='*80}")
    print(f"{'时间':<20} {'平台':<6} {'商品数':<6} {'最低价':<8} {'最高价':<8} {'均价':<8}")
    print("-"*80)

    for row in rows:
        crawl_id, crawl_time, plat, total, min_p, max_p, avg_p = row
        print(f"{crawl_time:<20} {plat:<6} {total:<6} ¥{min_p:<7.2f} ¥{max_p:<7.2f} ¥{avg_p:<7.2f}")


if __name__ == "__main__":
    # 初始化数据库
    init_db()
    print(f"数据库已初始化: {DB_PATH}")


path: scripts/analyze_prices.py
content: #!/usr/bin/env python3
"""
价格合规性检查脚本
识别低于标准价的违规商品
"""

import json
import argparse
from typing import List, Dict, Any


def analyze_prices(data: List[Dict[str, Any]], standard_price: float) -> List[Dict[str, Any]]:
    """
    分析商品价格，识别违规商品

    Args:
        data: 商品数据列表，每个商品包含 platform, shop, title, price, url
        standard_price: 标准价格

    Returns:
        违规商品列表
    """
    violations = []

    for item in data:
        try:
            price = float(item.get('price', 0))
            if price < standard_price:
                violations.append({
                    'platform': item.get('platform', ''),
                    'shop': item.get('shop', ''),
                    'title': item.get('title', ''),
                    'actual_price': price,
                    'standard_price': standard_price,
                    'diff': round(price - standard_price, 2),
                    'url': item.get('url', '')
                })
        except (ValueError, TypeError):
            continue

    return violations


def load_data(file_path: str) -> List[Dict[str, Any]]:
    """加载 JSON 数据文件"""
    with open(file_path, 'r', encoding='utf-8') as f:
        return json.load(f)


def save_data(data: List[Dict[str, Any]], file_path: str):
    """保存 JSON 数据文件"""
    with open(file_path, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def main():
    parser = argparse.ArgumentParser(description='价格合规性检查')
    parser.add_argument('--data', required=True, help='原始数据文件 (JSON)')
    parser.add_argument('--price', type=float, required=True, help='标准价格')
    parser.add_argument('--output', required=True, help='输出文件 (JSON)')

    args = parser.parse_args()

    # 加载数据
    data = load_data(args.data)

    # 分析价格
    violations = analyze_prices(data, args.price)

    # 保存结果
    save_data(violations, args.output)

    print(f"✅ 分析完成")
    print(f"📊 总商品数: {len(data)}")
    print(f"⚠️ 违规商品数: {len(violations)}")
    print(f"💾 结果已保存到: {args.output}")


if __name__ == '__main__':
    main()


path: scripts/db.py
content: #!/usr/bin/env python3
"""
药品价格监控数据库管理脚本
SQLite 数据库，支持按平台、按时间存储数据
"""

import sqlite3
import json
import argparse
from datetime import datetime
from typing import List, Dict, Any, Optional
import os

# 数据库路径
DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'data', 'pharmacy_monitor.db')


def get_db_path() -> str:
    """获取数据库路径"""
    return os.path.abspath(DB_PATH)


def init_db(db_path: Optional[str] = None) -> sqlite3.Connection:
    """初始化数据库，创建表结构"""
    if db_path is None:
        db_path = get_db_path()

    # 确保目录存在
    os.makedirs(os.path.dirname(db_path), exist_ok=True)

    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    # 创建商品表
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS products (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            platform TEXT NOT NULL,
            crawl_date TEXT NOT NULL,
            drug_name TEXT NOT NULL,
            drug_spec TEXT,
            title TEXT NOT NULL,
            shop TEXT,
            price REAL,
            url TEXT,
            is_violation INTEGER DEFAULT 0,
            is_unauthorized INTEGER DEFAULT 0,
            unauthorized_reason TEXT,
            created_at TEXT DEFAULT CURRENT_TIMESTAMP,
            UNIQUE(platform, crawl_date, url)
        )
    ''')

    # 创建汇总表
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS crawl_summary (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            platform TEXT NOT NULL,
            crawl_date TEXT NOT NULL,
            drug_name TEXT NOT NULL,
            drug_spec TEXT,
            total_products INTEGER DEFAULT 0,
            total_violations INTEGER DEFAULT 0,
            total_unauthorized INTEGER DEFAULT 0,
            standard_price REAL,
            created_at TEXT DEFAULT CURRENT_TIMESTAMP,
            UNIQUE(platform, crawl_date, drug_name, drug_spec)
        )
    ''')

    # 创建索引
    cursor.execute('''
        CREATE INDEX IF NOT EXISTS idx_platform_date
        ON products(platform, crawl_date)
    ''')
    cursor.execute('''
        CREATE INDEX IF NOT EXISTS idx_drug_name
        ON products(drug_name)
    ''')
    cursor.execute('''
        CREATE INDEX IF NOT EXISTS idx_crawl_date
        ON products(crawl_date)
    ''')

    conn.commit()
    return conn


def save_products(products: List[Dict], platform: str, drug_name: str,
                  drug_spec: str, standard_price: float,
                  db_path: Optional[str] = None) -> int:
    """
    保存商品数据到数据库

    Args:
        products: 商品列表
        platform: 平台名称
        drug_name: 药品名称
        drug_spec: 药品规格
        standard_price: 标准价格
        db_path: 数据库路径

    Returns:
        保存的商品数量
    """
    conn = init_db(db_path)
    cursor = conn.cursor()

    crawl_date = datetime.now().strftime('%Y-%m-%d')
    saved_count = 0
    violation_count = 0
    unauthorized_count = 0

    for product in products:
        try:
            price = float(product.get('price', 0))
            is_violation = 1 if price < standard_price else 0
            is_unauthorized = product.get('is_unauthorized', 0)
            unauthorized_reason = product.get('unauthorized_reason', '')

            if is_violation:
                violation_count += 1
            if is_unauthorized:
                unauthorized_count += 1

            cursor.execute('''
                INSERT OR REPLACE INTO products
                (platform, crawl_date, drug_name, drug_spec, title, shop,
                 price, url, is_violation, is_unauthorized, unauthorized_reason)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ''', (
                platform,
                crawl_date,
                drug_name,
                drug_spec or '',
                product.get('title', ''),
                product.get('shop', ''),
                price,
                product.get('url', ''),
                is_violation,
                is_unauthorized,
                unauthorized_reason
            ))
            saved_count += 1
        except Exception as e:
            print(f"保存商品失败: {e}")
            continue

    # 保存汇总
    cursor.execute('''
        INSERT OR REPLACE INTO crawl_summary
        (platform, crawl_date, drug_name, drug_spec, total_products,
         total_violations, total_unauthorized, standard_price)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    ''', (
        platform,
        crawl_date,
        drug_name,
        drug_spec or '',
        saved_count,
        violation_count,
        unauthorized_count,
        standard_price
    ))

    conn.commit()
    conn.close()

    return saved_count


def get_history(platform: str, drug_name: str,
                days: int = 30,
                db_path: Optional[str] = None) -> List[Dict]:
    """
    获取历史数据

    Args:
        platform: 平台名称
        drug_name: 药品名称
        days: 查询最近天数
        db_path: 数据库路径

    Returns:
        历史数据列表
    """
    conn = init_db(db_path)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()

    cursor.execute('''
        SELECT * FROM crawl_summary
        WHERE platform = ?
          AND drug_name = ?
          AND crawl_date >= date('now', '-' || ? || ' days')
        ORDER BY crawl_date DESC
    ''', (platform, drug_name, days))

    results = [dict(row) for row in cursor.fetchall()]
    conn.close()
    return results


def get_trend(drug_name: str, db_path: Optional[str] = None) -> Dict:
    """
    获取趋势分析数据

    Args:
        drug_name: 药品名称
        db_path: 数据库路径

    Returns:
        趋势分析数据
    """
    conn = init_db(db_path)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()

    # 获取最近7天的汇总
    cursor.execute('''
        SELECT
            crawl_date,
            platform,
            SUM(total_products) as total_products,
            SUM(total_violations) as total_violations,
            SUM(total_unauthorized) as total_unauthorized
        FROM crawl_summary
        WHERE drug_name = ?
          AND crawl_date >= date('now', '-7 days')
        GROUP BY crawl_date, platform
        ORDER BY crawl_date DESC
    ''', (drug_name,))

    daily_data = [dict(row) for row in cursor.fetchall()]

    # 获取上期数据对比
    cursor.execute('''
        SELECT
            platform,
            SUM(total_products) as total_products,
            SUM(total_violations) as total_violations,
            SUM(total_unauthorized) as total_unauthorized
        FROM crawl_summary
        WHERE drug_name = ?
          AND crawl_date >= date('now', '-14 days')
          AND crawl_date < date('now', '-7 days')
        GROUP BY platform
    ''', (drug_name,))

    prev_week = {row['platform']: dict(row) for row in cursor.fetchall()}

    conn.close()

    return {
        'daily_data': daily_data,
        'prev_week': prev_week
    }


def get_violations(platform: str, drug_name: str,
                   days: int = 7,
                   db_path: Optional[str] = None) -> List[Dict]:
    """
    获取违规商品列表

    Args:
        platform: 平台名称
        drug_name: 药品名称
        days: 查询最近天数
        db_path: 数据库路径

    Returns:
        违规商品列表
    """
    conn = init_db(db_path)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()

    cursor.execute('''
        SELECT * FROM products
        WHERE platform = ?
          AND drug_name = ?
          AND crawl_date >= date('now', '-' || ? || ' days')
          AND is_violation = 1
        ORDER BY price ASC
    ''', (platform, drug_name, days))

    results = [dict(row) for row in cursor.fetchall()]
    conn.close()
    return results


def get_unauthorized(platform: str, drug_name: str,
                      days: int = 7,
                      db_path: Optional[str] = None) -> List[Dict]:
    """
    获取非授权商品列表

    Args:
        platform: 平台名称
        drug_name: 药品名称
        days: 查询最近天数
        db_path: 数据库路径

    Returns:
        非授权商品列表
    """
    conn = init_db(db_path)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()

    cursor.execute('''
        SELECT * FROM products
        WHERE platform = ?
          AND drug_name = ?
          AND crawl_date >= date('now', '-' || ? || ' days')
          AND is_unauthorized = 1
        ORDER BY crawl_date DESC
    ''', (platform, drug_name, days))

    results = [dict(row) for row in cursor.fetchall()]
    conn.close()
    return results


def export_to_json(platform: str, drug_name: str,
                   output_path: str,
                   days: int = 7,
                   db_path: Optional[str] = None):
    """
    导出数据到 JSON 文件

    Args:
        platform: 平台名称
        drug_name: 药品名称
        output_path: 输出文件路径
        days: 查询最近天数
        db_path: 数据库路径
    """
    conn = init_db(db_path)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()

    # 获取商品数据
    cursor.execute('''
        SELECT * FROM products
        WHERE platform = ?
          AND drug_name = ?
          AND crawl_date >= date('now', '-' || ? || ' days')
        ORDER BY crawl_date DESC, price ASC
    ''', (platform, drug_name, days))

    products = [dict(row) for row in cursor.fetchall()]

    # 获取汇总数据
    cursor.execute('''
        SELECT * FROM crawl_summary
        WHERE platform = ?
          AND drug_name = ?
          AND crawl_date >= date('now', '-' || ? || ' days')
        ORDER BY crawl_date DESC
    ''', (platform, drug_name, days))

    summary = [dict(row) for row in cursor.fetchall()]

    conn.close()

    data = {
        'platform': platform,
        'drug_name': drug_name,
        'export_time': datetime.now().isoformat(),
        'products': products,
        'summary': summary
    }

    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

    print(f"✅ 导出完成: {output_path}")
    print(f"   商品数量: {len(products)}")
    print(f"   汇总记录: {len(summary)}")


def main():
    parser = argparse.ArgumentParser(description='药品价格监控数据库管理')
    subparsers = parser.add_subparsers(dest='command', help='子命令')

    # init 命令
    subparsers.add_parser('init', help='初始化数据库')

    # save 命令
    save_parser = subparsers.add_parser('save', help='保存商品数据')
    save_parser.add_argument('--platform', required=True, help='平台名称')
    save_parser.add_argument('--drug-name', required=True, help='药品名称')
    save_parser.add_argument('--spec', help='药品规格')
    save_parser.add_argument('--price', type=float, required=True, help='标准价格')
    save_parser.add_argument('--data', required=True, help='商品数据文件 (JSON)')

    # history 命令
    history_parser = subparsers.add_parser('history', help='查询历史数据')
    history_parser.add_argument('--platform', required=True, help='平台名称')
    history_parser.add_argument('--drug-name', required=True, help='药品名称')
    history_parser.add_argument('--days', type=int, default=30, help='查询天数')

    # trend 命令
    trend_parser = subparsers.add_parser('trend', help='趋势分析')
    trend_parser.add_argument('--drug-name', required=True, help='药品名称')

    # violations 命令
    violations_parser = subparsers.add_parser('violations', help='查询违规商品')
    violations_parser.add_argument('--platform', required=True, help='平台名称')
    violations_parser.add_argument('--drug-name', required=True, help='药品名称')
    violations_parser.add_argument('--days', type=int, default=7, help='查询天数')

    # export 命令
    export_parser = subparsers.add_parser('export', help='导出数据')
    export_parser.add_argument('--platform', required=True, help='平台名称')
    export_parser.add_argument('--drug-name', required=True, help='药品名称')
    export_parser.add_argument('--output', required=True, help='输出文件路径')
    export_parser.add_argument('--days', type=int, default=7, help='查询天数')

    args = parser.parse_args()

    if args.command == 'init':
        conn = init_db()
        conn.close()
        print(f"✅ 数据库初始化完成: {get_db_path()}")

    elif args.command == 'save':
        with open(args.data, 'r', encoding='utf-8') as f:
            products = json.load(f)
        count = save_products(products, args.platform, args.drug_name,
                            args.spec, args.price)
        print(f"✅ 保存完成: {count} 件商品")

    elif args.command == 'history':
        results = get_history(args.platform, args.drug_name, args.days)
        print(f"历史数据 ({args.days} 天):")
        for row in results:
            print(f"  {row['crawl_date']}: {row['total_products']} 件商品, "
                  f"{row['total_violations']} 违规, {row['total_unauthorized']} 非授权")

    elif args.command == 'trend':
        results = get_trend(args.drug_name)
        print(f"趋势分析 (最近7天):")
        for day in results['daily_data']:
            print(f"  {day['crawl_date']} [{day['platform']}]: "
                  f"{day['total_products']} 件, {day['total_violations']} 违规")

    elif args.command == 'violations':
        results = get_violations(args.platform, args.drug_name, args.days)
        print(f"违规商品 ({args.days} 天):")
        for row in results:
            print(f"  [{row['shop']}] {row['title']}: ¥{row['price']}")

    elif args.command == 'export':
        export_to_json(args.platform, args.drug_name, args.output, args.days)

    else:
        parser.print_help()


if __name__ == '__main__':
    main()


path: scripts/detect_versions.py
content: #!/usr/bin/env python3
"""
非授权版本识别脚本
基于关键词匹配识别非授权商品（海外版、港版等）
"""

import json
import argparse
from typing import List, Dict, Any


# 违规关键词列表
VIOLATION_KEYWORDS = [
    '海外版',
    '港版',
    '美版',
    '欧版',
    '代购',
    '进口',
    '跨境',
    '原装进口',
    '直邮',
    '免税',
    '保税仓',
]


def detect_unauthorized(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """
    检测非授权版本商品

    Args:
        data: 商品数据列表

    Returns:
        非授权商品列表
    """
    unauthorized = []

    for item in data:
        title = item.get('title', '').lower()

        # 检查是否命中违规关键词
        for keyword in VIOLATION_KEYWORDS:
            if keyword.lower() in title:
                unauthorized.append({
                    'platform': item.get('platform', ''),
                    'shop': item.get('shop', ''),
                    'title': item.get('title', ''),
                    'reason': f'命中关键词: {keyword}',
                    'url': item.get('url', '')
                })
                break

    return unauthorized


def load_data(file_path: str) -> List[Dict[str, Any]]:
    """加载 JSON 数据文件"""
    with open(file_path, 'r', encoding='utf-8') as f:
        return json.load(f)


def save_data(data: List[Dict[str, Any]], file_path: str):
    """保存 JSON 数据文件"""
    with open(file_path, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def main():
    parser = argparse.ArgumentParser(description='非授权版本识别')
    parser.add_argument('--data', required=True, help='原始数据文件 (JSON)')
    parser.add_argument('--output', required=True, help='输出文件 (JSON)')

    args = parser.parse_args()

    # 加载数据
    data = load_data(args.data)

    # 检测非授权商品
    unauthorized = detect_unauthorized(data)

    # 保存结果
    save_data(unauthorized, args.output)

    print(f"✅ 检测完成")
    print(f"📊 总商品数: {len(data)}")
    print(f"⚠️ 疑似非授权商品数: {len(unauthorized)}")
    print(f"💾 结果已保存到: {args.output}")


if __name__ == '__main__':
    main()


path: scripts/generate_report.py
content: #!/usr/bin/env python3
"""
Markdown 报告生成脚本
汇总分析结果并生成结构化报告
"""

import json
import argparse
from datetime import datetime
from typing import List, Dict, Any
from collections import Counter, defaultdict


def load_data(file_path: str) -> List[Dict[str, Any]]:
    """加载 JSON 数据文件"""
    with open(file_path, 'r', encoding='utf-8') as f:
        return json.load(f)


def generate_summary(violations: List[Dict], unauthorized: List[Dict],
                     all_data: List[Dict]) -> Dict[str, Any]:
    """生成汇总统计数据"""
    # 平台分布
    violation_by_platform = Counter([v['platform'] for v in violations])
    unauthorized_by_platform = Counter([u['platform'] for u in unauthorized])

    # 总商品数
    total_by_platform = Counter([d['platform'] for d in all_data])

    return {
        'total_violations': len(violations),
        'total_unauthorized': len(unauthorized),
        'total_products': len(all_data),
        'violation_by_platform': dict(violation_by_platform),
        'unauthorized_by_platform': dict(unauthorized_by_platform),
        'total_by_platform': dict(total_by_platform),
    }


def generate_markdown(summary: Dict, violations: List[Dict], unauthorized: List[Dict],
                      drug_name: str, spec: str, standard_price: float, platforms: List[str]) -> str:
    """生成 Markdown 报告"""

    md = f"""# 药品电商价格监控报告

**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

---

## 一、目标与结论

### 调研目标
- **药品名称**: {drug_name}
- **规格**: {spec or '未指定'}
- **标准价格**: {standard_price} 元
- **监控平台**: {', '.join(platforms)}

### 核心结论
- ✅ 低价违规店铺：**{summary['total_violations']} 家**
- 📍 平台分布：{', '.join([f'{k} {v} 家' for k, v in summary['violation_by_platform'].items()]) or '无'}
- ⚠️ 疑似非授权商品：**{summary['total_unauthorized']} 件**

---

## 二、总体情况

| 平台   | 商品数量 | 违规数量 | 非授权数量 |
|--------|----------|----------|------------|
"""

    # 平台汇总表
    all_platforms = set(summary['total_by_platform'].keys()) | set(platforms)
    for platform in sorted(all_platforms):
        total = summary['total_by_platform'].get(platform, 0)
        violation = summary['violation_by_platform'].get(platform, 0)
        unauthorized = summary['unauthorized_by_platform'].get(platform, 0)
        md += f"| {platform}   | {total}        | {violation}        | {unauthorized}          |\n"

    md += f"| **总计** | **{summary['total_products']}**  | **{summary['total_violations']}**    | **{summary['total_unauthorized']}**      |\n"

    md += "\n---\n\n"

    # 问题分析
    md += "## 三、问题分析\n\n"

    # 价格违规情况
    md += "### 价格违规情况\n\n"
    if summary['total_violations'] > 0:
        violation_rate = round(summary['total_violations'] / summary['total_products'] * 100, 1)
        md += f"- 违规率：**{violation_rate}%**（{summary['total_violations']}/{summary['total_products']}）\n"
        md += f"- 主要平台：{', '.join([f'{k}（{v} 家）' for k, v in summary['violation_by_platform'].items()])}\n"
    else:
        md += "- ✅ 无价格违规商品\n"

    md += "\n"

    # 非授权商品情况
    md += "### 非授权商品\n\n"
    if summary['total_unauthorized'] > 0:
        md += f"- 疑似非授权商品：**{summary['total_unauthorized']} 件**\n"
        md += f"- 主要平台：{', '.join([f'{k}（{v} 件）' for k, v in summary['unauthorized_by_platform'].items()])}\n"
    else:
        md += "- ✅ 无疑似非授权商品\n"

    md += "\n---\n\n"

    # 详细数据 - 违规商品清单
    if violations:
        md += "## 四、详细数据\n\n"

        md += "### 违规商品清单\n\n"
        md += "| 平台   | 店铺名称 | 商品名称 | 实际售价 | 标准价格 | 差价 | 链接 |\n"
        md += "|--------|----------|----------|----------|----------|------|------|\n"

        for v in violations:
            md += f"| {v['platform']}   | {v['shop']} | {v['title']} | {v['actual_price']} | {v['standard_price']} | {v['diff']} | [{v['url']}]({v['url']}) |\n"

        md += "\n---\n\n"

    # 非授权商品清单
    if unauthorized:
        md += "### 非授权商品清单\n\n"
        md += "| 平台   | 店铺名称 | 商品标题 | 判定依据 | 链接 |\n"
        md += "|--------|----------|----------|----------|------|\n"

        for u in unauthorized:
            md += f"| {u['platform']}   | {u['shop']} | {u['title']} | {u['reason']} | [{u['url']}]({u['url']}) |\n"

        md += "\n---\n\n"

    md += "---\n\n"
    md += "*本报告由 OpenClaw 药品电商价格监控系统自动生成*\n"

    return md


def save_report(markdown: str, file_path: str):
    """保存 Markdown 报告"""
    with open(file_path, 'w', encoding='utf-8') as f:
        f.write(markdown)


def main():
    parser = argparse.ArgumentParser(description='生成 Markdown 报告')
    parser.add_argument('--violations', help='违规商品数据文件 (JSON)')
    parser.add_argument('--unauthorized', help='非授权商品数据文件 (JSON)')
    parser.add_argument('--all-data', help='原始商品数据文件 (JSON)')
    parser.add_argument('--output', required=True, help='输出报告文件 (Markdown)')
    parser.add_argument('--drug-name', default='未知药品', help='药品名称')
    parser.add_argument('--spec', default='', help='药品规格')
    parser.add_argument('--price', type=float, required=True, help='标准价格')
    parser.add_argument('--platforms', nargs='+', default=['京东', '淘宝', '拼多多'],
                        help='监控平台列表')

    args = parser.parse_args()

    # 加载数据
    violations = load_data(args.violations) if args.violations else []
    unauthorized = load_data(args.unauthorized) if args.unauthorized else []
    all_data = load_data(args.all_data) if args.all_data else []

    # 生成汇总统计
    summary = generate_summary(violations, unauthorized, all_data)

    # 生成 Markdown 报告
    markdown = generate_markdown(summary, violations, unauthorized,
                                  args.drug_name, args.spec, args.price, args.platforms)

    # 保存报告
    save_report(markdown, args.output)

    print(f"✅ 报告生成完成")
    print(f"📊 违规商品数: {summary['total_violations']}")
    print(f"⚠️ 非授权商品数: {summary['total_unauthorized']}")
    print(f"💾 报告已保存到: {args.output}")


if __name__ == '__main__':
    main()


path: scripts/jd_scraper.py
content: #!/usr/bin/env python3
"""
京东爬虫 - 验证版 (2026-04-17)
关键修复（已通过实测）：
1. 使用已登录Cookie突破第1页限制
2. 点击数字分页按钮（不是"下一页"链接）
3. 用wait_until='load'+充分等待替代networkidle
4. 新选择器[data-sku]替代旧的.gl-item
"""
import argparse, json, time, random, os
from playwright.sync_api import sync_playwright

DEFAULT_COOKIES = '/tmp/jd_cookies.json'

def scrape_jd(keyword, max_pages=5, output='/tmp/jd_results.json', 
              headless=True, cookies_file=DEFAULT_COOKIES):
    """
    抓取京东搜索结果
    """
    results = []
    
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=headless,
            args=['--disable-blink-features=AutomationControlled']
        )
        ctx = browser.new_context(
            viewport={'width': 1920, 'height': 1080},
            locale='zh-CN',
            timezone_id='Asia/Shanghai',
            user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
        )
        
        # 加载JD cookies（只加载jd.com域名，避免其他cookie干扰）
        if os.path.exists(cookies_file):
            with open(cookies_file, 'r') as f:
                all_cookies = json.load(f)
            jd_cookies = [c for c in all_cookies if '.jd.com' in c.get('domain', '')]
            ctx.add_cookies(jd_cookies)
            print(f"✅ 已加载 {len(jd_cookies)} 个JD cookies")
        else:
            print(f"⚠️ Cookie文件不存在: {cookies_file}")
        
        page = ctx.new_page()
        page.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined});")
        
        search_url = f"https://search.jd.com/Search?keyword={keyword}&enc=utf-8"
        print(f"🔍 搜索: {keyword}")
        
        for page_num in range(1, max_pages + 1):
            url = f"{search_url}&page={page_num*2-1}" if page_num > 1 else search_url
            print(f"📄 第 {page_num}/{max_pages} 页 (page={page_num*2-1 if page_num > 1 else 1})...")
            
            # 京东有持续追踪请求，networkidle永远不会完成
            # 改用load+充分等待
            try:
                page.goto(url, wait_until='networkidle', timeout=15000)
            except:
                try:
                    page.goto(url, wait_until='load', timeout=20000)
                except:
                    page.goto(url, wait_until='domcontentloaded', timeout=20000)
            
            # 关键：充分等待JS渲染（京东页面复杂，需要10秒以上）
            time.sleep(12)
            
            items = page.query_selector_all('[data-sku]')
            print(f"   找到 {len(items)} 个商品")
            
            if len(items) == 0:
                print(f"   ⚠️ 无商品，停止抓取")
                break
            
            for item in items:
                try:
                    title_el = item.query_selector('[class*="title"]')
                    price_el = item.query_selector('[class*="price"]')
                    shop_el = item.query_selector('[class*="name"]')
                    
                    title = title_el.inner_text().strip() if title_el else ''
                    price = price_el.inner_text().strip().replace('\n', '').replace(' ', '') if price_el else ''
                    shop = shop_el.inner_text().strip() if shop_el else ''
                    
                    if title:
                        results.append({
                            'title': title,
                            'price': price,
                            'shop': shop,
                            'platform': '京东'
                        })
                except:
                    continue
            
            # 点击下一页数字按钮（不是"下一页"链接）
            if page_num < max_pages:
                clicked = False
                pagers = page.query_selector_all('[class*="pagination"]')
                
                for pager in pagers:
                    pager_text = pager.inner_text().strip()
                    if '下一页' in pager_text:
                        for child in pager.query_selector_all('*'):
                            try:
                                txt = child.inner_text().strip()
                                if txt.isdigit() and int(txt) == page_num + 1:
                                    child.click()
                                    clicked = True
                                    print(f"   -> 点击第{page_num+1}页按钮")
                                    time.sleep(8)  # 等待AJAX加载
                                    break
                            except:
                                continue
                        break
                
                if not clicked:
                    print(f"   -> 找不到第{page_num+1}页按钮，停止")
                    break
            
            time.sleep(random.uniform(2, 4))
        
        browser.close()
    
    # 去重（按 title前30字符 + price）
    seen = set()
    unique = []
    for p2 in results:
        key = p2['title'][:30] + p2['price']
        if key not in seen:
            seen.add(key)
            unique.append(p2)
    
    print(f"\n✅ 共获取 {len(unique)} 个唯一商品（共抓取 {len(results)} 条）")
    
    if output:
        with open(output, 'w', encoding='utf-8') as f:
            json.dump(unique, f, ensure_ascii=False, indent=2)
        print(f"💾 已保存到 {output}")
    
    return unique


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='京东商品爬虫')
    parser.add_argument('--keyword', '-k', required=True, help='搜索关键词')
    parser.add_argument('--max-pages', '-m', type=int, default=5, help='最大页数（默认5）')
    parser.add_argument('--output', '-o', default='/tmp/jd_results.json', help='输出文件')
    parser.add_argument('--cookies', '-c', default=DEFAULT_COOKIES, help='Cookie文件路径')
    parser.add_argument('--visible', action='store_true', help='显示浏览器窗口')
    
    args = parser.parse_args()
    
    scrape_jd(
        keyword=args.keyword,
        max_pages=args.max_pages,
        output=args.output,
        headless=not args.visible,
        cookies_file=args.cookies
    )


path: references/report-template.md
content: # 报告模板说明

本文档说明药品电商价格监控报告的结构、字段和生成规范。

---

## 报告结构

报告分为五个主要部分：

1. **目标与结论** - 监控目标和核心发现
2. **总体情况** - 各平台数据汇总
3. **问题分析** - 违规情况详细分析
4. **详细数据** - 违规/非授权商品明细表
5. **趋势分析**（可选）- 多期数据对比

---

## 字段说明

### 1. 目标与结论

#### 调研目标字段
- **药品名称** (string) - 被监控的药品全名
- **规格** (string, optional) - 药品规格（如："0.25g*24粒"）
- **标准价格** (float) - 公司规定的统一零售价（单位：元）
- **监控平台** (array) - 监控的电商平台列表

#### 核心结论字段
- **低价违规店铺数量** (int) - 低于标准价的店铺总数
- **平台分布** (object) - 各平台违规店铺数量
  ```json
  {
    "京东": 2,
    "淘宝": 1,
    "拼多多": 0
  }
  ```
- **疑似非授权商品数量** (int) - 疑似非授权商品总数

---

### 2. 总体情况

#### 汇总表字段

| 字段 | 类型 | 说明 |
|------|------|------|
| 平台 | string | 电商平台名称 |
| 商品数量 | int | 该平台抓取的商品总数 |
| 违规数量 | int | 低于标准价的商品数量 |
| 非授权数量 | int | 疑似非授权商品数量 |

---

### 3. 问题分析

#### 价格违规情况字段
- **违规率** (float) - 违规商品占比（百分比）
- **主要平台分布** (array) - 违规商品较多的平台列表

#### 非授权商品字段
- **疑似非授权商品数量** (int)
- **主要平台分布** (array) - 非授权商品较多的平台列表

---

### 4. 详细数据

#### 违规商品清单字段

| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| 平台 | string | 电商平台 | 京东 |
| 店铺名称 | string | 销售店铺名称 | 某某大药房旗舰店 |
| 商品名称 | string | 商品完整标题 | 阿莫西林胶囊 0.25g*24粒 |
| 实际售价 | float | 抓取到的商品售价（元） | 28.5 |
| 标准价格 | float | 公司规定的标准价（元） | 33.0 |
| 差价 | float | 实际售价与标准价的差额 | -4.5 |
| 链接 | string | 商品详情页链接 | https://item.jd.com/xxx |

#### 非授权商品清单字段

| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| 平台 | string | 电商平台 | 淘宝 |
| 店铺名称 | string | 销售店铺名称 | 某某代购 |
| 商品标题 | string | 商品完整标题 | 阿莫西林胶囊（港版） |
| 判定依据 | string | 命中的关键词 | 命中关键词: 港版 |
| 链接 | string | 商品详情页链接 | https://item.taobao.com/xxx |

---

### 5. 趋势分析（可选）

#### 趋势表字段

| 字段 | 类型 | 说明 |
|------|------|------|
| 指标 | string | 监控指标名称（如：违规店铺数量） |
| 本期 | int | 本次监控数据 |
| 上期 | int | 上次监控数据 |
| 变化 | string | 变化趋势（如："-2 ⬇️"） |

---

## 生成规范

### 命名规范

报告文件名格式：`{药品名称}_价格监控_{日期}.md`

示例：
- `阿莫西林胶囊_价格监控_2026-04-17.md`
- `阿莫西林胶囊_价格监控_2026-04-17_0937.md`

### 数据格式

1. **价格字段**：保留 1-2 位小数
2. **百分比**：保留 1 位小数
3. **日期时间**：格式为 `YYYY-MM-DD HH:MM:SS`

### 排序规则

1. 违规商品清单：按差价从大到小排序
2. 非授权商品清单：按平台分组，组内按店铺名称排序

---

## 报告示例

完整报告示例见 `scripts/generate_report.py` 脚本输出。

---

## 自定义扩展

### 添加新字段

如需在报告中添加自定义字段，修改 `scripts/generate_report.py`：

```python
# 1. 在 generate_summary 函数中添加新统计
summary['custom_field'] = custom_value

# 2. 在 generate_markdown 函数中添加新章节
md += "## 自定义章节\n\n"
md += "| 字段 | 值 |\n"
md += "|------|-----|\n"
md += f"| 自定义字段 | {summary['custom_field']} |\n\n"
```

### 调整报告格式

如需修改报告样式或结构，直接编辑 `scripts/generate_report.py` 中的 Markdown 模板。

---

## JSON 数据格式

如需输出 JSON 格式报告（用于程序处理），可修改 `generate_report.py` 或创建新脚本：

```python
def generate_json_report(summary, violations, unauthorized) -> dict:
    """生成 JSON 格式报告"""
    return {
        'generated_at': datetime.now().isoformat(),
        'summary': summary,
        'violations': violations,
        'unauthorized': unauthorized
    }

# 保存 JSON
with open('report.json', 'w', encoding='utf-8') as f:
    json.dump(json_report, f, ensure_ascii=False, indent=2)
```


path: references/violation-keywords.md
content: # 非授权版本识别关键词列表

## 核心关键词

以下关键词用于识别非授权版本商品（如海外版、港版、代购等）。

---

## 默认关键词列表

### 地区版本类
- 海外版
- 港版
- 美版
- 欧版
- 日版
- 韩版
- 英版
- 德版
- 法版
- 澳版

### 购买渠道类
- 代购
- 代下单
- 代买
- 海淘
- 直邮
- 转运
- 跨境
- 跨境电商

### 进口相关类
- 进口
- 原装进口
- 纯进口
- 正品进口
- 保税仓
- 免税
- 自贸区
- 免税店

### 特殊渠道类
- 水货
- 平行进口
- 专柜代购
- 柜台代购
- 专柜货
- 门店代购

---

## 自定义扩展

如需添加新的关键词，修改 `scripts/detect_versions.py` 文件：

```python
VIOLATION_KEYWORDS = [
    # 原有关键词
    '海外版',
    '港版',
    # ...

    # 添加新关键词
    '你的新关键词1',
    '你的新关键词2',
]
```

---

## 关键词匹配规则

### 匹配逻辑
1. **不区分大小写**：关键词匹配时忽略大小写
2. **部分匹配**：商品标题中包含关键词即判定为命中
3. **多关键词**：一个商品可能命中多个关键词，但只记录第一个匹配

### 排除规则（可选）
如需排除某些特殊情况，可在脚本中添加排除逻辑：

```python
# 排除规则示例
EXCLUDED_PATTERNS = [
    '官方进口',
    '旗舰店',  # 官方旗舰店排除
]

def is_excluded(title: str) -> bool:
    """检查是否需要排除"""
    for pattern in EXCLUDED_PATTERNS:
        if pattern.lower() in title.lower():
            return True
    return False

# 在主逻辑中使用
if keyword in title and not is_excluded(title):
    # 标记为非授权
```

---

## 命中示例

### 命中案例
- ✅ "阿莫西林胶囊（海外版）" → 命中"海外版"
- ✅ "香港代购阿莫西林" → 命中"代购"
- ✅ "保税仓发货阿莫西林" → 命中"保税仓"
- ✅ "原装进口阿莫西林" → 命中"进口"

### 未命中案例
- ❌ "阿莫西林胶囊（国产）" → 未命中
- ❌ "官方正品阿莫西林" → 未命中

---

## 注意事项

1. **关键词更新**：根据实际监控情况，定期更新关键词列表
2. **误判处理**：如发现误判，可通过排除规则或人工审核机制处理
3. **关键词分类**：可按地区、渠道等维度分类管理，便于后续分析

---

## 扩展功能（可选）

### 1. 按关键词分类统计

```python
from collections import defaultdict

keyword_stats = defaultdict(int)

for item in unauthorized:
    for keyword in VIOLATION_KEYWORDS:
        if keyword.lower() in item['title'].lower():
            keyword_stats[keyword] += 1
            break

# 输出统计结果
print("非授权类型分布：")
for keyword, count in keyword_stats.items():
    print(f"  {keyword}: {count} 件")
```

### 2. 多关键词匹配

如需记录所有匹配的关键词（不只是第一个），修改检测逻辑：

```python
def detect_unauthorized_multi(data: List[Dict]) -> List[Dict]:
    """检测非授权版本，记录所有匹配关键词"""
    unauthorized = []

    for item in data:
        title = item.get('title', '').lower()
        matched_keywords = []

        for keyword in VIOLATION_KEYWORDS:
            if keyword.lower() in title:
                matched_keywords.append(keyword)

        if matched_keywords:
            unauthorized.append({
                'platform': item.get('platform', ''),
                'shop': item.get('shop', ''),
                'title': item.get('title', ''),
                'reason': f'命中关键词: {", ".join(matched_keywords)}',
                'url': item.get('url', '')
            })

    return unauthorized
```


path: references/browser-workflow.md
content: # OpenClaw Browser 工具使用指南

## 工具概览

OpenClaw `browser` 工具提供真实 Chromium 浏览器控制能力，支持：
- 页面导航与加载
- 快照与页面结构提取
- 元素交互（点击、输入、滚动）
- 处理动态 JavaScript 内容
- 登录与 session 保留

---

## 基础操作

### 1. 启动浏览器

```bash
# 默认配置启动
browser action:start

# 指定 profile（user 为用户浏览器，需浏览器已运行）
browser action:start profile:user
```

### 2. 打开页面

```bash
# 打开 URL
browser action:open url:"https://search.jd.com/Search?keyword=阿莫西林"
```

### 3. 获取页面快照

```bash
# 使用 role 标签（默认）
browser action:snapshot

# 使用 ARIA 标签（推荐，更稳定）
browser action:snapshot refs:aria
```

**快照返回内容：**
- 页面标题
- URL
- 页面元素列表（带 role、name、selector 等）
- 可交互元素

### 4. 元素交互

```bash
# 点击元素
browser action:act ref:<element_ref> kind:click

# 输入文本
browser action:act ref:<element_ref> kind:type text:"阿莫西林胶囊"

# 滚动页面
browser action:act kind:press key:PageDown
```

---

## 京东商品抓取流程

### 步骤 1：打开搜索页面

```bash
browser action:open url:"https://search.jd.com/Search?keyword=阿莫西林胶囊 0.25g*24粒"
```

### 步骤 2：等待页面加载

```bash
# 等待几秒确保 JavaScript 执行完成
# 或使用 action:act kind:wait timeMs:3000

browser action:snapshot refs:aria
```

### 步骤 3：定位商品列表

从 snapshot 中查找：
- `role="list"` 或 `role="listitem"` 元素
- 包含商品卡片的容器

**典型 ARIA 结构：**
```json
{
  "role": "listitem",
  "name": "阿莫西林胶囊 0.25g*24粒",
  "elements": [
    {
      "role": "text",
      "name": "¥28.50"
    },
    {
      "role": "link",
      "name": "某某大药房旗舰店",
      "selector": ".shop-name"
    },
    {
      "role": "link",
      "name": "查看详情",
      "url": "https://item.jd.com/12345678.html"
    }
  ]
}
```

### 步骤 4：提取商品数据

**提取字段：**
- 商品标题：`name` 属性
- 店铺名称：店铺链接的 `name` 属性
- 价格：价格文本元素（去掉 "¥" 符号）
- 商品链接：`url` 属性

**代码示例（Python 解析）：**
```python
import json

# 假设 snapshot_data 是 browser action:snapshot 返回的 JSON
data = json.loads(snapshot_data)

# 遍历商品列表
for item in data['elements']:
    if item.get('role') == 'listitem':
        # 提取商品信息
        title = item.get('name', '')

        # 提取价格
        price = None
        for elem in item.get('elements', []):
            if '¥' in elem.get('name', ''):
                price = float(elem['name'].replace('¥', ''))
                break

        # 提取店铺
        shop = None
        for elem in item.get('elements', []):
            if elem.get('role') == 'link' and '店' in elem.get('name', ''):
                shop = elem['name']
                break

        # 提取链接
        url = None
        for elem in item.get('elements', []):
            if elem.get('role') == 'link' and 'item.jd.com' in elem.get('url', ''):
                url = elem['url']
                break

        if title and price:
            products.append({
                'title': title,
                'price': price,
                'shop': shop,
                'url': url,
                'platform': '京东'
            })
```

### 步骤 5：翻页（必须执行，确保数据完整）

京东翻页机制：底部有 "上一页 | 1 2 3 4 5 ... 下一页 | 输入框+跳转"

```bash
# 循环翻页，抓取全部数据
while True:
    # 抓取当前页数据
    browser action:snapshot refs:aria
    extract_products()
    
    # 检查是否最后一页
    total_pages = extract_total_pages()  # 从"共100页"中提取
    if current_page >= total_pages:
        break
    
    # 点击"下一页"（使用selector，最稳定）
    browser action:act selector:".pn-next a" kind:click
    browser action:act kind:wait timeMs:3000
    current_page += 1
```

**翻页选择器参考：**
- 下一页：`.pn-next a`
- 上一页：`.pn-prev`
- 页码数字：`.pn-num a`
- 输入框：`.p-skip input`
- 跳转按钮：`.p-skip button`

---

## 淘宝商品抓取流程

### 步骤 1：打开搜索页面

```bash
browser action:open url:"https://s.taobao.com/search?q=阿莫西林胶囊 0.25g*24粒"
```

### 步骤 2：处理登录（如需要）

淘宝可能需要登录，遇到登录提示时：
- 用户手动登录
- 重新 snapshot 获取登录后的页面

```bash
# 登录后重新快照
browser action:snapshot refs:aria
```

### 步骤 3：提取商品数据

淘宝的 ARIA 结构与京东类似，提取字段相同：
- 商品标题
- 店铺名称
- 价格
- 商品链接

### 步骤 4：翻页（必须执行，确保数据完整）

淘宝翻页机制：底部有 "上一页 | 下一页 | 1/100" 格式

```bash
# 循环翻页，抓取全部数据
while True:
    # 抓取当前页数据
    browser action:snapshot refs:aria
    extract_products()
    
    # 检查"下一页"按钮是否禁用（最后一页时禁用）
    browser action:snapshot refs:aria
    if is_next_button_disabled(snapshot):
        break
    
    # 点击"下一页"（使用selector）
    browser action:act selector:".next" kind:click
    browser action:act kind:wait timeMs:3000
    current_page += 1
```

**翻页选择器参考：**
- 下一页：`.next`
- 上一页：`.prev`
- 页码显示：`.page-num`（格式：1/100）

**检测最后一页：**
- 方法1：检查 `.next` 元素是否包含 `disabled` 类
- 方法2：从页码显示提取当前页和总页数，判断是否到达最后一页

---

## 拼多多商品抓取流程

### 步骤 1：打开搜索页面

```bash
browser action:open url:"https://mobile.yangkeduo.com/search_result.html?search_key=阿莫西林胶囊"
```

### 步骤 2：提取商品数据

拼多多的 ARIA结构可能不同，需要通过 snapshot 实际分析后调整提取逻辑。

---

## 常见问题处理

### 1. 验证码处理

遇到滑块验证码时：
- 用户手动完成验证
- 重新 snapshot 获取验证后的页面

```bash
browser action:snapshot refs:aria
```

### 2. 页面加载慢

```bash
# 使用 wait 等待特定元素出现
browser action:act kind:wait textGone:"加载中"
```

### 3. 动态加载内容

```bash
# 滚动到底部触发加载
browser action:act kind:press key:End

# 等待内容加载
browser action:act kind:wait timeMs:2000
browser action:snapshot refs:aria
```

### 4. 封控处理

如遇到封控：
- 等待 5-10 分钟后重试
- 切换 IP（如有条件）
- 使用账号登录后再抓取

---

## 最佳实践

1. **使用 ARIA 标签**：`refs:aria` 更稳定，推荐使用
2. **等待页面加载**：重要页面加载完成后再 snapshot
3. **错误重试**：网络问题或加载失败时重试 1-2 次
4. **避免高频请求**：每个平台间隔 30-60 秒，防止封控
5. **保存 session**：如需登录，使用 `profile:user` 保留 cookies

---

## 完整工作流示例

```bash
# 1. 启动浏览器
browser action:start

# 2. 打开京东搜索页
browser action:open url:"https://search.jd.com/Search?keyword=阿莫西林"

# 3. 等待加载
browser action:snapshot refs:aria

# 4. 提取数据（解析 snapshot 返回的 JSON）
# [Python 脚本处理]

# 5. 翻页（如需要）
browser action:act ref:<下一页> kind:click
browser action:snapshot refs:aria

# 6. 切换平台，重复步骤 2-5
browser action:open url:"https://s.taobao.com/search?q=阿莫西林"
browser action:snapshot refs:aria

# 7. 关闭浏览器
browser action:stop
```


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)