Building a Labubu bot s tep-by-step tutorial

没抢到 Labubu 盲盒?这并不奇怪。限量款如今备受粉丝、收藏家和转售商追捧,而线上购买往往是唯一途径。在这种情况下,靠手动操作几乎不可能抢到心仪款式,专用机器人便成了有效的解决方案。本指南将介绍用机器人购买 Labubu 时需要注意的要点,并提供可直接使用的方案。

为什么 Labubu 机器人不可或缺?

Labubu 是一只毛绒小怪兽。自其创作者 Kasing Lung 与 Pop Mart 合作以来,它在 2019 年开始走红。这家中国零售商是其独家制造商,主要以盲盒形式发售 Labubu。盲盒会组成主题系列,其中包括与其他品牌联名的限量款,系列里通常还会有一款未公开的稀有”隐藏款”。此外,Pop Mart 的补货并非固定时间进行,粉丝必须留意规律并持续关注。这掀起了抢购热潮,买玩具也变成了一场争夺。这并非夸张,由于曾发生顾客受伤事件,Pop Mart 已在部分线下门店停售 Labubu 盲盒。线上购买虽然更安全,但同样需要速度和精准度,哪怕误点一个链接这样的小失误,也可能让您与玩具失之交臂。
换个角度看, 

专用机器人能帮您省去不少麻烦:

  • 监控新品补货,无需手动查看是否有库存; 
  • 自动执行购买流程,您只需完成最后的结账;
  • 即时执行操作,速度是抢购 Labubu 的关键;
  • 减少人为失误,例如误点错误按钮。 

使用机器人公平吗?

并不禁止使用机器人,但您应遵守 Pop Mart 的服务条款,且不得从事违法行为。此外,您不应利用机器人获取敏感数据或损害网站。 DataImpulse 鼓励您仅以合乎道德的方式进行抓取,且不对任何非法活动负责。 

那么,让我们为下一次补货做好准备。

使用 Python 构建 Labubu 机器人 

开始编写代码前,先了解决定机器人能否成功的关键因素: 

  • 设计整体逻辑;
  • 处理验证提示、弹窗和基于地理位置的限制;
  • 模拟真实用户行为,例如设置请求之间的时间间隔;
  • 选择辅助工具;
  • 定期检查并更新机器人,因为 Pop Mart 会持续更新网站,您的机器人也应能适应这些变化。 

下面我们将逐步处理这些问题。 

入门

  1. 访问 Python 官方网站,下载最新版本或进行更新。安装时,请务必将 Python 添加到 PATH,以便能在终端中运行它。 
  2. 运行以下命令来安装必要的库:

pip install playwright httpx python-dotenv

      
        
        

这会安装 3 个 Python 软件包:

  • Playwright,用于浏览器自动化;
  • httpx,Python 的 HTTP 客户端,支持 HTTP/1.1 和 HTTP/2、异步请求、代理等;
  • python-dotenv,用于从 .env 文件加载环境变量,适合将密码、令牌等敏感信息存放在代码之外。 
  1. 使用以下命令下载浏览器二进制文件:

playwright install

      
        
      

创建 Telegram 机器人和频道

我们需要一个 Telegram 机器人,以便在新品上架时接收通知。这样您可以照常忙自己的事,也不会错过任何一次补货。 

这里提供了创建机器人的说明,以及获取 API 密钥的方法。 

接着,创建一个公开频道,并将机器人设为管理员。 

开发机器人 

机器人项目的结构大致如下:

labububot/

├─ main.py
├─ runner.py
├─ config.py
├─ scraping.py
├─ telegram_utils.py
├─ state.py
├─ labubu_state.json
├─ .env 

下面说明各个文件的作用。

runner.py 负责协调机器人的运行逻辑。它会按设定的时间间隔持续循环:加载当前状态、收集产品信息、检查库存、发送 Telegram 通知,并将更新保存到 labubu_state.json 文件中。该文件会自动创建。 


import asyncio
from playwright.async_api import async_playwright

from state import load_state, save_state
from scraping import gather_products_once
from telegram_utils import render_message, send_telegram, now_iso
from config import INTERVAL_SECONDS

async def run():
    state = load_state()
    async with async_playwright() as pw:
        print("[START] Labubu stock watcher started.")
        while True:
            try:
                items = await gather_products_once(pw)
                for it in items:
                    url = it["url"]
                    prev = state.get(url, {}).get("in_stock")
                    state[url] = {"in_stock": it["in_stock"], "name": it["name"], "price": it["price"]}
                    if it["in_stock"] and prev is not True:
                        msg = render_message(it)
                        await send_telegram(msg)
                save_state(state)
                print(f"[OK] Cycle finished: {len(items)} item(s). {now_iso()}")
            except Exception as e:
                print(f"[FATAL] {e}")
            await asyncio.sleep(INTERVAL_SECONDS)

      
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
      

config.py 集中管理常量和相关配置。它定义要扫描的 Pop Mart 页面、数据的存储位置,以及从 .env 文件中读取代理详情和 Telegram 机器人 API 密钥等敏感信息。


import os
from pathlib import Path
from dotenv import load_dotenv

SEARCH_PAGES = [
    "https://www.popmart.com/us/search/LABUBU",
    "https://www.popmart.com/us/new-arrivals",
    "https://www.popmart.com/us/collection/11/the-monsters",
]

load_dotenv()

INTERVAL_SECONDS = int(os.getenv("INTERVAL_SECONDS", "120"))

TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")

PROXY_SERVER = os.getenv("PROXY_SERVER")
PROXY_USERNAME = os.getenv("PROXY_USERNAME")
PROXY_PASSWORD = os.getenv("PROXY_PASSWORD")

HEADLESS = os.getenv("HEADLESS", "1") != "0"

STATE_FILE = Path("labubu_state.json")

      
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
      

.env 用于存储敏感数据。将这些信息放在单独文件中而非硬编码到程序里,既更安全,也便于在需要时更改密码等内容。该文件还设置浏览器的运行模式(有头或无头)及抓取间隔。请务必填入您的真实凭据和数据。您可以从所选套餐的标签页复制您的 DataImpulse 凭据 。 


TELEGRAM_BOT_TOKEN=your_token
TELEGRAM_CHAT_ID=@name

INTERVAL_SECONDS=90

PROXY_SERVER=proxy_server
PROXY_USERNAME=proxy_login
PROXY_PASSWORD=proxy_password

HEADLESS=1

      
        
        
        
        
        
        
        
        
        
        
      

scraping.py 是抓取模块的核心。它使用 Playwright 监控指定 Pop Mart 页面的产品库存、提取所需信息,并将结构化数据解析为 .json 文件。  


import re
from urllib.parse import urljoin
from playwright.async_api import async_playwright

from config import SEARCH_PAGES, PROXY_SERVER, PROXY_USERNAME, PROXY_PASSWORD, HEADLESS

async def extract_products_from_search(page) -> list[dict]:
    products, seen = [], set()
    anchors = await page.locator('a[href*="/products/"]').all()
    for a in anchors:
        href = await a.get_attribute("href")
        if not href or "/products/" not in href:
            continue
        url = urljoin(page.url, href)
        if url in seen:
            continue
        name = (await a.text_content() or "").strip()
        if not name or len(name) < 3:
            try:
                name = (await a.locator("..").text_content() or "").strip()
            except Exception:
                pass
        if "labubu" in (name or "").lower() or "labubu" in url.lower() or "the-monsters" in url.lower():
            products.append({"name": name, "url": url})
            seen.add(url)
    return products

async def parse_ld_json(page):
    try:
        content = await page.locator('script[type="application/ld+json"]').nth(0).text_content()
        import json as _json
        data = _json.loads(content)
        if isinstance(data, list):
            for item in data:
                if isinstance(item, dict) and "offers" in item:
                    offers = item["offers"]
                    if isinstance(offers, dict):
                        return {
                            "availability": offers.get("availability"),
                            "price": offers.get("price"),
                            "priceCurrency": offers.get("priceCurrency"),
                        }
        elif isinstance(data, dict) and "offers" in data:
            offers = data["offers"]
            return {
                "availability": offers.get("availability"),
                "price": offers.get("price"),
                "priceCurrency": offers.get("priceCurrency"),
            }
    except Exception:
        pass
    return {}

async def check_product_stock(page, url: str) -> dict:
    await page.goto(url, wait_until="domcontentloaded")
    await page.wait_for_timeout(1200)
    title = (await page.title()) or ""
    title = title.replace(" - POP MART", "").strip()
    meta = await parse_ld_json(page)
    availability = (meta.get("availability") or "") if meta else ""
    price = meta.get("price") if meta else None
    if not price:
        try:
            price_text = await page.locator('[class*="price"], .price, [data-test*="price"]').first.text_content()
            if price_text:
                price_clean = re.sub(r"[^\d\.,]", "", price_text)
                price = price_clean if price_clean else None
        except Exception:
            pass
    in_stock = None
    try:
        if await page.locator("text=ADD TO BAG").count():
            in_stock = True
        elif await page.locator("text=SOLD OUT").count():
            in_stock = False
    except Exception:
        pass
    if in_stock is None and availability:
        in_stock = "InStock" in availability or "instock" in availability.lower()
    if in_stock is None:
        try:
            btn_texts = [t.lower() for t in await page.locator("button, div[role=button]").all_text_contents()]
            joined = " | ".join(btn_texts)
            if "add to bag" in joined or "add to cart" in joined:
                in_stock = True
            elif "sold out" in joined or "out of stock" in joined:
                in_stock = False
        except Exception:
            pass
    return {
        "url": url,
        "name": title or "LABUBU",
        "price": price,
        "in_stock": bool(in_stock),
    }

async def gather_products_once(pw) -> list[dict]:
    browser = await pw.chromium.launch(headless=HEADLESS)
    context_args = {}
    if PROXY_SERVER:
        context_args["proxy"] = {"server": PROXY_SERVER, "username":PROXY_USERNAME, "password":PROXY_PASSWORD}
    context = await browser.new_context(**context_args)
    page = await context.new_page()

    pool = {}
    for url in SEARCH_PAGES:
        try:
            await page.goto(url, wait_until="domcontentloaded")
            await page.wait_for_timeout(1000)
            await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
            await page.wait_for_timeout(800)
            prods = await extract_products_from_search(page)
            for p in prods:
                pool[p["url"]] = p
        except Exception as e:
            print(f"[WARN] Failed to open {url}: {e}")

    results = []
    for url in list(pool.keys()):
        try:
            res = await check_product_stock(page, url)
            results.append(res)
        except Exception as e:
            print(f"[WARN] Failed to check {url}: {e}")

    await context.close()
    await browser.close()
    return results

      
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        
        
        
        
      

telegram_utils.py 负责在目标商品有货时发送 Telegram 通知。 


import html
import httpx
from datetime import datetime, timezone
from config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID

def now_iso() -> str:
    return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")

async def send_telegram(text: str, disable_preview: bool = False) -> None:
    if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
        print("[WARN] TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID not set — message not sent.")
        return
    
    api_url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
    payload = {
        "chat_id": TELEGRAM_CHAT_ID,
        "text": text,
        "parse_mode": "HTML",
        "disable_web_page_preview": disable_preview,
    }
    async with httpx.AsyncClient(timeout=20) as client:
        r = await client.post(api_url, data=payload)
        if r.status_code != 200:
            print(f"[TG ERROR] {r.status_code}: {r.text}")

def render_message(item: dict) -> str:
    title = html.escape(item.get("name") or "Labubu item")
    price = item.get("price")
    price_line = f"\n💵 Price: {price}" if price else ""
    return (
        f"🟢 LABUBU in stock\n"
        f"{title}{price_line}\n"
        f"🔗 {item['url']}\n"
        f"🕒 {now_iso()}\n"
        f"#Labubu #PopMart"
    )

      
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
 
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
      

state.py 负责加载和保存产品状态信息到 labubu_state.json 文件。 


import json
from config import STATE_FILE

def load_state() -> dict:
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text("utf-8"))
        except Exception:
            return {}
    return {}

def save_state(state: dict) -> None:
    STATE_FILE.write_text(
        json.dumps(state, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )

      
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
      

main.py 是整个应用程序的入口。就像汽车的”启动引擎”按钮一样,这段代码会启动机器人。 


import asyncio
from runner import run

if __name__ == "__main__":
    try:
        asyncio.run(run())
    except KeyboardInterrupt:
        print("\n[STOP] Stopped by user.")

      
        
        
        
        
        
        
        
        
      

要运行该文件并启动机器人,请在终端中输入以下命令:


python main.py

      
        
      

让机器人稳定运行的辅助工具 

还可以使用以下工具:

  • 日志框架,用于跟踪错误和监控机器人的整体活动,对调试很有帮助。 
  • 错误跟踪工具,可自动捕获并实时报告错误,帮助您快速解决问题。
  • 监控与告警服务,用于监测机器人性能,并在异常活动或停机时发出警报。 
  • 环境隔离工具,容器可创建隔离环境,从而简化部署。 

在示例代码中,我们还使用 dotenv 保护敏感数据。在攻击日益增多的今天,这一点尤为重要。代理同样关键,因为它们可将流量分配到不同的 IP 地址,帮助您模拟真实用户行为。它们还能让您同时使用多个连接,提高抢到 Labubu 的机会。如需代理支持,DataImpulse 随时为您服务。请发送电子邮件至 [email protected],或点击”立即尝试“按钮开始使用。

Share article: