新增截图OCR识别昵称功能

- GUI新增「从截图识别」按钮
- 截屏后用EasyOCR识别文字(GPU=False)
- 提取QQ昵称模式:[头衔]用户名、@用户名、行首用户名
- 自动去重并入库config.json的qq_nicknames
- Windows OCR作为后备方案
This commit is contained in:
youpaishidifu
2026-08-10 10:34:12 +08:00
parent d8734021bb
commit 16c6986bca
+181 -1
View File
@@ -261,7 +261,13 @@ class MsgBotApp(ctk.CTk):
nick_btn_frame, text="刷新", width=56,
command=self._refresh_nick_list
)
self._refresh_nick_btn.pack(side="left")
self._refresh_nick_btn.pack(side="left", padx=(0, 4))
self._ocr_nick_btn = ctk.CTkButton(
nick_btn_frame, text="📸 从截图识别", width=100,
command=self._ocr_nicknames_from_screenshot
)
self._ocr_nick_btn.pack(side="left")
# 初始加载昵称列表
self._refresh_nick_list()
@@ -423,6 +429,180 @@ class MsgBotApp(ctk.CTk):
except Exception as e:
self._log(f"⚠ 删除失败: {e}")
def _ocr_nicknames_from_screenshot(self):
"""截图OCR识别昵称:最小化窗口→截屏→OCR→提取昵称→入库"""
self._log("📸 正在截屏,请确保QQ聊天窗口可见...")
self._ocr_nick_btn.configure(state="disabled", text="截屏中...")
def _do_ocr():
try:
import time
time.sleep(1.5) # 等待用户切换到QQ窗口
# 截取全屏
screenshot = self._capture_screen()
if screenshot is None:
self._log_queue.put("⚠ 截屏失败")
return
self._log_queue.put("🔍 OCR识别中...")
# OCR识别
try:
import easyocr
reader = easyocr.Reader(['ch_sim', 'en'], gpu=False, verbose=False)
results = reader.readtext(screenshot, detail=0)
except Exception as e:
self._log_queue.put(f"⚠ EasyOCR失败: {e},尝试Windows OCR...")
results = self._win_ocr(screenshot)
if not results:
self._log_queue.put("⚠ OCR未识别到任何文字")
return
self._log_queue.put(f"📝 OCR识别到 {len(results)} 段文字")
# 从OCR结果中提取昵称
nicks_found = self._extract_nicknames_from_ocr(results)
if not nicks_found:
self._log_queue.put("⚠ 未检测到QQ昵称模式([头衔]用户名 或 @用户名)")
return
# 添加到配置
self.cfg = load_config()
existing = self.cfg.setdefault("qq_nicknames", [])
added = []
for n in nicks_found:
if n not in existing:
existing.append(n)
added.append(n)
if added:
save_config(self.cfg)
self._refresh_nick_list()
self._log_queue.put(f"✅ 新增 {len(added)} 个昵称: {', '.join(added)}")
else:
self._log_queue.put(f"ℹ 识别到的昵称已全部存在: {', '.join(nicks_found)}")
except Exception as e:
self._log_queue.put(f"⚠ 截图OCR异常: {e}")
finally:
# 恢复按钮
self.after(0, lambda: self._ocr_nick_btn.configure(
state="normal", text="📸 从截图识别"))
threading.Thread(target=_do_ocr, daemon=True).start()
def _capture_screen(self):
"""使用Windows API截取全屏,返回PIL Image"""
try:
import win32gui
import win32ui
import win32con
from PIL import Image
# 获取屏幕尺寸
screen_w = win32gui.GetSystemMetrics(win32con.SM_CXSCREEN)
screen_h = win32gui.GetSystemMetrics(win32con.SM_CYSCREEN)
# 创建设备上下文
hdesktop = win32gui.GetDesktopWindow()
hdc = win32gui.GetDC(hdesktop)
mfcDC = win32ui.CreateDCFromHandle(hdc)
saveDC = mfcDC.CreateCompatibleDC()
# 创建位图
saveBitMap = win32ui.CreateBitmap()
saveBitMap.CreateCompatibleBitmap(mfcDC, screen_w, screen_h)
saveDC.SelectObject(saveBitMap)
# 复制屏幕
saveDC.BitBlt((0, 0), (screen_w, screen_h), mfcDC, (0, 0), win32con.SRCCOPY)
# 转为PIL Image
bmpinfo = saveBitMap.GetInfo()
bmpstr = saveBitMap.GetBitmapBits(True)
img = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
bmpstr, 'raw', 'BGRX', 0, 1)
# 清理
win32gui.DeleteObject(saveBitMap.GetHandle())
saveDC.DeleteDC()
mfcDC.DeleteDC()
win32gui.ReleaseDC(hdesktop, hdc)
return img
except Exception as e:
logger.error(f"截屏失败: {e}")
return None
def _win_ocr(self, image):
"""Windows OCR后备方案(无需GPU)"""
try:
import io
from PIL import Image
# 保存为临时文件
tmp_path = os.path.join(os.environ.get('TEMP', '.'), '_ocr_temp.png')
image.save(tmp_path)
# 使用Windows OCR(需要winrt)
from winrt.windows.media.ocr import OcrEngine
from winrt.windows.graphics.imaging import BitmapDecoder
from winrt.windows.storage import StorageFile
import asyncio
async def _do_ocr():
file = await StorageFile.get_file_from_path_async(tmp_path)
stream = await file.open_read_async()
decoder = await BitmapDecoder.create_async(stream)
bitmap = await decoder.get_software_bitmap_async()
engine = OcrEngine.try_create_from_user_profile_languages()
result = await engine.recognize_async(bitmap)
return result.text
text = asyncio.run(_do_ocr())
# 按行分割
return [line.strip() for line in text.split('\n') if line.strip()]
except Exception as e:
logger.error(f"Windows OCR失败: {e}")
return []
def _extract_nicknames_from_ocr(self, ocr_results):
"""从OCR识别结果中提取QQ昵称模式"""
nicks = set()
# 合并所有识别文本
full_text = '\n'.join(ocr_results) if isinstance(ocr_results, list) else str(ocr_results)
# 模式1: [头衔]用户名 或 【头衔】用户名
for m in re.finditer(r'[\[【]([^\]】]+)[】\]]\s*([A-Za-z0-9_\u4e00-\u9fff]{2,30})', full_text):
nick = m.group(2).strip()
if len(nick) >= 2:
nicks.add(nick)
# 模式2: @用户名
for m in re.finditer(r'@([A-Za-z0-9_\u4e00-\u9fff]{2,30})', full_text):
nick = m.group(1).strip()
if len(nick) >= 2:
nicks.add(nick)
# 模式3: 行首的纯中文/英文用户名(2-15字符),后跟消息内容
# 这个模式比较宽松,需要人工确认
for line in ocr_results if isinstance(ocr_results, list) else full_text.split('\n'):
line = line.strip()
if not line:
continue
# 行首用户名:2-15字符的中英文,后跟逗号/冒号/空格+内容
m = re.match(r'^([A-Za-z0-9_\u4e00-\u9fff]{2,15})\s*[,,::]\s*.+', line)
if m:
nick = m.group(1).strip()
# 过滤常见非昵称词
skip_words = {'你好', '请问', '怎么', '什么', '为什么', '多少', '在哪', '下载',
'安装', '配置', '设置', '使用', '更新', '版本', '错误', '问题'}
if nick not in skip_words:
nicks.add(nick)
return list(nicks)
# ── 消息处理 ──
def _on_new_message(self, platform: str, msg: str):