复制粘贴是每天用几百次的操作,但有个问题很少有人注意:你复制完,其实并不知道剪贴板里装的是什么。
我踩过一次实打实的坑:从网页上复制一段 Markdown 正文,粘进编辑器,6 个代码块全没了,行内代码变成一堆 <span style="color:rgb(...)"> 标签。原因是我从渲染后的预览画面复制的,不是从源文件——渲染视图会吞掉代码块标记、把行内代码转成 HTML 标签。
从那以后我养成一个习惯:粘贴之前,先给剪贴板做个体检。
这篇文章讲怎么用 Python 标准库读写 Windows 剪贴板,不装任何第三方库。里面三个坑都是我实际踩出来的,尤其是第一个,查了两小时。
pyperclip 一行就能搞定剪贴板,但它要 pip 装,还要带上 pywin32 之类的依赖。下面这些场景装不了:
而 ctypes 是 Python 自带的,直接调 Windows 的 API 就行,复制过去就能跑。
Windows 剪贴板 API 就四个函数:OpenClipboard、GetClipboardData、GlobalLock、CloseClipboard。ctypes 直接调:
import ctypes
CF_UNICODETEXT = 13 # Windows 的 Unicode 文本格式编号
def read_clipboard():
ctypes.windll.user32.OpenClipboard(0)
handle = ctypes.windll.user32.GetClipboardData(CF_UNICODETEXT)
if not handle: # 剪贴板为空,或装的是图片等非文本
ctypes.windll.user32.CloseClipboard()
return ""
ptr = ctypes.windll.kernel32.GlobalLock(handle)
text = ctypes.wstring_at(ptr) # 必须在 Unlock 之前取值
ctypes.windll.kernel32.GlobalUnlock(handle)
ctypes.windll.user32.CloseClipboard()
return text
print(read_clipboard()[:50])两个细节:
wstring_at(ptr) 必须在 GlobalUnlock 之前调用。解锁之后那块内存就不归你了,再读就是未定义行为——轻则乱码,重则直接崩。if not handle 这个判断别省。剪贴板空着、或者你复制的是一张图片时,GetClipboardData 会返回 0,后面 GlobalLock(0) 再交给 wstring_at 就是读 0 地址,程序当场崩掉,报错是 OSError: exception: access violation reading 0x0000000000000000。我第一次没写这个判断,就撞上了。写入比读取多两步:申请内存(GlobalAlloc)、把内容拷进去,然后交给剪贴板。
import ctypes
CF_UNICODETEXT = 13
GMEM_MOVEABLE = 0x0002
def _declare():
"""声明 Win32 函数签名。64 位 Python 上不声明,句柄会被截断。"""
k = ctypes.windll.kernel32
u = ctypes.windll.user32
k.GlobalAlloc.argtypes = [ctypes.c_uint, ctypes.c_size_t]
k.GlobalAlloc.restype = ctypes.c_void_p
k.GlobalLock.argtypes = [ctypes.c_void_p]
k.GlobalLock.restype = ctypes.c_void_p
k.GlobalUnlock.argtypes = [ctypes.c_void_p]
u.GetClipboardData.argtypes = [ctypes.c_uint]
u.GetClipboardData.restype = ctypes.c_void_p
u.SetClipboardData.argtypes = [ctypes.c_uint, ctypes.c_void_p]
u.SetClipboardData.restype = ctypes.c_void_p
_declare() # 程序开头调一次
def set_clipboard(text):
kernel32 = ctypes.windll.kernel32
user32 = ctypes.windll.user32
user32.OpenClipboard(0)
user32.EmptyClipboard()
# UTF-16 编码,每个字符 2 字节,末尾还要留 1 个 \0
h = kernel32.GlobalAlloc(GMEM_MOVEABLE, (len(text) + 1) * 2)
ptr = kernel32.GlobalLock(h)
buf = (ctypes.c_wchar * (len(text) + 1)).from_address(ptr)
buf.value = text
kernel32.GlobalUnlock(h)
user32.SetClipboardData(CF_UNICODETEXT, h)
user32.CloseClipboard()
set_clipboard("你好,剪贴板")ctypes 默认把 Python 传进去的整数按 32 位处理。64 位 Python 上,GlobalAlloc 返回的是一个 64 位内存句柄,不声明 restype = c_void_p,高 32 位会被截掉,后面 SetClipboardData 拿到的就是个野指针。
它的表现特别坑人:不报错,但粘贴出来是空的,或者程序在某个奇怪的地方崩掉。
我踩的更绝——不只 GlobalAlloc/GlobalLock/SetClipboardData,连 GlobalUnlock 都得声明。漏了它的表现是:
ctypes.ArgumentError: argument 1: OverflowError: int too long to convert读取那边同理,GetClipboardData 不声明 restype,返回的句柄也是截断的,wstring_at 一读就崩:
OSError: exception: access violation reading 0x0000000049E85010注意那个地址——高 32 位全是 0,就是被截断的铁证。
结论:用 ctypes 碰 Win32 句柄,每个函数都老老实实声明 argtypes 和 restype。
验完不等于它还等着你。中间只要复制了别的东西,内容就被覆盖了。验完立刻粘贴,别去干别的。
做 UI 自动化时这是个经典陷阱:你先设置好新内容,再模拟 Ctrl+C 去复制编辑器里的东西。如果复制失败(焦点没进去、页面没响应),读回来的还是你刚设进去的内容——看起来"复制成功",其实是假阳性。
正确做法是读之前先清空剪贴板,读到 0 就知道复制真失败了:
def empty_clipboard():
ctypes.windll.user32.OpenClipboard(0)
ctypes.windll.user32.EmptyClipboard()
ctypes.windll.user32.CloseClipboard()别的程序正用着剪贴板时,OpenClipboard 会失败(返回 0)。稳妥写法是重试几次:
import time
def open_with_retry(times=5, delay=0.2):
for _ in range(times):
if ctypes.windll.user32.OpenClipboard(0):
return True
time.sleep(delay)
return False还有个隐蔽情况:某些剪贴板增强/监控类软件会在你复制时弹窗(比如"识别到股票代码""是否翻译"),弹窗会抢走键盘焦点,把后续的自动化操作全部打断。做自动化之前,这类功能最好先关掉。
我要把日志、代码、配置文件贴到公开平台之前,都会先跑一遍这个脚本。它的核心原则是:只报结论,绝不打印剪贴板原文——万一里面是密码,打印出来就等于泄露了。
# -*- coding: utf-8 -*-
"""剪贴板体检:只报结论,不打印原文"""
import ctypes, re, time
CF_UNICODETEXT = 13
def _declare():
k = ctypes.windll.kernel32
u = ctypes.windll.user32
k.GlobalLock.argtypes = [ctypes.c_void_p]
k.GlobalLock.restype = ctypes.c_void_p
k.GlobalUnlock.argtypes = [ctypes.c_void_p]
u.GetClipboardData.argtypes = [ctypes.c_uint]
u.GetClipboardData.restype = ctypes.c_void_p
_declare()
def read_clipboard():
if not ctypes.windll.user32.OpenClipboard(0):
return None
try:
h = ctypes.windll.user32.GetClipboardData(CF_UNICODETEXT)
if not h:
return ""
ptr = ctypes.windll.kernel32.GlobalLock(h)
text = ctypes.wstring_at(ptr)
ctypes.windll.kernel32.GlobalUnlock(h)
return text
finally:
ctypes.windll.user32.CloseClipboard()
def scan(text):
"""返回 (结论, 明细)。命中只显示 前6...后4,够定位不泄露。"""
hits = []
for m in re.finditer(r'[A-Za-z0-9_\-]{32,}', text):
s = m.group()
if set(s) <= set('-='): # 排除 ---- 这类分隔线
continue
hits.append('长随机串 ' + s[:6] + '...' + s[-4:])
if 'eyJ' in text: # JWT 的开头
hits.append('疑似 JWT 令牌')
for m in re.finditer(r'1[3-9]\d{9}', text):
hits.append('疑似手机号 ' + m.group()[:3] + '****' + m.group()[-4:])
for kw in ['<span style=', ' ', 'rgb(']:
if kw in text:
hits.append('渲染污染 ' + kw)
return hits
text = read_clipboard()
if text is None:
print('剪贴板被占用,稍后重试')
elif text == '':
print('剪贴板是空的,或装的不是文本')
else:
print('字符总数:', len(text))
print('代码块围栏数:', text.count('```'), '(偶数才正常)')
hits = scan(text)
if hits:
print('⚠ 需要留意:', len(hits), '处')
for h in hits[:10]:
print(' -', h)
else:
print('✅ 没发现明显问题,可以粘贴')实际跑起来的输出长这样:
字符总数: 9208
代码块围栏数: 12 (偶数才正常)
⚠ 需要留意: 2 处
- 长随机串 eyJhbG...9xK2m
- 疑似 JWT 令牌看到这个,我就知道:内容是对的(12 个围栏 = 6 个代码块都在),但里面混了令牌,得先处理再往外发。
扫描思路:用正则 [A-Za-z0-9_\-]{32,} 找长随机串(令牌、密钥、Session ID 基本都长这样),再单独查 eyJ(JWT 固定开头)、手机号、邮箱。命中一律只显示 前6...后4——够你定位是哪一个,又不至于把完整密钥打在屏幕上、留在终端日志里。
Ctrl+V 粘贴ctypes 调 Win32 API 看着吓人,其实就那么几个函数。真正要记住的就三条:
argtypes 和 restype——64 位下句柄截断是最隐蔽的坑wstring_at 必须在 GlobalUnlock 之前调用完整代码上面都有了,拼起来保存成 剪贴板工具.py,直接 python 剪贴板工具.py 就能跑,不需要 pip 装任何东西。
环境:Windows 10/11 + Python 3.8 以上,64 位实测通过。
#WorkBuddy#
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。