Python+OpenCV+PyAutoGUI实现游戏交易行自动抢单脚本实战
2026/9/17 14:03:18 网站建设 项目流程

我平时玩三角洲行动,最烦的一件事就是蹲交易行。想低价囤点素材,要么没事就切过去刷新,要么对着价格排序一页一页翻,手都点麻了。后来我干脆花了一个周末,写了一套交易行自动化脚本,挂在后台帮我盯盘、比价、抢单。今天把整套思路和源码级拆解分享出来,从技术选型到落地实操,再到我踩过的坑,一次性讲清楚。

这个项目说白了,就是用 Python 模拟人工操作,自动完成“打开交易行—搜索道具—读取价格—判断是否值得买—点击购买”这一整条链路。它适合三种人:一是想低价扫货的玩家,二是想研究 Windows 桌面自动化的朋友,三是想了解 OpenCV 图像识别和 PyAutoGUI 键鼠模拟怎么配合落地的新手。

1. 整体设计思路:为什么选图像识别+键鼠模拟,而不是走内存或封包

先回答一个很多人会问的问题:都做自动化了,为什么不直接读内存、嗅探封包,或者直接调游戏接口?那样既快又准确,还能绕过界面逻辑。

我一开始确实犹豫过。读完内存能找到物品价格在内存中的地址,截包能拿到服务器下发的价格数据,看起来都是“更正规”的路子。但真往下做,问题就来了:游戏的内存数据有 CRC 校验,改动会被立刻检测,而纯读取虽然风险小,但每次游戏更新都会换偏移量,维护成本高到离谱;封包被加密后,你还要先逆向加密算法,这已经超出“交易行脚本”的范畴,属于外挂开发的领域了。最核心的一点是,这两种方案都已经跨过了脚本的边界,在合规性上站不住脚。

所以我把方案定为“OpenCV 图像识别 + PyAutoGUI 键鼠模拟”。这个组合的本质是:把屏幕当作输入源,把鼠标键盘当作输出手段,模拟一个真人坐在电脑前的所有操作。它不读内存、不碰封包、不注入进程,风险等级低很多,而且游戏怎么更新都不影响,因为识别的是画面,不是数据。

整个系统的运作链路是这样设计的:截屏获取当前屏幕画面,OpenCV 在图中定位交易行的搜索框、价格列表、购买按钮等关键区域;OCR 或模板匹配把图像中的价格数字、物品名转换成文本;Python 判断当前价格是否低于我的心理价位;低于就移动鼠标点击购买。这套流程的工程设计重点在于:每一步都要稳,不能快,快就容易出错,出错就等于白干。

2. 核心工具链与界面识别原理解析

2.1 工具选型:Python 生态三件套

我用的是 Python 3.10,配合三个核心库:PyAutoGUI 负责鼠标键盘控制,OpenCV 负责图像处理,PIL 负责快速截屏。辅助用 numpy 做图像数组运算,用 pyperclip 处理剪贴板内容。

PyAutoGUI 是这套方案里最容易上手也最容易出问题的库。它的鼠标移动、点击、键盘输入全部是模拟系统级事件,不需要管理员权限,但正因为太底层,它对屏幕分辨率极其敏感。我前期测试时,在 2560×1440 的屏幕上定位好坐标,一换到 1920×1080 就全部偏移,后来统一封装了一个坐标换算函数,所有点位都按当前分辨率动态计算,才算彻底解决。

OpenCV 负责的核心任务是模板匹配和边缘识别。模板匹配是这么工作的:我预先截取一张“搜索框”的小图作为模板,然后在整个屏幕截图中滑动匹配,找到相似度最高的位置,返回坐标。这个原理跟拼图找人很像,模板越小、特征越明显,匹配越准、越快。

2.2 图像识别:模板匹配和 OCR 的取舍

交易行界面里,有静态的部分也有动态的部分。静态部分比如搜索框、标签页、按钮图标,用模板匹配就够了。动态部分比如价格数字、物品名称,必须用 OCR。我对比过 Tesseract 和百度 OCR 接口,最终选了 Tesseract 5.0 的英文+数字模型配合 psml 模式,识别价格数字非常稳,中文道具名准确率就差一些,所以我做了一层映射处理。

道具名的识别,我建议别直接上通用 OCR。交易行的物品名背景色统一、字体统一,完全可以走模板匹配。先按稀有度颜色过滤出候选区域,再用每个物品的名称小图去做匹配,这样准确率能达到 99% 以上,速度也快。我的代码里维护了一个“目标物品图片库”,每个物品存一张裁好的名称栏截图,脚本上线后先加载进内存,匹配时逐个对比。

价格数字的识别用 OCR 就够了,因为数字只有 10 个字符,识别率本身不低。需要注意的是,游戏里价格会用逗号分隔,比如 1,234,000,OCR 容易把逗号识别成小数点或直接漏掉,我在后处理里做了强制规则:数字之间不允许出现小数点,只允许出现逗号,逗号后必须跟三位数。

2.3 为什么不做深度学习目标检测

有人问我,现在 YOLO 这么成熟,为什么不训练一个模型直接检测界面元素,精度更高、泛化更好?

这个问题的答案很现实:投入产出比。做一个 YOLO 模型,先要标注几千张交易行界面的截图,然后训练、验证、调参,一套流程下来至少两周。而界面元素就固定那么几个,搜索框、按钮、价格列表,模板匹配已经能 99% 搞定。唯一会变的场景是分辨率不同,但这个我用动态缩放解决了。杀鸡不用牛刀,做自动化脚本的第一原则是:用最简单可靠的技术解决 80% 的问题,剩下 20% 用规则补齐。

3. 实操过程:从截屏到点击购买的完整实现

3.1 项目目录与基础配置

先看我的项目结构:

trade_bot/ ├── main.py # 主逻辑入口 ├── config.py # 所有配置参数 ├── modules/ │ ├── screen.py # 截屏与图像预处理 │ ├── recognize.py # 模板匹配与OCR │ ├── decision.py # 价格判断逻辑 │ ├── action.py # 键鼠模拟操作 │ └── logger.py # 日志记录 ├── templates/ # 界面元素模板图 ├── items/ # 目标物品名称模板图 └── logs/ # 运行日志

config.py 里放的是全局参数,最重要的几个是:目标物品清单,每个物品的心理价位和最大购买数量;扫描间隔时间,默认 8 秒一轮;安全延时范围,鼠标移动和点击之间的随机等待区间。

# config.py 核心配置 ITEMS = { "4级甲修复工具": {"max_price": 45000, "max_buy": 5}, "5级弹药箱": {"max_price": 120000, "max_buy": 2}, } SCAN_INTERVAL = 8 # 每轮扫描间隔(秒) MIN_MOVE_DELAY = 0.2 # 鼠标移动后最小等待 MAX_MOVE_DELAY = 0.5 # 鼠标移动后最大等待 CONFIDENCE_THRESHOLD = 0.85 # 模板匹配置信度阈值

3.2 截屏与图像预处理

截屏我用的是 PIL 的 ImageGrab,它比 PyAutoGUI 自带的截图更快,返回的是 PIL Image 对象,转成 numpy 数组后直接交给 OpenCV 处理。

# modules/screen.py import numpy as np from PIL import ImageGrab import cv2 def grab_screen(region=None): """截取屏幕指定区域,region 为 (x1, y1, x2, y2)""" img = ImageGrab.grab(bbox=region) frame = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) return frame def preprocess(gray_img): """灰度图二值化,增强文字区域对比度""" _, binary = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) return binary

预处理这一步特别关键。游戏界面本身有背景纹理,直接拿彩色图去匹配,干扰因素太多。我先把图片转灰度,再用 Otsu 自适应阈值做二值化,把前景文字和背景彻底分开。这样模板匹配的置信度能从 0.6 直接拉升到 0.9 以上。

烫知识:ImageGrab 在 Windows 上走的是 GDI 接口,部分游戏全屏模式会黑屏,解决方法是把游戏窗口改成无边框窗口化,或者用 DXGI 截图方案。我实测无边框窗口化最省事,对性能影响可以忽略。

3.3 搜索框定位与物品搜索

整体流程的第一步,是找到搜索框并输入物品名。我的实现是先点击游戏内置的交易行标签,唤起搜索界面,然后截一张图,用模板匹配定位搜索框的坐标。

# main.py 中搜索物品的片段 import pyautogui import cv2 def find_template(screen, template_path, threshold=0.85): template = cv2.imread(template_path) result = cv2.matchTemplate(screen, template, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(result) if max_val >= threshold: return max_loc return None def search_item(item_name): screen = grab_screen() pos = find_template(screen, "templates/search_box.png") if pos is None: log("搜索框未找到,重试") return False x, y = pos pyautogui.click(x + 20, y + 10) pyautogui.hotkey("ctrl", "a") pyautogui.write(item_name, interval=0.02) pyautogui.press("enter") time.sleep(1) return True

这段代码有一个细节:click 之后不是马上输入字符,而是先 ctrl+A 全选,再写入新的物品名。这样做是为了防止上一次搜索残留的字符没有被清空。很多新手脚本在这块会翻车,清空输入框用 ctrl+A 比反复按退格键可靠一百倍,速度也更快。

write 函数里的 interval=0.02 是每个字符间隔 20 毫秒,这个参数被很多人忽略,但它的作用是真实模拟人的打字速度。如果 interval 设成 0,系统会瞬间发送一串字符,游戏端的输入框处理逻辑可能来不及响应,轻则丢字符,重则被当作异常操作。

3.4 价格读取与比价决策

搜索结果出来后,交易行会展示一排物品列表,每个条目包含名称、价格、数量等信息。我需要读取第一个可购买物品的价格,也就是列表第一行(通常是价格最低的排列方式)。

# modules/recognize.py import pytesseract def read_price_from_region(screen, region): """从指定区域读取价格数字""" x1, y1, x2, y2 = region roi = screen[y1:y2, x1:x2] gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) scaled = cv2.resize(gray, None, fx=3, fy=3, interpolation=cv2.INTER_CUBIC) _, binary = cv2.threshold(scaled, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) text = pytesseract.image_to_string(binary, config="--psm 7 -c tessedit_char_whitelist=0123456789,") price = parse_price_text(text) return price

这里我做了两件事来提高 OCR 准确性:放大三倍,过滤白名单字符。交易行的价格字体本来就是比较小的像素字体,直接识别很容易把 8 认成 3,把 0 认成 8。放大三倍后笔画特征更明显,白名单限制后只会输出数字和逗号,把干扰项全部干掉了。

parse_price_text 是处理 OCR 原始结果的。OCR 输出的文本经常有换行、空格、识别错误,我需要提取第一个有效的数字串:

import re def parse_price_text(raw_text): """从 OCR 原始文本中提取价格""" cleaned = re.sub(r"[^\d,]", "", raw_text) parts = cleaned.split(",") if len(parts) >= 2 and len(parts[1]) == 3: digits = parts[0] + parts[1] return int(digits) if cleaned: return int(cleaned) return None

拿到价格之后,决策逻辑就简单了:

# modules/decision.py def should_buy(item_name, current_price): if current_price is None: return False config = ITEMS.get(item_name) if not config: return False if current_price <= config["max_price"]: return True return False

比价的逻辑不是简单的当前价低于心理价就买,我建议加一个上下浮动缓冲区。比如心理价是 45000,那么 44999 买,45001 就不买,这个 1 块钱的差距其实毫无意义,但脚本会频繁触发。我会做一个小优化:当前价 <= 心理价 * 1.02 的时候,记录日志但不购买,等到下一轮再看。这样能过滤掉大量价格在临界点波动的噪音。

3.5 模拟点击购买与异常处理

价格确认满足条件后,就是定位购买按钮并点击。购买按钮是固定位置的,我用绝对坐标偏移来定位,因为它是和搜索结果列表联动的,一般在价格下方的固定偏移位置。

def click_buy(price_pos): x, y = price_pos buy_x = x + 180 buy_y = y + 60 pyautogui.moveTo(buy_x, buy_y, duration=random.uniform(0.2, 0.5)) time.sleep(random.uniform(0.1, 0.3)) pyautogui.click() time.sleep(0.8) confirm_screen = grab_screen() confirm_pos = find_template(confirm_screen, "templates/confirm_btn.png") if confirm_pos: pyautogui.click(confirm_pos[0] + 30, confirm_pos[1] + 15)

这里有个非常大的坑:交易行购买时弹出的确认框不是每次都一样的。有时候是“确认购买”,有时候是“价格已变动,是否继续”,有时候是排队人数过多后的“重新尝试”。我最初只做了“确认购买”一个模板,结果遇到价格变动确认框时脚本直接卡住,直到超时。

解决方式是做一个状态机,每个弹窗模板对应一个点击策略:

弹窗类型模板特征处理动作
确认购买确认按钮为大面积绿色直接点击确认
价格变动提示文字“价格已更新”点击取消,重新搜索
排队限制提示“该物品热度高”等待 3 秒后重试
库存不足按钮灰置不可点结束该物品流程

状态机的实现思路是:每轮操作后等待 0.8 秒,截屏,依次匹配四个弹窗模板,看命中哪个就走哪个分支。都不命中就认为操作成功,进入下一轮。

4. 防错机制、日志系统与踩坑记录

4.1 运行监控与异常自恢复

脚本跑长时间之后,意外情况一定会出现。最典型的一种是网络延迟导致界面停留在一个中间状态:搜索框输入了文字但结果还没加载,这时候去读价格区域会读到空白,脚本继续往下走就会点错位置。

我加入了心跳监控机制。每一轮开始前先检测交易行界面的标志性元素,比如顶部“交易行”三个字的位置,检测不到就直接判定界面异常,执行一次系统重置:按下 ESC 键两次,等待 2 秒,重新打开交易行。这个自恢复逻辑虽然粗暴,但实测可以把脚本的无人值守时间从 2 小时延长到 12 小时以上。

日志系统也值得说一下。我用的 Python 标准库 logging,同时输出到文件和控制台,每条日志包含时间戳、模块名、事件类型和关键参数。日志格式长这样:

[2025-01-12 14:23:31] [action] 搜索物品: 5级弹药箱, 搜索框位置: (840, 412) [2025-01-12 14:23:32] [ocr] 读取到价格: 115000, 原始文本: "115,000" [2025-01-12 14:23:32] [decision] 5级弹药箱 当前价 115000 > 心理价 120000, 不购买

不要小看日志。第一次跑连续报错的时候,没有日志你根本不知道脚本在哪一步卡住了。我后来专门在每次 OCR 后记录原始文本,就是因为识别出来的数字偶尔会多一位或者少一位,看原文才能判断是 OCR 的问题还是显示的问题。

4.2 边界情况与异常处理速查表

实操中我整理了这些坑,按出现频率排序:

问题现象解决方法
OCR 价格多出空格换行数字对不上正则过滤所有非数字逗号字符
搜索框点击偏移点击后焦点不在输入框用模板匹配命中区域中心偏右偏移
搜索结果为空物品名打错了检查输入法状态,改用英文/数字时禁用中文输入
鼠标移出游戏窗口点击无效开启 PyAutoGUI failSafe,并加屏幕边缘检测
游戏窗口被遮挡截图截到别的程序运行期间禁止窗口切换,脚本检测前台窗口标题
网络延迟波动界面加载慢关键步骤后统一增加 1 秒等待,不做硬编码 sleep

第二条和第四条我重点补充一下。搜索框模板匹配返回的是图片左上角的坐标,但实际可点击区域是搜索框内部偏右的位置。如果直接点击左上角,可能点到搜索框边框,光标虽然看起来在框里,但没有触发输入焦点。我的处理是点击坐标加上一个固定偏移(搜索框宽的 20%,搜索框高的 50%)。

PyAutoGUI 有一个 failSafe 功能:把鼠标移到屏幕左上角会触发 FailSafeException 强制停止。这个功能我一直开着,脚本跑飞的时候可以一把甩鼠标到左上角紧急刹车。但 failSafe 只防失控,防不了游戏窗口被遮挡。我加了一个前置检测:每次操作前用 win32gui 读取当前前台窗口句柄和游戏窗口句柄,不一致就暂停脚本直到用户切回游戏。

4.3 为什么延时策略比连点更可靠

看过一些脚本作者喜欢把操作间隔压到极限,点击之间只隔 0.05 秒,觉得越快效率越高。我实测后彻底放弃了这种思路。

交易行系统的购买成功判定,核心在于服务器是否在截止时间前收到请求,而客户端显示的价格只是本地渲染结果。如果我在 0.05 秒内连续点击购买,服务器端可能已经把当前价格更新为更高的值,但我还在按旧价格提交,结果就是频繁触发“价格变动”失败提示,白忙一场。

我最终把每轮操作总时长控制在 8 到 12 秒之间:搜索 1 秒,读价 1 秒,判断 0.5 秒,点击 2 秒,确认等待 2 秒,额外随机缓冲 1 到 3 秒。这样虽然慢,但每一单的成功率高,整体收益反而比疯狂连点强得多。而且从风控角度,过于规律的高速操作是最容易被识别的,随机延时能把操作特征打散。

4.4 关于账号收益与风险,说点清醒的

做交易行自动化,最核心的收益来源是价差。游戏里道具价格不是实时动态波动的,很多时候一个热门物品在白天和晚上的价格差能到 10% 到 15%,如果你常驻扫货,能以心理价买到,再在高峰期卖出,利润空间是存在的。但这完全取决于市场行情和你的选品能力,脚本本身只是帮你把“蹲点”这件事自动化了,它不创造价值,只节省时间。

关于风险,我必须说明白:任何第三方自动化脚本在游戏里都有被检测和封禁的可能性。三角洲行动官方对于脚本的态度是零容忍的,虽然图像识别加键鼠模拟属于最外围的方案,检测难度高,但不代表绝对安全。我个人的使用原则是:小金额、低频率、不跨天挂机,单次运行不超过 4 小时,并且绝不使用脚本进行囤货后高价倒卖这种明显异常的操作。技术归技术,边界感还是要有。

5. 后续优化方向与个人心得

做完这套脚本之后,我最大的收获不是“挂在后台蹲到了多少便宜材料”,而是对整个 Windows 桌面自动化技术栈有了完整的理解。图像识别、坐标换算、状态机、异常恢复,这些技术在任何一个桌面端 RPA 项目里都能复用。

后续值得优化的方向有三个。第一个是买入后自动上架卖出,形成完整的低买高卖闭环,但这一步涉及价格预测和市场分析,复杂度和风险都上了一个台阶。第二个是把价格数据落库,每天跑完存一份 CSV,积累几周后做价格趋势分析,找到道具价格的最低点时间段,在那个时间段集中运行,效率会更高。第三个是用多开方案同时监视多个账号的浏览记录,但多开本身就是高风险行为,我不建议也不支持。

最后分享一个我在反复调参中发现的细节:脚本的扫描间隔不该是一个固定值,而应该配合物品的成交热度动态调整。热门物品比如弹药箱、高级修复工具,价格波动快,间隔可以短到 5 秒;冷门物品半小时都未必有人上新,间隔拉到 30 秒也不影响。你把所有物品统一用 8 秒间隔去扫,CPU 占用高不说,还容易触发游戏端的异常流量检测。我最终的方案是给每个物品单独配置 scan_weight,整体扫描顺序按权重循环,热门多扫、冷门少扫。这个思路,套用到任何自动化采集项目里都成立。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询