python实现全局快捷键和读取剪贴板(支持所有格式)

最近在做一个聚合图床,为了实现我文章的所有图片都自动上传到各个图床中然后服务器自动返回一个可用的链接(已经有一个大佬实现了,不过收费,所以我想自己来)。

于是想实现一个监听剪贴板文件然后响应全局快捷键的方法。在网上找了很久,大部分的实现都是用 Pillow 的 ImageGrab 的类方法来实现。通过 grabclipboard 来读取剪贴板然后用 isinstance(img, Image.Image) 来检测是否是图片。不过这样有一个非常大的弊端,就是只能识别 JPG 和 PNG 格式的图片。难道 gif 和 webp 就不配称为图片吗(滑稽)

经过多方查找,终于找到了一个适合我的方法。

全局快捷键响应

全局快捷键的实现我参考的 https://github.com/sqlsec/imageMD/blob/master/imageMD.py 虽然这样对于定制快捷键不友好,但是也将就能用。

本质是使用 pynput 库的 keyboard 的 Listener 来监听键盘动作。全局必定涉及到while循环,这也为后面的读取剪贴板带来了坑(也是我自己挖的坑),话不多说,上代码

from pynput.keyboard import Key, Listener


def clipboard_has_img():
    pass


def Key_press(key):
    # 监听按键
    if format(key) == 'Key.ctrl_l':
        q.put(1)
    if format(key) == 'Key.shift':
        q.put(2)
    if q.full():
        clipboard_has_img()  # 这里改成需要按键后响应的函数。


def Key_release(keys):
    # 监听释放
    if format(keys) == 'Key.ctrl_l':
        q.get_nowait()
    if format(keys) == 'Key.shift':
        q.get_nowait()
    if keys == Key.esc:
        return False


with Listener(on_press=Key_press, on_release=Key_release) as listener:
    listener.join()

其中,Key.ctrl_l 可以修改的值请参考: https://github.com/moses-palmer/pynput/blob/master/lib/pynput/keyboard/_base.py#L156

还有另外一种全局快捷键的写法,可以参考: https://github.com/xiebruce/PicUploader/blob/master/accessorys/PicUploaderHelper/PicUploaderHelper.py#L170 不过我觉得写起来不够优雅,而且只是我一个人用。所以没有采用这个方法。

读取剪贴板内容

这个才是重头戏,因为平时我是windows和linux同时使用,所以需要一个全平台(伪)的剪贴板读取方案。所以像win32clipboard的方案就不能使用了(也不好用)。网上绝大多数的方案都是使用 pillow 的 ImageGrab 方法。但是这个方法有局限性,识别的图片种类也不够。不够我还是把实现方法贴出来,如果正好有人需要呢。

def clipboard_has_img():
    img = ImageGrab.grabclipboard()
    if isinstance(img, Image.Image):
        # Save the image to disk
        img.save('tmp_image.png', "png")  # 保存为PNG或者jpg格式都可以
        print("保存成功!")
    else:
        print("没有图片!")
        pyperclip.copy("在剪贴板中没有发现图片数据,或者图片没有转换成功!")

后来发现Pyqt5的clipboard功能非常强大。能够识别复制的格式

我们需要用的就是 hasText 和 hasImage 这两个函数。

上代码:

from PyQt5.QtWidgets import QApplication

app = QApplication([])
clipboard = app.clipboard()
img_list = ['jpg', 'jpeg', 'gif', 'png', 'webp']


def on_clipboard_change():
    data = clipboard.mimeData()
    if data.hasImage():
        print("复制了图片")
    elif data.hasText():  
        tmp_text = data.text()
        if "/" in tmp_text:
            tmp_text = tmp_text.split("/")[-1]
            if "." in tmp_text:
                tmp_text = tmp_text.split(".")[-1]
                if tmp_text in img_list:
                    print("可能复制了图片")
                else:
                    print("复制了文件: ", data.text())
        else:
            print("复制的内容为: ", tmp_text)


clipboard.dataChanged.connect(on_clipboard_change)
app.exec_()

直接复制图片文件

直接复制非图片文件

直接打开文件进行复制

输出结果

从第二个输出结果可以看到,在复制了非图片文件后也可以根据文件名来判断是否是图片,这样就有更多可玩性。如下所示,根据后缀名判断是否是图片。

结合起来

前面的listener和 app.exec_() 都是死循环,这就为代码的编写带来了困难。首先我想到的就是多线程和多进程。于是我分别写了多线程和多线程的代码。

多线程

import threading
from pynput.keyboard import Key, Listener
from multiprocessing import Queue, Process
from PyQt5.QtWidgets import QApplication

q = Queue(2)

def clipboard_has_img():
    img = ImageGrab.grabclipboard()
    if isinstance(img, Image.Image):
        # Save the image to disk
        img.save('tmp_image.png', "png")  # 保存为PNG或者jpg格式都可以
        print("保存成功!")
    else:
        print("没有图片!")
        pyperclip.copy("在剪贴板中没有发现图片数据,或者图片没有转换成功!")


def Key_press(key):
    # 监听按键
    if format(key) == 'Key.ctrl_l':
        q.put(1)
    if format(key) == 'Key.shift':
        q.put(2)
    if q.full():
        clipboard_has_img()  # 这里改成需要按键后响应的函数。


def Key_release(keys):
    # 监听释放
    if format(keys) == 'Key.ctrl_l':
        q.get_nowait()
    if format(keys) == 'Key.shift':
        q.get_nowait()
    if keys == Key.esc:
        return False


def thread_method():
    """
    memory usage: 36MB
    :return:
    """
    t1 = threading.Thread(target=keyboard_listener)
    t1.start()
    app.exec_()
    t1.join()


# ----------gui----------------
app = QApplication([])
clipboard = app.clipboard()
img_list = ['jpg', 'jpeg', 'gif', 'png', 'webp', 'ico']

if __name__ == '__main__':
    thread_method()

多进程

方法一: 只开一个进程

from pynput.keyboard import Key, Listener
from multiprocessing import Queue, Process
from PyQt5.QtWidgets import QApplication

q = Queue(2)

def clipboard_has_img():
    img = ImageGrab.grabclipboard()
    if isinstance(img, Image.Image):
        # Save the image to disk
        img.save('tmp_image.png', "png")  # 保存为PNG或者jpg格式都可以
        print("保存成功!")
    else:
        print("没有图片!")
        pyperclip.copy("在剪贴板中没有发现图片数据,或者图片没有转换成功!")


def Key_press(key):
    # 监听按键
    if format(key) == 'Key.ctrl_l':
        q.put(1)
    if format(key) == 'Key.shift':
        q.put(2)
    if q.full():
        clipboard_has_img()  # 这里改成需要按键后响应的函数。


def Key_release(keys):
    # 监听释放
    if format(keys) == 'Key.ctrl_l':
        q.get_nowait()
    if format(keys) == 'Key.shift':
        q.get_nowait()
    if keys == Key.esc:
        return False
 
def multiprocess_method_1():
    """
    memory usage: 58MB
    :return:
    """
    p1 = Process(target=keyboard_listener)
    p1.start()
    app.exec_()
    p1.join()

# ----------gui----------------
app = QApplication([])
clipboard = app.clipboard()
img_list = ['jpg', 'jpeg', 'gif', 'png', 'webp', 'ico']

if __name__ == '__main__':
    multiprocess_method_1()

方法二: 开两个进程

from pynput.keyboard import Key, Listener
from multiprocessing import Queue, Process
from PyQt5.QtWidgets import QApplication

q = Queue(2)

def clipboard_has_img():
    img = ImageGrab.grabclipboard()
    if isinstance(img, Image.Image):
        # Save the image to disk
        img.save('tmp_image.png', "png")  # 保存为PNG或者jpg格式都可以
        print("保存成功!")
    else:
        print("没有图片!")
        pyperclip.copy("在剪贴板中没有发现图片数据,或者图片没有转换成功!")


def Key_press(key):
    # 监听按键
    if format(key) == 'Key.ctrl_l':
        q.put(1)
    if format(key) == 'Key.shift':
        q.put(2)
    if q.full():
        clipboard_has_img()  # 这里改成需要按键后响应的函数。


def Key_release(keys):
    # 监听释放
    if format(keys) == 'Key.ctrl_l':
        q.get_nowait()
    if format(keys) == 'Key.shift':
        q.get_nowait()
    if keys == Key.esc:
        return False
        
def multiprocess_method_2():
    """
    memory usage: 80MB
    :return:
    """
    p1 = Process(target=keyboard_listener)
    p2 = Process(target=app_run)
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    
# ----------gui----------------
app = QApplication([])
clipboard = app.clipboard()
img_list = ['jpg', 'jpeg', 'gif', 'png', 'webp', 'ico']

if __name__ == '__main__':
    multiprocess_method_2()

三种方法内存占用依次上升,多线程占用最低,只有36MB,而开两个进程占用有80MB。两倍多的差距。