RPA自动化进阶:我开发了一套店群管理系统,彻底解决100+店铺并发卡死痛点

RPA自动化进阶:我开发了一套店群管理系统,彻底解决100+店铺并发卡死痛点

写在最前面:

  1. 禁止说“这不就是多开几个浏览器”。你多开30个Chrome试试,不蓝屏算我输。
  2. 本文所有坑都是我拿命换的,看到就是赚到。
  3. 店群老板们,如果你还在让运营手动切号,建议直接把这篇文章转给技术。

两年前的夏天,我被一个做TEMU店群的朋友拉去“参观”他的工作室。

两排电脑,六个运营,每人面前贴着密密麻麻的便利贴:账号、密码、代理IP、Cookie过期时间。

整个屋子像极了达美乐后厨——每个人都低着头,机械地重复“登录-截图-退出-清缓存-换代理-登录”。

我站了半小时,一个姑娘连头都没抬过。

朋友叹气:林焱,你看到了,这就是我的日常。一百多个店铺,人肉运维,每个月工资发出去五六万,利润全被吃掉了。买了影刀脚本,十来个号一起跑就卡死;上了指纹浏览器,隔离还行,但并发一高电脑直接崩。

他说这话时,隔壁工位的电脑刚好蓝屏了。

我当天晚上回去就开始写代码。

半年后,Alien 1.0 跑在他那台32核工作站上,22个浏览器窗口整整齐齐,一百多个店铺的任务并发调度,CPU稳稳压在70%。

picture.image

picture.image 那个曾经蓝屏的工位,现在放着一盆绿萝。


一、店群“并发卡死”的本质

picture.image

很多人以为卡死是因为电脑配置不够。

不对。

我们做过测试:同样开22个Chrome窗口,手动打开,每个窗口随便逛逛,机器很流畅。

但一旦用影刀脚本自动化操作,半小时后就开始卡。

picture.image

为什么?

因为脚本不会“节制”。

它不管当前CPU是不是已经满了,不管内存是不是在疯狂分配,不管磁盘IO是不是在排队。

它只管一件事:拿到一个店铺,就开一个浏览器,执行任务,然后——不退出。

picture.image

picture.image 几十个Chrome进程同时跑,每个都在读写自己的缓存目录,再加上影刀本身的日志和截图,磁盘队列长度直接飙到几十。

这才是卡死的真相:不是硬件不够,是资源调度没有章法

所以我的破局思路很简单:

不要用影刀去管理浏览器实例。影刀只负责“用手操作”,Python负责“用脑子调度”。

调度器控制同时运行的窗口数量,超出就排队。任务执行完,浏览器不急着关,复用给下一个任务。空闲太久才回收,顺便清理缓存。

就这么一套机制,把“并发卡死”变成了“并发稳如老狗”。

下面我拆开讲Alien的核心模块。


二、环境隔离矩阵:Alien的“环境管理中心”

2.1 界面设计:让老板一眼看懂

Alien的“环境管理中心”不是给极客用的,是给每天切号切到手抽筋的运营用的。

所以界面必须像Excel一样直观。

左侧是一棵分组树。根节点是平台(拼多多/TEMU/TikTok),下面可以建多级子分组,比如“TEMU → 美区 → 服装类目”。

右侧是一个表格,每一行是一个店铺环境。列包括:

  • 店铺ID(可编辑别名)
  • 代理IP(带在线检测图标)
  • 环境状态(🟢在线 / 🟡需重新登录 / 🔴异常)
  • 最后操作时间
  • 健康分(基于失败率自动计算)
  • 操作按钮(打开、编辑、运行、删除)

顶部四个大按钮:导入CSV新建分组批量打开导出报表

导入CSV模板只有三列:shop_id, platform, proxy。其他指纹参数(分辨率、时区、语言)系统自动生成,生成规则用店铺ID做哈希种子——保证同一店铺每次指纹不变,不同店铺差异足够大。

“批量打开”是运营最爱。选中10个店铺,点一下,系统依次启动10个独立的Chrome窗口,每个窗口已经配好代理和指纹。她们可以像操作普通浏览器一样进去处理订单、改价格,不用记任何账号密码。

2.2 技术实现:独立Profile + 指纹生成

环境隔离的核心是Chromium的 --user-data-dir

每个店铺启动时,指定一个完全独立的用户数据目录。这个目录里装着该店铺所有的Cookies、LocalStorage、缓存、插件状态。只要目录不串,店铺就不会串。

但只有目录隔离不够。平台还会采集分辨率、时区、语言、WebGL、Canvas指纹。如果两个店铺这些特征完全一样,仍然容易被关联。

所以我写了一个 EnvironmentManager,为每个店铺生成一套稳定的指纹配置。

import os
import json
import hashlib
import random
from pathlib import Path

class AlienEnv:
    BASE = Path("./AlienData")
    
    def __init__(self, shop_id, platform):
        self.shop_id = shop_id
        self.platform = platform
        self.dir = self.BASE / platform / shop_id
        self.profile = self.dir / "profile"
        self.config = self.dir / "fingerprint.json"
    
    def create(self, proxy, group="default"):
        self.dir.mkdir(parents=True, exist_ok=True)
        self.profile.mkdir(exist_ok=True)
        
        fp = self._gen_fingerprint(proxy, group)
        with open(self.config, "w") as f:
            json.dump(fp, f, indent=2)
        
        # 预建缓存目录
        (self.profile / "Cache").mkdir(exist_ok=True)
        (self.profile / "Local Storage").mkdir(exist_ok=True)
        return str(self.profile)
    
    def _gen_fingerprint(self, proxy, group):
        seed = hashlib.md5(f"{self.platform}:{self.shop_id}:{group}".encode()).hexdigest()[:8]
        rng = random.Random(int(seed, 16))
        
        res_pool = [(1920,1080), (1366,768), (1440,900), (1536,864)]
        tz_map = {
            "pdd": ["Asia/Shanghai"],
            "temu": ["America/New_York", "America/Los_Angeles"],
            "tiktok": ["America/New_York", "Europe/London"]
        }
        lang_map = {
            "pdd": ["zh-CN"],
            "temu": ["en-US", "en-GB"],
            "tiktok": ["en-US", "en-GB"]
        }
        return {
            "proxy": proxy,
            "group": group,
            "screen_w": rng.choice(res_pool)[0],
            "screen_h": rng.choice(res_pool)[1],
            "timezone": rng.choice(tz_map.get(self.platform, ["UTC"])),
            "language": rng.choice(lang_map.get(self.platform, ["en-US"])),
            "platform_os": rng.choice(["Win32", "MacIntel"]),
            "webgl": rng.choice(["Google Inc.", "Intel Inc."]),
            "cores": rng.choice([2,4,8])
        }
    
    def load(self):
        if not self.config.exists():
            return None
        with open(self.config) as f:
            return json.load(f)

启动浏览器时,读取指纹配置,拼装命令行参数,并开启远程调试端口供影刀连接。

def launch(env):
    cfg = env.load()
    cmd = [
        "chrome.exe",
        f"--user-data-dir={env.profile}",
        f"--proxy-server={cfg['proxy']}",
        f"--window-size={cfg['screen_w']},{cfg['screen_h']}",
        f"--lang={cfg['language']}",
        "--remote-debugging-port=0",
        "--disable-blink-features=AutomationControlled"
    ]
    proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    # 从输出解析调试端口...

这样每个店铺就是一个完全独立的“虚拟电脑”。


三、自动化调度编排:22个窗口并发不卡不崩

3.1 拖拽式流程设计

Alien的“自动化编排流”模块,让运营像搭积木一样组合业务流程。

左侧动作库:

  • 店铺控制:打开环境、关闭环境
  • 登录管理:自动登录、刷新Cookie
  • 电商操作:上架商品、领优惠券、同步订单、回复消息
  • 流程控制:等待、循环、条件判断

运营把动作拖到画布上,用箭头串联。

比如一个“TEMU每日养号”流程:

打开环境 → 自动登录 → 浏览首页30秒 → 随机点开3个商品 → 每个停留15秒 → 关闭环境

流程保存后,可以分配给单个店铺、一个分组或所有店铺。

3.2 调度核心:有界并发 + 店铺级互斥

调度器要解决三个问题:

  • 同时运行的浏览器窗口不能超过物理极限(我们设为22)
  • 同一个店铺不能同时执行多个任务(防止数据错乱)
  • 任务结束后浏览器不急着关,复用给下一个任务

下面这段是调度器的核心骨架:

import threading
import queue
import time
from concurrent.futures import ThreadPoolExecutor

class Scheduler:
    def __init__(self, max_concurrent=22):
        self.max_concurrent = max_concurrent
        self.task_q = queue.Queue()
        self.active = 0
        self.active_lock = threading.Lock()
        self.shop_busy = {}
        self.shop_lock = threading.Lock()
        self.pool = ThreadPoolExecutor(max_workers=max_concurrent)
    
    def submit(self, shop_id, flow, params=None):
        self.task_q.put((shop_id, flow, params))
    
    def _worker(self):
        while True:
            shop_id, flow, params = self.task_q.get()
            
            # 等该店铺空闲
            while True:
                with self.shop_lock:
                    if not self.shop_busy.get(shop_id, False):
                        self.shop_busy[shop_id] = True
                        break
                time.sleep(0.5)
            
            # 等有空闲槽位
            while True:
                with self.active_lock:
                    if self.active < self.max_concurrent:
                        self.active += 1
                        break
                time.sleep(0.5)
            
            self.pool.submit(self._run, shop_id, flow, params)
    
    def _run(self, shop_id, flow, params):
        try:
            debug_port = self._get_or_launch_browser(shop_id)
            self._call_yingdao(flow, shop_id, debug_port, params)
        finally:
            with self.active_lock:
                self.active -= 1
            with self.shop_lock:
                self.shop_busy[shop_id] = False
            self.task_q.task_done()
    
    def _get_or_launch_browser(self, shop_id):
        # 检查该店铺是否已有存活浏览器实例,没有则启动
        pass
    
    def _call_yingdao(self, flow, shop_id, port, params):
        import subprocess, json
        cmd = [
            "影刀RPA.exe", "-run", flow,
            "-param", f"shop_id={shop_id}",
            "-param", f"debug_port={port}",
            "-param", f"params={json.dumps(params)}"
        ]
        subprocess.run(cmd, timeout=600, check=True)
    
    def start(self):
        for _ in range(self.max_concurrent):
            threading.Thread(target=self._worker, daemon=True).start()

这个调度器上线后,22个窗口长期满负荷运行,CPU和内存曲线是一条直线,再也没有出现过“跑着跑着卡死”的情况。

踩坑:早期版本没有 shop_busy 互斥锁,结果同一个店铺的两个任务(比如“上架”和“领券”)同时操作浏览器,一个在点“发布”,另一个在点“删除”,把商品删光了。加上锁之后,同一店铺的任务强制串行,问题解决。

3.3 智能平铺

22个窗口如果全部堆叠在左上角,影刀点击时会点错目标。

我在启动每个浏览器后,调用Windows API把窗口平铺到整个屏幕。

def tile_windows(hwnds, cols=5):
    sw = 1920
    sh = 1080
    tw = sw // cols
    rows = (len(hwnds) + cols - 1) // cols
    th = sh // rows
    for i, hwnd in enumerate(hwnds):
        row = i // cols
        col = i % cols
        x = col * tw
        y = row * th
        win32gui.SetWindowPos(hwnd, None, x, y, tw, th, 0x0040)

运营第一次看到22个窗口整整齐齐排列时,说:“这才是自动化该有的样子。”


四、底层工程封装:双击exe就能用

老板和运营不懂Python、不懂ChromeDriver、不懂环境变量。

所以Alien必须是一个双击就能跑的商业软件。

4.1 PyQt6 做的专业界面

我用PyQt6写了完整的GUI,抛弃了命令行黑框框。

  • 仪表盘:用pyqtgraph画实时曲线,显示并发数、任务成功率、内存占用
  • 环境管理:QTreeView分组树 + QTableView表格,支持拖拽、右键菜单、批量操作
  • 流程编排:基于QGraphicsView的画布,节点可拖拽、连线可编辑
  • 日志面板:彩色输出,支持按店铺ID搜索

暗黑主题,圆角卡片,按钮有悬停效果。客户第一次打开说:“这软件看起来就很贵。”

4.2 PyInstaller 打包成单文件

客户不需要安装任何运行环境。

我用PyInstaller把Python解释器、所有依赖库、一个便携版Chromium(约110MB)全部打进一个Alien.exe

首次启动时,程序在%APPDATA%\Alien下解压Chromium,创建数据目录。

客户只需要:下载 → 解压 → 双击exe。

整个交付过程不超过两分钟。

4.3 一机一码安全验证

为了防止软件被随意复制,我做了硬件绑定授权。

程序启动时获取硬盘序列号、MAC地址、主板ID,生成机器码。用户把机器码发给我,我用RSA私钥签名生成license文件。

每次启动验证签名和硬件是否匹配。

虽然不能100%防破解,但已经挡住了99%的“拷贝即用”。


五、血泪踩坑清单

1. 内存泄漏排查了两周。

线上跑了50个店铺,内存从2GB慢慢涨到14GB,然后卡死。查了很久,发现每个浏览器实例退出后,user-data-dir/CacheCode Cache没有被清理。日积月累,一个店铺的Profile能到5GB。后来加了定时清理:空闲超过1小时的店铺,删除其缓存目录,保留Cookies和LocalStorage。

2. 影刀超时导致进程残留。

影刀流程默认等待元素60秒,跨境网络经常超时。超时后我们杀进程,但Chrome没关。几天后系统里残留几十个chrome.exe。解决方案:在任务finally块里调用psutil.Process(pid).kill(),并加了超时后强制清理的兜底逻辑。

3. 代理IP的质量决定生死。

图便宜买过5元/天的静态代理,用了两天被TEMU全部拉黑。后来换成30元/天的住宅代理池,封店率从15%降到0.5%。这个钱不能省。

4. 运营的误删事故。

有一次运营在环境管理里选中“全部店铺”,然后点了“删除”。一瞬间所有环境配置都没了。我从备份恢复,但损失了当天部分任务记录。之后我加了二次确认弹窗和回收站机制——删除的环境先进入回收站,7天后才真正删除。


写在最后

从一个人写Alien到现在,已经两年。

它从最初的几十行脚本,长成了上万行代码的完整系统。

有人问我:你为什么不直接用现成的指纹浏览器加影刀?

我的回答很简单:自己造轮子不是为了证明自己多厉害,而是为了在每一个细节上拥有控制权。

代理切换需要特殊逻辑?改代码。并发数要动态调整?改配置。平台更新了风控策略?加一层指纹伪装。

所有东西都在自己手里,不用等第三方更新,不用看供应商脸色。

如果你也在做店群自动化,希望这篇文章能帮你少踩几个坑。

技术不复杂,复杂的是对细节的死磕。

作者:林焱
独立开发者,RPA架构师
博客:林焱RPA(全网同名)
转载需授权,喷子绕道

(全文约4700字)

0
0
0
0
评论
未登录
暂无评论