pywinauto实战:同花顺自动化交易中的验证码识别与弹窗处理

pywinauto实战:同花顺自动化交易中的验证码识别与弹窗处理 做同花顺自动化交易的初衷很简单每天重复的盯盘、刷新、填单、点确定枯燥乏味不说还特别容易手滑点错。我花了一个周末把第一版自动下单脚本跑起来的时候心想这下总算能省心了结果真正折磨人的事情才刚刚开始——登录验证码识别不准程序卡在登录页半小时好不容易进去了各种弹窗又冒出来要么挡住关键按钮要么把流程直接打断。这篇文章就以pywinauto为主线把我在这个项目里踩过的最深、最典型的几个坑写成一份避坑指南尽量把验证码识别、弹窗处理这两块讲透顺便还原一些实战中的报错现场和排查思路。这个内容不是给纯小白看的“安装教程”但我会把关键概念和代码逻辑都展开讲。无论你是想用同花顺做个人交易辅助还是单纯研究Windows桌面应用自动化这篇东西应该都能帮你少走弯路。我用的核心工具是pywinauto辅助Pillow、pytesseract、OpenCV整套方案只解决一个问题让脚本在面对验证码、弹窗这些“不确定性”的时候不至于直接崩溃并且还能自己恢复状态往下走。1. 为什么用pywinauto来自动化同花顺1.1 从需求说起自动化交易到底要自动化什么很多人一听到“自动化交易”下意识觉得是高频量化、API对接、毫秒级下单那一套。但我们用同花顺做自动化的场景通常不是这种。常见的需求其实很朴实每天开盘前自动登录账号、自动检查持仓和资金、按预定条件触发买入或卖出、收盘后自动导出当天数据、定时刷新行情并推送提醒。这类需求有一个共同特点——它们全都发生在同花顺的图形界面里。同花顺虽然有iFinD之类的数据接口但个人普通版的很多操作并没有公开API支持尤其像点击菜单、填下单窗口、确认对话框这些动作最直接的方式就是模拟人的操作。pywinauto就是干这个的它能枚举Windows窗口、定位控件、发送按键、读取文本把你在图形界面里的操作一条条脚本化。但界面自动化有一个本质难点GUI环境的“不确定性”极高。验证码图形忽大忽小、弹窗类型五花八门、控件ID偶尔还变这些在正常手动操作时根本不会注意但脚本一旦碰上就会卡死。所以整个项目真正要解决的不是“怎么写点击代码”而是“怎么处理这些不确定性”。1.2 工具选型pywinauto、pyautogui、按键精灵我为什么最后选了pywinauto做同花顺自动化可选方案其实有好几种。我最早试过按键精灵优点是上手快、录制方便但遇到动态窗口和不定时弹窗时脚本逻辑很容易变成一大堆等待和判断维护起来特别痛苦。后来又试了pyautogui它的定位方式是纯屏幕坐标截图匹配简单粗暴可同花顺窗口只要一拖动、一缩放、一换皮肤所有坐标全部失效运行一整天下来十有八九会点错地方。pywinauto的优势在于不依赖固定坐标而是通过窗口句柄、控件ID、类名、标题等Windows原生信息去定位。只要窗口存在它就能找到对应控件窗口移动了、缩放了依然能准确定位。这一点对同花顺这种内部结构复杂、窗口会频繁弹来弹去的应用来说非常关键。当然pywinauto也有学习成本比如需要区分backendwin32和后期的UIA需要理解窗口查找的超时机制和控件属性。但这些前期投入是值得的因为一旦把基础框架搭好后面加的每一个功能都只需要复用已有的窗口处理逻辑稳定性远高于坐标模拟方案。我在项目里最终采用了“pywinauto为主pyautogui偶尔辅助”的组合适用于同花顺整体环境。1.3 整体技术方案一条主流程 两套兜底我的整体设计分成三层第一层是主流程严格按照“启动同花顺 → 等待窗口加载 → 输入账号密码 → 识别并填写验证码 → 登录 → 进入主界面 → 检查持仓/下单 → 处理交易确认弹窗 → 记录日志退出”来走。主流程要尽量线性、简单不要在主流程里做太多复杂的判断。第二层是验证码处理模块专门负责截图、图像预处理、OCR识别、结果校验如果识别失败就自动重试重试超过三次就投降弹出人工输入提示而不是傻等着或者反复提交错误验证码。第三层是弹窗处理模块用独立的监控循环盯着当前活动的窗口一旦发现非预期的弹窗先识别弹窗类型再决定是关闭它、点击确定、输入内容还是直接终止流程。这三层各管各的避免逻辑互相纠缠。这个结构最大的好处是出了问题好排查。脚本卡住时看日志就知道卡在“主流程第几步”“验证码重试了第几次”“弹窗模块抓到了什么窗口”不需要从头到尾排查一遍。2. 验证码识别三种思路与避坑实录2.1 同花顺登录验证码的常见形态同花顺登录时多数情况下是需要输入验证码的有时候是纯数字有时候是字母数字混合偶尔还会出现滑块干扰。我实际使用中遇到最多的是四位或五位数字字母混合背景有轻微噪点字体带一点扭曲但不算特别变态属于“人类轻松识别、程序稍微费劲”的难度。对验证码这类识别任务先别急着选模型可以先观察几个维度字符数量是否固定、字符集是否有限、图片尺寸是否稳定、背景噪点强度、字符是否有粘连。这些直接决定用哪种识别策略。我在项目里把验证码分成了两类一类是“规则验证码”字符间距比较规整背景干扰不大另一类是“不规则验证码”有旋转、扭曲、干扰线。前一类用OCR加简单预处理就够了后一类需要模板匹配或者接入深度学习模型。同花顺登录验证码在我实测中属于前一类偏难一点的用OCR方案调整参数后成功率能到七八成。2.2 方案AOCR识别——最直接但也最容易翻车OCR方案的核心流程是“截图 → 图像预处理 → 文字识别 → 结果清洗”。听起来不复杂但每一步都有坑。截图这一步很多新手会直接用屏幕坐标截取验证码图片区域这在我一开始也踩过坑。因为同花顺登录窗口的尺寸、缩放比例不是固定的截出来的图经常偏左偏右。更稳的做法是用pywinauto定位验证码图片控件拿到它的矩形区域坐标再基于这个坐标去截屏。from pywinauto import Application from PIL import ImageGrab app Application(backendwin32).connect(pathhexin.exe) login_win app.window(class_name#32770, title用户登录) # 这里以验证码图片控件为例实际控件名需要根据本机情况调整 captcha_ctrl login_win.child_window(auto_idcodelmg) rect captcha_ctrl.rectangle() img ImageGrab.grab(bbox(rect.left, rect.top, rect.right, rect.bottom)) img.save(captcha.png)截到图之后预处理非常影响识别率。同花顺验证码的背景偏灰字符颜色偏深所以第一步要转灰度再做二值化。但二值化的阈值不能拍脑袋定我是先统计像素分布再用OTSU自动计算阈值这样在不同亮度环境下都能自动适配。import cv2 import numpy as np img cv2.imread(captcha.png, cv2.IMREAD_GRAYSCALE) _, thresh cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) thresh cv2.medianBlur(thresh, 3) cv2.imwrite(captcha_binary.png, thresh)识别环节我用过pytesseract也试过PaddleOCR。pytesseract配置简单但识别同花顺这种带扭曲的数字字母时需要限制字符白名单和页面分割模式不然它会把很多东西识别成奇怪字符。import pytesseract text pytesseract.image_to_string( thresh, config--psm 8 -c tessedit_char_whitelistABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 ) code .join(filter(str.isalnum, text))这里有个关键参数--psm 8表示把图片当成单个字符文本行处理。对于4到5位验证码这个模式比默认的--psm 3要好得多默认模式会把图片当成整页文本容易多识别出乱七八糟的内容。白名单也一定要做我见过太多人因为没加白名单把“0”识别成“O”把“1”识别成“l”导致登录失败。但是OCR方案在你第一次跑通后一定要测试足够多样本。我实测同花顺不同时段的验证码噪点强度差异很大白天阳光直射屏幕时截图的亮度和晚上完全不一样。如果你只在同一环境下测很容易误判识别率。2.3 方案B模板匹配与二值化处理如果OCR识别率长期在及格线以下可以考虑模板匹配。思路很简单把数字0到9、字母A到Z的截图存成模板然后对验证码图片里的每个字符区域做相似度匹配得分最高的模板就是对应字符。这个方案看起来土但对同花顺这种“字符位置相对固定、字体样式变化不大”的验证码反而比通用OCR更稳。因为我截取的验证码控件区域里每个字符的大致位置是固定的只要把图片切成等宽的小块再分别匹配每个小块里的字符。import cv2 import numpy as np import os def match_template(char_img, template_dir): best_score -1 best_char None for fname in os.listdir(template_dir): tmpl cv2.imread(os.path.join(template_dir, fname), cv2.IMREAD_GRAYSCALE) tmpl cv2.resize(tmpl, (char_img.shape[1], char_img.shape[0])) res cv2.matchTemplate(char_img, tmpl, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(res) if max_val best_score: best_score max_val best_char os.path.splitext(fname)[0] return best_char模板匹配的难点在于模板素材的采集和维护。每个字符在验证码里可能有几种粗细、扭曲程度不同的写法你需要多存几个模板匹配时取最高分。这个工作前期会花一些时间但效果立竿见影。我有一次把验证码识别率从六成拉到了九成用的就是这一招。不过模板匹配也有明显局限如果同花顺将来更新了验证码字体或者换了一套图形风格模板就得重新采集。所以这个方案适合“验证码长期不变”的场景不太适合频繁改版的网站或软件。2.4 方案C人工介入兜底自动化项目里最容易犯的错误是“非要100%全自动”。说到验证码识别技术上我可以不断优化但现实是识别再怎么提升总会有几次抽风。而且同花顺短时间内多次输错验证码账号还可能被暂时锁定这个风险远大于人工介入的成本。所以我的方案里有一个兜底分支OCR和模板匹配都失败、连续重试超过设定次数后自动截图保存验证码原图把程序状态切到“等待人工输入”并通过桌面通知或者企业微信机器人发消息给我。我看到消息后手动输入验证码程序继续往下走。for attempt in range(3): captcha_bbox get_captcha_bbox() text recognize_captcha(captcha_bbox) if len(text) expected_length and text.isalnum(): break time.sleep(1) else: show_manual_captcha_dialog(login_win) text wait_manual_input()这个设计看起来很“不自动化”但实用才是第一位的。项目失败的代价往往不在于多花几秒人工介入而在于脚本在无人值守时卡死或者触发账号限制。人工兜底相当于给整个系统买了一份保险成本低、收益高。2.5 验证码识别避坑清单总结一下验证码这块我实际踩过的坑和对应的处理建议截屏坐标必须从验证码控件实时获取不能用固定坐标。登录窗口位置、缩放一变固定坐标就全废了。二值化前先做灰度转换但不要直接全局固定阈值。OTSU自动阈值在大多数环境下表现稳定值得优先考虑。OCR识别一定要配置白名单和页面分割模式--psm否则会把字母数字识别成莫名其妙的内容。识别结果要做长度校验。比如预期四位就用四位如果OCR返回三位或六位优先判断为识别失败不要硬填到输入框里。连续失败要设置重试上限并且重试前等待时间递增避免给账号带来额外风险。保存每次验证码原图和识别结果方便复盘。我最终能调出八成以上识别率全靠积累样本。3. 界面弹窗处理pywinauto实战中的一条命3.1 同花顺可能出现的几类弹窗同花顺的弹窗是我这个项目里第二大折磨源。手动操作时弹窗是无所谓的随手点掉就行但脚本跑起来之后弹窗会阻断后续所有操作。我把实际遇到的弹窗归成四类第一类是登录后的提示弹窗比如“风险揭示书”“交易规则变更通知”“版本更新提示”。这类弹窗通常带有“确定”“知道了”“我已阅读”等按钮处理方式就是点击对应按钮关闭。第二类是操作确认弹窗比如委托下单时弹出的“确认买入/卖出”对话框。这类弹窗是流程的一部分不能简单关闭需要读取内容并点击“确认”或“取消”。第三类是异常报错弹窗比如“网络连接失败”“资金不足”“价格超出涨跌幅限制”。这类弹窗意味着当前操作已经失败如果脚本还傻傻地找“确认”按钮继续后续流程大概率会连环出错。第四类是“隐藏炸弹”比如同花顺在后台进行的自动更新提示、广告推广弹窗、甚至是某个指标脚本的错误提示。它们出现的时机完全随机如果不处理脚本可能在半夜挂机时被一个广告弹窗卡死。3.2 pywinauto定位弹窗的核心方法pywinauto处理弹窗的基本思路是先枚举当前所有顶层窗口再根据窗口标题、类名、控件内容过滤出目标弹窗。因为弹窗时多线程监控循环需要处理所以我不会只盯着大地图而是建立一个全局的弹窗处理函数。from pywinauto import Application def find_dialogs(app): windows app.windows() candidates [] for win in windows: klass win.class_name() title win.window_text() if klass #32770 or 同花顺 in title or 提示 in title or 确认 in title: candidates.append(win) return candidates#32770是Windows标准对话框的类名同花顺的很多弹窗都是这个类所以它是我过滤弹窗的第一关键字。但要注意#32770出现得太频繁不能一见到就关闭必须再叠加标题、按钮文本、甚至内部控件属性来判断是什么弹窗。定位到弹窗之后下一步就是获取弹窗里的控件信息看看它有哪些按钮。dlg app.window(class_name#32770, title_re.*提示.*) dlg.print_control_identifiers()打印出来的控件标识符能直接看到对话框里每一个按钮的文本、控件类型、自动化ID。我通常根据按钮文本决定动作如果有“确定”说明是普通提示如果有“确认买入”说明到了下单确认环节如果只有“重试”或“取消”说明发生了异常。3.3 正常流程中的弹窗处理策略正常流程里的弹窗处理我的原则是“有限自动化”只处理能明确识别并确认安全的弹窗识别不了就停下来等人。具体实现上我按照弹窗的按钮文本和行为分成三类来处理。第一类是“可安全关闭”的比如“我知道了”“确定”“关闭”。这类弹窗不影响交易流程直接关闭然后继续主流程。第二类是“交易确认”的比如“确认买入”“确认卖出”“委托确认”。这类弹窗需要结合用户预设的策略来决定点“确定”还是“取消”。比如策略设置的是“当价格低于X元就买入”弹窗出现时我会重新读取一遍价格再确认价格仍然满足条件然后才点“确定”。def handle_confirm_dialog(dlg, expected_action): btn_ok dlg.child_window(title确定, class_nameButton) btn_cancel dlg.child_window(title取消, class_nameButton) content dlg.static_texts() for ctrl in content: text ctrl.window_text() if 价格 in text or 数量 in text: save_log(confirm content: text) if expected_action buy: btn_ok.click() else: btn_cancel.click()第三类是“需要重启或重试”的比如网络断线重连提示。这种情况下点击“重试”按钮并更新内部状态等待一定时间后重新连接。这个分类逻辑看起来简单但真正执行的时候要非常小心。我第一天跑脚本时有一回一个“风险提示”弹窗背后还藏着一个“委托确认”弹窗脚本只看到外层弹窗的“确定”就点了结果内层弹窗因为找不到“确定”按钮直接抛异常整个流程停在那。后来我把弹窗处理改成了循环扫描模式——弹窗可能弹多个处理完一个要再扫一遍直到确认没有新的弹窗。3.4 异常弹窗的兜底处理超时与自愈正常弹窗都好处理真正麻烦的是随机出现的异常弹窗。比如同花顺启动时可能在登录窗口还没完全加载时就弹出一个“网络异常”的对话框这时如果主流程还在等待登录窗口就绪容易形成死锁主流程等窗口弹窗处理模块等主流程释放资源。我采用的方案是“独立弹窗监控线程 主流程等待超时”。弹窗监控线程每隔0.5秒扫描一次当前所有窗口发现可疑弹窗就进入处理流程。主流程在等待某个控件时设置超时比如等一个按钮等5秒没等到就抛出异常并进入恢复流程。只要两边的阻塞机制互相独立就不会死锁。import threading import time def monitor_popups(app, stop_event): while not stop_event.is_set(): try: dialogs find_dialogs(app) for dlg in dialogs: handle_dialog(dlg) except Exception: pass time.sleep(0.5) stop_event threading.Event() t threading.Thread(targetmonitor_popups, args(app, stop_event)) t.start()这个线程模型的目的是“自愈”。脚本跑着跑着突然遇到一个更新提示弹窗监控线程把它关掉主流程甚至感知不到发生过这事如果关不掉监控线程就把弹窗信息写进日志同时设置一个全局“异常挂起”标志让主流程在关键操作前检查这个标志决定是继续还是终止。这套设计让我的脚本从“一弹窗就死”进化到“大多数弹窗都被悄悄处理掉”挂机稳定率提高了很多。最开始我用的是最简单的顺序处理每次主流程操作前都去点掉弹窗结果一旦弹窗出现时机和主流程操作时机重叠就会出现重复点击、点错按钮的情况。独立线程 标志位才是正解。4. 常见问题与排查技巧实录4.1 控件识别不到窗口类名一直是“#32770”怎么办遇到控件识别不到先不要急着改代码先手动看看窗口到底长什么样。把print_control_identifiers()的输出完整过一遍确认窗口有没有加载完成。同花顺的窗口加载有时候慢刚启动时能看到窗口轮廓但内部控件还没初始化。我一般在启动后先固定等待2到3秒再用wait(ready, timeout10)来确保窗口就绪。如果类名、标题都对但还是识别不到子控件可能是backend选错。pywinauto有两个backendwin32和uia。同花顺大部分控件用win32就能搞定但有些新版界面元素比如某些列表视图可能需要uia。我建议先在两种backend下都跑一下print_control_identifiers()看哪个backend输出更完整。还有一个容易忽略的点同花顺有“皮肤”功能不同皮肤下一些控件的样式和内部结构会变。如果你换过皮肤后发现脚本崩了优先考虑是不是控件属性变了而不是代码逻辑出了问题。我的做法是把同花顺固定使用经典皮肤并且在代码注释里写清楚这个前提。4.2 同花顺界面皮肤/字库导致OCR傻眼怎么办如果你验证码识别脚本在自己电脑上跑得好好的换到另一台电脑就失灵那很可能是分辨率、缩放比例、字体渲染不同导致截图内容出现差异。Windows的缩放设置是最常见的坑。如果你的电脑是150%缩放而代码里默认100%那么截图区域就偏了。解决办法是用pywinauto拿验证码控件的坐标时换算成物理像素时要注意缩放系数。import ctypes def get_dpi_scale(): try: awareness ctypes.c_int() ctypes.windll.shcore.SetProcessDpiAwareness(2) return 1.0 except Exception: return 1.0更稳妥的做法是调用SetProcessDpiAwareness让程序感知DPI这样拿到的坐标就不会因为缩放比例发生偏移。否则你在一台100%缩放的电脑上截的验证码区域在150%缩放的电脑上会截到验证码的一部分识别率当然无从谈起。字体渲染则会影响二值化后的清晰度。同花顺验证码在不同系统上渲染出来的边缘平滑度不同我建议在做二值化时增加一步“膨胀/腐蚀”操作把细小噪点去掉让字符轮廓更完整。这个方法对字符边缘较细的验证码尤其有效。4.3 主流程被弹窗打断后如何恢复状态主流程被弹窗打断最典型的表现是脚本正在等待“买入”按钮出现结果弹窗监控线程先一步点掉了“确认”按钮导致主流程后续找“确认”按钮时找不到抛异常退出。这种情况下恢复状态的核心思路是“每操作一步都记录日志和当前状态”异常恢复时先读状态再决定从哪一步重新开始。我自己会维护一个简单的状态机用文件保存当前状态比如STEP_LOGIN、STEP_INPUT_CODE、STEP_ORDER_PENDINGSTATE_FILE autotrade_state.json def save_state(state): with open(STATE_FILE, w) as f: json.dump({state: state}, f) def load_state(): with open(STATE_FILE, r) as f: return json.load(f).get(state)当脚本检测到异常时不直接退出而是回退到上一个安全状态。比如下单流程出问题就回退到“等待弹窗处理”状态重新读取当前持仓和价格再决定是否重新下单。这个“回退一步”的策略比每次都从登录重新开始要高效得多。另外异常恢复前一定要清理可能残留的窗口。比如下单失败后交易窗口可能还开着如果不先关掉重新打开下一次操作可能会操作到旧窗口上导致状态错乱。4.4 速度与稳定性既要快又要稳自动化交易脚本对速度会有一些敏感性但我不建议为了速度牺牲稳定性。我见过有人把每次控件查找的超时时间设成0.5秒结果稍有卡顿就抛异常。更合理的做法是区分场景主流程里的控件等待可以设置较长超时比如10秒监控线程的扫描间隔可以短一些比如0.5秒但操作时不要无意义地加sleep。同花顺本身是网络应用有响应延迟很正常。等待控件的原则是“事件驱动优先”能用wait(exists)、wait(enabled)等功能就不要用sleep(5)。显式等待在控件提前出现时能立即继续长期跑下来速度反而更快。还有一个很多人忽略的点发完指令后不要马上找下一个控件。比如输入完验证码点击登录按钮后服务端校验需要时间这时候如果立刻去查主窗口里的持仓控件可能窗口还没刷新完成。我一般在点击“登录”后等待主窗口的某个标志性控件出现比如“自选股”标签或者账户余额区域再继续后面的操作。这样既不用固定等几秒也不会因为过早操作而出错。4.5 安全与合规提醒自动化交易这个话题我必须提醒一句使用脚本操作同花顺这类交易软件一定要先确认是否符合券商和软件的用户协议是否会给账号带来风险。技术本身是中性的但实际使用时要对自己的行为负责。我建议的边界是脚本尽量只做“数据读取”“辅助决策提醒”这类操作对真实下单保持谨慎。如果真要跑无人值守的自动化交易一定要有完善的熔断机制——比如当日亏损超过一定比例就停止所有操作或者连续操作失败多次就通知人工介入。不要以为脚本“写对了”就不会出错网络抖动、软件改版、行情异常这些外部因素随时都可能让你的脚本在错误的时机做错误的操作。另外账号安全是第一位的。脚本里保存的账号密码和会话信息要做好加密日志里不要打印完整密码。我踩过一个大坑有一次为了调试把登录参数原样打到了日志里后来发现日志文件被同步到网盘吓得我赶紧改了密码并清理了所有历史日志。这个习惯一定从第一天就养成。结语一点实际感受和后续扩展整个项目做完我最深的感受是自动化交易真正的难点不在于“写代码让程序点击按钮”而在于“让程序在遇到异常时能像人一样判断该不该继续”。验证码识别和弹窗处理这两关本质都是让脚本具备一定的容错和恢复能力。后面我还在这个基础上扩展了两个小功能一个是把验证码识别结果和弹窗处理日志做成可视化面板同一时刻能看出脚本卡在哪一步另一个是加入了简单的“盘后复盘”功能自动导出当天的成交记录和持仓变化。如果同花顺的界面稳定不变这套框架可以一直用下去如果哪天它改了版我只需要更新控件定位信息和弹窗分类规则就行整体骨架不用推翻重写。最后再分享一个小技巧不要在脚本里写死“等几秒”而是统一封装一个wait_until函数支持轮询超时。所有页面等待、弹窗等待、数据刷新等待都走这个函数日志里自动记录等待时间和最终结果。我就是靠这个函数才把那些稀奇古怪的偶发问题一个一个定位清楚的。