Python自动化截图与OCR识别:实现无API券商持仓数据抓取

📅 2026/8/14 9:03:07
Python自动化截图与OCR识别:实现无API券商持仓数据抓取
1. 项目缘起为什么选择“截图识别”这条“野路子”在量化交易或者个人资产管理的圈子里获取实时、准确的持仓数据是第一步也是最关键的一步。很多朋友第一时间想到的是找券商开放API这确实是“正规军”的做法。但现实情况是国内绝大多数面向个人投资者的券商客户端尤其是那些功能丰富的传统桌面软件并没有提供稳定、易用的官方数据接口。即便有也往往伴随着复杂的申请流程、严格的权限审核和令人头疼的签名加密。对于只是想自动化记录一下自己账户盈亏、做个简单分析的个人投资者来说这条路门槛太高。于是“截图图像识别”这条技术路线就成了一种务实且高效的替代方案。它的核心思想很直接既然人眼能从客户端界面上看到持仓股票、成本、市值、盈亏这些信息那为什么不让程序模拟人的操作先截图再像人一样去“读”这些信息呢听起来有点“曲线救国”甚至带点“黑科技”的味道但它的优势非常明显——几乎通用。无论你用的是哪家券商的PC客户端只要它能正常显示持仓这个方案就有用武之地。它不依赖券商的底层接口只与客户端呈现的UI界面打交道避开了最复杂的协议对接环节。我最初也是被这个“通用性”所吸引。当时手头有几个不同券商的账户数据分散手动记录效率极低。在评估了各种方案后最终决定用Python打造一套自动化的持仓数据抓取工具。这条路走下来虽然踩了不少坑比如客户端界面更新导致识别失败、弹窗干扰、识别精度问题等但整套流程跑通后确实解放了双手数据获取的稳定性和准确性也超出了最初的预期。接下来我就把这套从自动截图到精准识别的完整方案包括其中的技术选型、核心步骤和避坑经验详细拆解一遍。2. 技术栈选型与核心原理拆解整个项目可以清晰地划分为两个核心阶段自动化控制与截图、图像识别与数据提取。每个阶段的技术选型都直接决定了项目的稳定性和易用性。2.1 自动化控制为什么是PyAutoGUI和PyGetWindow在自动化控制领域Python有不少库如Selenium用于Web、Appium用于移动端。但对于Windows桌面应用程序特别是那些用传统技术如MFC、WinForms、甚至DirectUI开发的客户端PyAutoGUI和PyGetWindow的组合是目前最轻量、最直接的选择。PyAutoGUI它的核心价值在于模拟人的鼠标和键盘操作。我们可以用它来激活券商客户端窗口、点击菜单、甚至滚动持仓列表。更重要的是它提供了screenshot()函数可以截取整个屏幕或屏幕上指定区域的图像。它的工作原理是调用操作系统底层的API因此兼容性极好。PyGetWindow它的任务是精准地定位目标窗口。券商客户端一旦启动在系统中就是一个窗口对象。PyGetWindow可以通过窗口标题哪怕是不完整的来找到这个窗口并获取其位置、大小等信息。有了这些信息PyAutoGUI才能知道该对哪里进行截图避免了“盲狙”。为什么不直接用PyAutoGUI找窗口PyAutoGUI虽然也有简单的窗口查找功能但PyGetWindow的API更专一、更强大特别是在处理窗口标题模糊匹配、获取窗口层级关系时更加可靠。两者结合一个负责“定位”一个负责“操作”和“截图”分工明确。这里有一个关键细节很多券商客户端的主界面其实是一个“框架”持仓信息显示在内部的某个子面板Pane或控件Control里。直接截取整个窗口可能会包含大量的菜单、标签页、广告等噪音区域。因此更精细的做法是先用PyGetWindow定位到主窗口再通过计算或预先设定的坐标偏移量定位到持仓列表所在的精确区域然后只对这个区域截图。这能大幅减少后续图像识别需要处理的无关信息提升识别速度和准确率。2.2 图像识别从OCR到结构化数据的关键一跃截图拿到的是图片我们的目标是从图片中提取结构化的文本数据股票代码、名称、数量、成本价、市价、盈亏等。这里的主角是**光学字符识别OCR**技术。Tesseract OCR pytesseract这是开源OCR领域的标杆。Tesseract由Google维护识别精度高支持多种语言包括中文。pytesseract是它的Python封装库让我们能在Python中方便地调用。它的工作流程是接收一张图片通过内置的图像预处理和文字检测、识别算法输出图片中的文本内容。然而直接对持仓截图使用pytesseract.image_to_string()你得到的很可能是一团杂乱无章的文本像这样平安银行 000001 持仓: 1000 成本价: 15.20 市价: 16.50 盈亏: 1300.00 招商银行 600036 持仓: 500 成本价: 35.80 市价: 34.20 盈亏: -800.00这虽然包含了所有信息但程序无法区分哪段文字是股票名称哪段是代码。我们需要的是结构化的数据比如一个字典列表[ {name: 平安银行, code: 000001, position: 1000, cost: 15.20, price: 16.50, profit: 1300.00}, {name: 招商银行, code: 600036, position: 500, cost: 35.80, price: 34.20, profit: -800.00} ]因此简单的OCR识别只是第一步更关键的是对识别结果的“后处理”。这就需要用到文本处理和正则表达式。正则表达式re模块它是从混乱文本中提取规律信息的利器。持仓数据的显示通常有固定格式比如“股票名称 代码 持仓: 数量 成本价: 价格 ...”。我们可以为每个字段编写正则表达式模式来捕获。例如匹配股票代码6位数字r(\d{6})匹配浮点数价格、盈亏r([-]?\d\.\d)匹配中文股票名称r([\u4e00-\u9fa5])这是一个匹配中文字符的简单范围一个常见的误区是试图用一个复杂的正则表达式匹配整行。这在实际中非常脆弱因为客户端UI的微小变化比如多了个空格、换了分隔符就会导致匹配失败。更稳健的策略是先分而行之用换行符(\n)将OCR输出的整段文本分割成独立的行每行对应一只股票前提是截图区域干净OCR分行正确。再逐个击破对每一行文本使用多个相对简单的正则表达式分别去“抓取”可能存在的字段。即使某个字段匹配不到比如某些客户端不显示成本价也不会影响其他字段的提取。容错处理对匹配结果进行校验。例如检查股票代码是否为6位数字检查价格是否为合理范围的浮点数。将无法通过校验的行标记为识别失败便于后续人工复核或调整识别参数。3. 实战步骤详解从零搭建自动化脚本理论清晰后我们开始动手。假设我们的目标是每天下午3点收盘后自动获取某券商客户端的持仓数据并保存为CSV文件。3.1 环境准备与依赖安装首先确保你的Python环境建议3.7以上已经就绪。然后安装核心依赖库pip install pyautogui pygetwindow Pillow opencv-python pytesseractPillow(PIL)Python图像处理库pytesseract依赖它来读取图片。opencv-python(cv2)可选但强烈推荐。用于在图像识别前进行更高级的预处理如灰度化、二值化、降噪可以显著提升Tesseract的识别精度。特别注意Tesseract-OCR引擎的安装pytesseract只是一个调用接口你需要单独安装Tesseract-OCR引擎本体。Windows从 GitHub - tesseract-ocr/tesseract 下载安装程序。安装时记得勾选中文语言包如chi_sim。安装完成后需要将Tesseract的安装路径如C:\Program Files\Tesseract-OCR添加到系统的PATH环境变量中或者在你的Python脚本里指定路径pytesseract.pytesseract.tesseract_cmd rC:\Program Files\Tesseract-OCR\tesseract.exemacOS使用Homebrew安装brew install tesseract tesseract-langLinux使用包管理器安装例如Ubuntusudo apt install tesseract-ocr tesseract-ocr-chi-sim3.2 步骤一定位并激活券商客户端窗口这是自动化的起点必须确保脚本能找到正确的窗口。import pyautogui import pygetwindow as gw import time def activate_client(client_window_title): 通过窗口标题或部分标题激活券商客户端窗口。 :param client_window_title: 窗口标题关键词如通达信、同花顺 :return: 窗口对象如果找不到则返回None # 获取所有包含关键词的窗口 windows gw.getWindowsWithTitle(client_window_title) if not windows: print(f未找到标题包含 {client_window_title} 的窗口。) return None # 通常第一个就是最匹配的或者你可以根据更复杂的逻辑选择 target_window windows[0] # 激活窗口将其提到最前面 if target_window.isMinimized: # 如果窗口最小化则恢复 target_window.restore() target_window.activate() # 等待窗口完全激活界面稳定 time.sleep(1) # 可选将窗口移动到固定位置便于后续截图坐标稳定 # target_window.moveTo(0, 0) return target_window # 使用示例 client_title 您的券商客户端名称 window activate_client(client_title) if window is None: print(客户端未启动或窗口标题不匹配程序退出。) exit(1)避坑点1窗口标题的动态变化。有些客户端窗口标题会随着登录账号或行情变化而改变例如“某某证券 - [账号]”。这时client_window_title参数应该设置为标题中固定不变的部分。可以使用print([w.title for w in gw.getAllWindows()])来查看所有窗口标题找到规律。避坑点2窗口激活后的延迟。activate()之后立即截图可能会截到窗口还未完全渲染好的画面。time.sleep(1)是一个简单的缓冲。更可靠的做法是截图后检查某个固定位置比如窗口的某个角落的像素颜色是否变为预期值以此作为界面加载完成的信号。3.3 步骤二精准截图与图像预处理成功激活窗口后我们需要截取持仓数据所在的精确区域。from PIL import ImageGrab import cv2 import numpy as np def capture_position_data(window, region_offset): 截取窗口内特定区域的图像。 :param window: PyGetWindow的窗口对象 :param region_offset: 一个四元组 (left, top, width, height)表示相对于窗口左上角的偏移和区域大小。 :return: 截取的PIL Image对象 # 获取窗口在屏幕上的绝对位置和大小 win_left, win_top, win_width, win_height window.left, window.top, window.width, window.height # 计算截图区域的绝对屏幕坐标 region_left win_left region_offset[0] region_top win_top region_offset[1] region_right region_left region_offset[2] region_bottom region_top region_offset[3] screenshot_region (region_left, region_top, region_right, region_bottom) # 使用PIL的ImageGrab截取指定区域 image ImageGrab.grab(bboxscreenshot_region) return image def preprocess_image_for_ocr(pil_image): 对截图进行预处理优化OCR识别效果。 :param pil_image: PIL Image对象 :return: 处理后的图像通常为二值化的OpenCV格式 # 1. 转换为OpenCV格式 (numpy数组) open_cv_image np.array(pil_image) # PIL是RGBOpenCV是BGR需要转换 open_cv_image cv2.cvtColor(open_cv_image, cv2.COLOR_RGB2BGR) # 2. 转换为灰度图减少计算量 gray cv2.cvtColor(open_cv_image, cv2.COLOR_BGR2GRAY) # 3. 二值化阈值处理让文字更清晰 # 自适应阈值比全局阈值更能应对光照不均的界面 binary cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) # 4. 可选降噪中值滤波或形态学操作 # denoised cv2.medianBlur(binary, 3) # 5. 将处理后的图像转换回PIL格式供pytesseract使用 # 注意pytesseract可以直接接受OpenCV格式但为了清晰我们转回PIL final_image Image.fromarray(cv2.cvtColor(binary, cv2.COLOR_GRAY2RGB)) return final_image # 使用示例 # 假设你已经通过反复测试确定了持仓列表区域相对于窗口左上角的偏移是 (50, 150)宽高为 (800, 400) region_offset (50, 150, 800, 400) raw_image capture_position_data(window, region_offset) # 保存原始截图以供调试 raw_image.save(raw_holding_screenshot.png) # 进行预处理 processed_image preprocess_image_for_ocr(raw_image) processed_image.save(processed_holding_screenshot.png)如何确定region_offset这是一个需要手动校准的步骤。你可以写一个简单的脚本在激活客户端后用pyautogui.displayMousePosition()实时获取鼠标所在位置的屏幕坐标。然后将鼠标移动到持仓列表区域的左上角和右下角分别记录坐标。通过计算与窗口左上角(window.left, window.top)的差值就能得到region_offset。务必在客户端界面稳定无弹窗、持仓列表已完全加载的状态下进行校准。预处理的重要性直接从客户端截取的图像可能因为背景色、字体抗锯齿、阴影等因素导致OCR识别率低。灰度化和二值化是提升黑白文本识别精度的最有效手段。cv2.adaptiveThreshold自适应阈值处理特别适合软件界面因为它能根据图像局部区域动态计算阈值比简单的cv2.threshold全局阈值效果更好。3.4 步骤三OCR识别与文本后处理这是将图像转化为可用数据最核心的一步。import pytesseract import re def extract_data_from_image(image, langchi_simeng): 从预处理后的图像中提取文本并解析为结构化的持仓数据。 :param image: PIL Image对象 :param lang: Tesseract语言配置chi_simeng表示中英文混合识别 :return: 持仓数据列表 # 配置Tesseract参数提升识别精度 custom_config r--oem 3 --psm 6 -c tessedit_char_whitelist0123456789.-%·股票名称代码持仓数成本价市价盈亏可用余额 # --oem 3: 使用默认的OCR引擎模式 # --psm 6: 将图像视为一个统一的文本块适合排列整齐的表格文本 # -c tessedit_char_whitelist...: 白名单只识别这些字符可以过滤很多干扰符号 # 进行OCR识别 text pytesseract.image_to_string(image, langlang, configcustom_config) print( OCR识别原始文本 ) print(text) print() # 文本后处理按行分割 lines text.strip().split(\n) holdings [] # 定义正则表达式模式需要根据你的客户端实际显示格式调整 # 示例模式假设格式为“股票名称 代码 持仓/可用: 数量 成本价: 价格 市价: 价格 盈亏: 金额” pattern_name r([\u4e00-\u9fa5A-Z]) # 股票名称中英文 pattern_code r(\d{6}) # 股票/基金代码6位数字 pattern_quantity r持仓[:]?\s*(\d(?:\.\d)?) # 匹配“持仓1000”或“持仓 1000” pattern_cost r成本[价]?[:]?\s*([-]?\d\.\d) # 匹配成本价 pattern_price r市价[:]?\s*([-]?\d\.\d) # 匹配市价 pattern_profit r盈亏[:]?\s*([-]?\d\.\d) # 匹配盈亏金额 for line in lines: line line.strip() if not line or len(line) 5: # 过滤空行和过短行 continue holding {} # 尝试匹配各个字段 name_match re.search(pattern_name, line) code_match re.search(pattern_code, line) qty_match re.search(pattern_quantity, line) cost_match re.search(pattern_cost, line) price_match re.search(pattern_price, line) profit_match re.search(pattern_profit, line) if name_match: holding[name] name_match.group(1) if code_match: holding[code] code_match.group(1) if qty_match: holding[position] float(qty_match.group(1)) if . in qty_match.group(1) else int(qty_match.group(1)) if cost_match: holding[cost] float(cost_match.group(1)) if price_match: holding[price] float(price_match.group(1)) if profit_match: holding[profit] float(profit_match.group(1)) # 只有识别出关键信息如代码和名称才认为是有效持仓 if holding.get(code) and holding.get(name): holdings.append(holding) else: print(f警告行 {line} 未能解析为有效持仓已跳过。) return holdings # 使用示例 holdings_data extract_data_from_image(processed_image) print(解析后的持仓数据) for item in holdings_data: print(item)避坑点3OCR配置的调优。--psm页面分割模式参数对识别结果影响巨大。对于整齐的表格文本--psm 6统一文本块通常比默认模式更好。tessedit_char_whitelist字符白名单是另一个神器它能强制Tesseract只关注你指定的字符集极大减少乱码和错误识别。你需要根据客户端实际显示的字符来调整这个白名单。避坑点4正则表达式的健壮性。客户端UI上的文字格式并非一成不变。可能使用全角冒号“”或半角冒号“:”可能有空格也可能没有。因此正则表达式要写得宽松一些使用[:]?来兼容两种冒号使用\s*来匹配0个或多个空白字符。最好的开发方式是先用你的脚本截几张图识别出原始文本然后基于这些真实的文本来设计和测试你的正则表达式。3.5 步骤四数据校验、保存与任务调度得到初步解析的数据后还需要进行校验和持久化。import pandas as pd from datetime import datetime import json def validate_and_save_data(holdings_list, output_formatcsv): 验证数据并保存到文件。 :param holdings_list: 提取出的持仓数据列表 :param output_format: 输出格式支持 csv 或 json if not holdings_list: print(未提取到任何有效持仓数据保存中止。) return # 基础校验检查必要字段 required_fields [name, code, position, price] for holding in holdings_list: for field in required_fields: if field not in holding: print(f警告持仓 {holding.get(code, 未知)} 缺少必要字段 {field}) # 可以赋予默认值如 holding[field] 0.0 # 添加时间戳 timestamp datetime.now().strftime(%Y-%m-%d %H:%M:%S) for holding in holdings_list: holding[update_time] timestamp # 转换为DataFrame便于查看和保存为CSV df pd.DataFrame(holdings_list) # 重排列顺序可选 column_order [update_time, code, name, position, cost, price, profit] # 只保留DataFrame中实际存在的列 existing_columns [col for col in column_order if col in df.columns] df df[existing_columns] print(\n提取到的持仓数据概览) print(df.to_string(indexFalse)) # 保存文件 filename_base fportfolio_{datetime.now().strftime(%Y%m%d_%H%M%S)} if output_format.lower() csv: filename f{filename_base}.csv df.to_csv(filename, indexFalse, encodingutf-8-sig) # utf-8-sig支持Excel直接打开 print(f数据已保存至{filename}) elif output_format.lower() json: filename f{filename_base}.json with open(filename, w, encodingutf-8) as f: # 使用orientrecords保存为字典列表格式 json.dump(holdings_list, f, ensure_asciiFalse, indent2) print(f数据已保存至{filename}) else: print(f不支持的输出格式{output_format}) # 使用示例 validate_and_save_data(holdings_data, output_formatcsv)最后我们可以使用Windows任务计划程序、macOS的launchd或Linux的cron将整个脚本设置为定时任务。一个简单的做法是写一个批处理文件.bat或Shell脚本在指定时间运行你的Python脚本。# 一个简单的run.bat文件内容示例Windows echo off cd /d D:\Your\Project\Path D:\Your\Python\Path\python.exe main.py pause然后将这个批处理文件添加到Windows任务计划程序中设定在每天15:05收盘后执行即可。4. 进阶优化与常见问题排查一套能跑起来的脚本只是开始要让它在长期内稳定可靠还需要考虑以下优化和应对各种异常情况。4.1 提升识别稳定性的关键技巧模板匹配定位如果持仓列表有固定的表头如“股票名称”、“持仓数量”可以使用OpenCV的模板匹配功能先找到表头在截图中的位置再根据相对位置动态计算数据行的区域。这样即使窗口位置稍有变动也能准确定位。import cv2 # 预先截取“股票名称”表头的小图作为模板 header_template cv2.imread(header_template.png, 0) # 在整体截图中匹配模板 result cv2.matchTemplate(screenshot_gray, header_template, cv2.TM_CCOEFF_NORMED) # 找到最佳匹配位置进而推算出数据区域多区域识别与合并持仓很多时可能需要滚动屏幕。可以设计脚本先识别当前可视区域截图、识别、滚动、再截图、再识别最后合并数据。pyautogui.scroll()可以模拟鼠标滚轮。验证与重试机制识别完成后对数据进行合理性验证。例如股票代码应为6位股价应为正数且在一定范围内如0-1000元。如果某次识别出的数据明显异常比如数量为0的持仓突然出现可以触发一次重试重新激活窗口、截图、识别。日志记录为脚本添加详细的日志功能记录每次运行的时间、截图的保存路径、识别出的原始文本、解析后的数据以及任何错误信息。这对于后期排查问题至关重要。4.2 典型问题与解决方案问题OCR识别率突然下降乱码增多。排查首先检查原始截图(raw_holding_screenshot.png)。是否出现了之前没有的弹窗如软件更新提示、新闻公告遮挡了持仓区域客户端界面主题或字体是否被更改解决增加弹窗检测。可以在截图前先对屏幕特定位置通常是弹窗出现的位置进行像素颜色检测如果发现异常颜色块则用pyautogui.hotkey(esc)或点击关闭按钮尝试关闭弹窗。或者调整预处理参数如尝试不同的二值化阈值方法。问题正则表达式匹配不到数据但OCR文本看起来正常。排查将OCR识别出的原始文本打印出来仔细对比和你编写的正则表达式。很可能出现了意料之外的空格、换行符如\r\n、全半角符号混用或者字段顺序发生了变化。解决在正则表达式中使用更通用的模式如用\s匹配任意空白用.*?进行非贪婪匹配。或者放弃复杂的单行匹配改用更灵活的方法先按行分割然后对每一行用多个简单的正则表达式分别去“搜索”各个字段而不是试图“匹配”整行。问题脚本在无人值守运行时客户端被其他窗口遮挡或最小化。解决在activate_client函数中加强容错。除了restore()和activate()还可以使用pyautogui.getWindowsWithTitle()循环查找并设置一个超时时间。确保窗口被成功激活并前置后再进行后续操作。问题持仓数量带逗号分隔符如“1,000”导致识别为字符串无法转换为数字。解决在数据转换前先使用字符串的replace(,, )方法去除逗号。4.3 安全与合规性提醒这是一个必须严肃对待的方面。本方案仅涉及对你自己电脑上已登录的、你有权操作的客户端软件进行自动化操作用于个人数据分析符合一般软件的用户协议中关于“自动化工具”的条款但务必自行仔细阅读你所使用券商客户端的最终用户许可协议。绝对不能用于以下用途对他人账户或未经授权的系统进行操作。进行高频、影响市场或违反券商规定的交易本方案仅用于数据获取不涉及交易指令。破解、绕过客户端的任何安全措施。将获取的数据用于商业用途或非法活动。整个过程中你的账户密码依然是通过手动输入或保存在本地的安全方式登录客户端脚本并不涉及密码的获取与传输这在安全性上比许多需要输入API密钥的方案更高。走通“自动截图图像识别”这条路你会发现它不仅仅是一个获取持仓数据的工具更是一套通用的、用于与任何没有开放API的桌面软件进行“自动化交互”的范式。你可以举一反三用它来抓取其他软件的报表数据、监控状态信息甚至是实现一些简单的自动化操作流程。它的核心魅力在于用相对简单的技术解决了一个实实在在的痛点。