首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python 零依赖读写 Windows 剪贴板:给“复制的内容”做体检 #WorkBuddy#

Python 零依赖读写 Windows 剪贴板:给“复制的内容”做体检 #WorkBuddy#

原创
作者头像
用户12727734
发布2026-09-01 09:58:30
发布2026-09-01 09:58:30
160
举报

复制粘贴是每天用几百次的操作,但有个问题很少有人注意:你复制完,其实并不知道剪贴板里装的是什么

我踩过一次实打实的坑:从网页上复制一段 Markdown 正文,粘进编辑器,6 个代码块全没了,行内代码变成一堆 <span style="color:rgb(...)"> 标签。原因是我从渲染后的预览画面复制的,不是从源文件——渲染视图会吞掉代码块标记、把行内代码转成 HTML 标签。

从那以后我养成一个习惯:粘贴之前,先给剪贴板做个体检

这篇文章讲怎么用 Python 标准库读写 Windows 剪贴板,不装任何第三方库。里面三个坑都是我实际踩出来的,尤其是第一个,查了两小时。

为什么不装 pyperclip

pyperclip 一行就能搞定剪贴板,但它要 pip 装,还要带上 pywin32 之类的依赖。下面这些场景装不了:

  • 公司电脑没管理员权限,pip 装包被拦
  • 内网离线环境
  • 你想把脚本发给别人,不想让他先配环境

而 ctypes 是 Python 自带的,直接调 Windows 的 API 就行,复制过去就能跑

一、读剪贴板:20 行搞定

Windows 剪贴板 API 就四个函数:OpenClipboardGetClipboardDataGlobalLockCloseClipboard。ctypes 直接调:

代码语言:python
复制
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])

两个细节:

  1. wstring_at(ptr) 必须在 GlobalUnlock 之前调用。解锁之后那块内存就不归你了,再读就是未定义行为——轻则乱码,重则直接崩。
  2. if not handle 这个判断别省。剪贴板空着、或者你复制的是一张图片时,GetClipboardData 会返回 0,后面 GlobalLock(0) 再交给 wstring_at 就是读 0 地址,程序当场崩掉,报错是 OSError: exception: access violation reading 0x0000000000000000。我第一次没写这个判断,就撞上了。

二、写剪贴板:真正的坑在签名声明

写入比读取多两步:申请内存(GlobalAlloc)、把内容拷进去,然后交给剪贴板。

代码语言:python
复制
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 都得声明。漏了它的表现是:

代码语言:txt
复制
ctypes.ArgumentError: argument 1: OverflowError: int too long to convert

读取那边同理,GetClipboardData 不声明 restype,返回的句柄也是截断的,wstring_at 一读就崩:

代码语言:txt
复制
OSError: exception: access violation reading 0x0000000049E85010

注意那个地址——高 32 位全是 0,就是被截断的铁证。

结论:用 ctypes 碰 Win32 句柄,每个函数都老老实实声明 argtypes 和 restype。

三、另外三个必须知道的坑

1. 剪贴板是易失的

验完不等于它还等着你。中间只要复制了别的东西,内容就被覆盖了。验完立刻粘贴,别去干别的。

2. Ctrl+C 失败时,剪贴板保留上一次的内容

做 UI 自动化时这是个经典陷阱:你先设置好新内容,再模拟 Ctrl+C 去复制编辑器里的东西。如果复制失败(焦点没进去、页面没响应),读回来的还是你刚设进去的内容——看起来"复制成功",其实是假阳性。

正确做法是读之前先清空剪贴板,读到 0 就知道复制真失败了:

代码语言:python
复制
def empty_clipboard():
    ctypes.windll.user32.OpenClipboard(0)
    ctypes.windll.user32.EmptyClipboard()
    ctypes.windll.user32.CloseClipboard()

3. OpenClipboard 可能被别的程序占着

别的程序正用着剪贴板时,OpenClipboard 会失败(返回 0)。稳妥写法是重试几次:

代码语言:python
复制
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

还有个隐蔽情况:某些剪贴板增强/监控类软件会在你复制时弹窗(比如"识别到股票代码""是否翻译"),弹窗会抢走键盘焦点,把后续的自动化操作全部打断。做自动化之前,这类功能最好先关掉。

四、实战:粘贴前的敏感信息体检

我要把日志、代码、配置文件贴到公开平台之前,都会先跑一遍这个脚本。它的核心原则是:只报结论,绝不打印剪贴板原文——万一里面是密码,打印出来就等于泄露了。

代码语言:python
复制
# -*- 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=', '&nbsp;', '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('✅ 没发现明显问题,可以粘贴')

实际跑起来的输出长这样:

代码语言:txt
复制
字符总数: 9208
代码块围栏数: 12 (偶数才正常)
⚠ 需要留意: 2 处
   - 长随机串 eyJhbG...9xK2m
   - 疑似 JWT 令牌

看到这个,我就知道:内容是对的(12 个围栏 = 6 个代码块都在),但里面混了令牌,得先处理再往外发。

扫描思路:用正则 [A-Za-z0-9_\-]{32,} 找长随机串(令牌、密钥、Session ID 基本都长这样),再单独查 eyJ(JWT 固定开头)、手机号、邮箱。命中一律只显示 前6...后4——够你定位是哪一个,又不至于把完整密钥打在屏幕上、留在终端日志里。

五、顺手还能干的事

  • 批量清洗文本:读出来 → 替换、去格式、删空行 → 写回去
  • 自动化填表:写入剪贴板 → 模拟 Ctrl+V 粘贴
  • 简易复制历史:定时轮询,内容变了就存一份
  • 粘贴前统一格式:比如把 Tab 换成 4 个空格,专治"粘进编辑器缩进全乱"

小结

ctypes 调 Win32 API 看着吓人,其实就那么几个函数。真正要记住的就三条:

  1. 每个函数都要声明 argtypes restype——64 位下句柄截断是最隐蔽的坑
  2. wstring_at 必须在 GlobalUnlock 之前调用
  3. 剪贴板是易失的,验完立刻用

完整代码上面都有了,拼起来保存成 剪贴板工具.py,直接 python 剪贴板工具.py 就能跑,不需要 pip 装任何东西。

环境:Windows 10/11 + Python 3.8 以上,64 位实测通过。

#WorkBuddy#

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 为什么不装 pyperclip
  • 一、读剪贴板:20 行搞定
  • 二、写剪贴板:真正的坑在签名声明
    • 这就是最要命的坑
  • 三、另外三个必须知道的坑
    • 1. 剪贴板是易失的
    • 2. Ctrl+C 失败时,剪贴板保留上一次的内容
    • 3. OpenClipboard 可能被别的程序占着
  • 四、实战:粘贴前的敏感信息体检
  • 五、顺手还能干的事
  • 小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档