'''
一个统信linux中运行的一个名叫"剪切板循环粘贴"的python程序,程序会自动保存设置,用于读取剪切板的内容并输出,如果是文本则以硬回车为分组且不忽略空段落,如果是表格则以单元格为分组且不忽略空单元。通过tktinter输入参数,界面尺寸300*250,底色淡黄色,一键处理的按键是糖果绿色,字体判断:字号12号。如果是linux字体fangsong ti,否则字体仿宋。有删除选项,默认选中。有开始延时输入框默认0.3s。有间隔延时输入框默认0.2s。有间隔操作输入框默认回车键。有浅绿开始键。
程序开始运行时,等待Ctrl+B为触发功能,执行以下操作命令,执行完毕后自动进入下一次等待触发一直到循环次数结束。具体是打开资源管理器触发功能后,自动将选中的文件通过该F2进入改名状态
功能1:输入每组的间隔,默认值0.2s,输入默认循环次数,默认循环次数为剪切板中的表格内容数量(包括空单元格)。
功能2:通过读取剪切板中的段落或表格内容,按单元格逐个执行粘贴判断及间隔时间及回车及间隔时间,实现各个组的连续输入。默认是:首先执行执行删除,然后按照间隔延时时间(默认0.2秒),然后执行粘贴判断,然后按照间隔延时时间(默认0.2秒),然后执行"回车键",然后按照间隔延时(默认0.2秒),然后执行下一次循环。
粘贴判断:如果段落或单元格有内容则正常执行粘贴操作,是空段落或空单元格则判断:如果勾选删除选项则执行删除,否则无操作,直接执行间隔延时时间(默认0.2秒)。
功能3:可以点击浅绿开始键,则也能开始连点,首先执行开始延时之后执行操作命令,点击浅红结束键或者再按一次Ctrl+B,则自动结束。
'''
#!/usr/bin/env python3
-*- coding: utf-8 -*-
"""
剪切板循环粘贴工具
功能:读取剪切板内容(文本按段落分组,表格按单元格分组),
按组循环执行粘贴、延时、回车操作。
触发方式:Ctrl+B 或点击开始按钮
"""
import tkinter as tk
from tkinter import messagebox
import json
import os
import threading
import time
import sys
import csv
import io
import re
尝试导入键盘监听库
try:
from pynput import keyboard
PYNPUT_AVAILABLE = True
except ImportError:
PYNPUT_AVAILABLE = False
print("提示: 未安装pynput库,全局热键Ctrl+B可能无法使用。")
print("请运行: pip install pynput")
尝试导入剪切板库
try:
import pyperclip
PYPERCLIP_AVAILABLE = True
except ImportError:
PYPERCLIP_AVAILABLE = False
print("提示: 未安装pyperclip库,剪切板功能无法使用。")
print("请运行: pip install pyperclip")
CONFIG_FILE = "clipboard_looper_config.json"
默认配置
DEFAULT_CONFIG = {
"window_size": "320x300",
"window_x": None,
"window_y": None,
"delete_empty": True,
"start_delay": 0.3,
"interval_delay": 0.2,
"action_key": "enter",
"loop_count": 0,
}
def load_config():
"""加载配置文件"""
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
cfg = json.load(f)
for key, value in DEFAULT_CONFIG.items():
if key not in cfg:
cfg[key] = value
return cfg
except Exception as e:
print(f"加载配置失败: {e}")
return DEFAULT_CONFIG.copy()
return DEFAULT_CONFIG.copy()
def save_config(cfg):
"""保存配置文件"""
try:
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"保存配置失败: {e}")
字体设置
if sys.platform.startswith("linux"):
DEFAULT_FONT = ("Fangsong Ti", 12)
else:
DEFAULT_FONT = ("仿宋", 12)
颜色
COLOR_BG = "#FAF0D7"
COLOR_BUTTON_START = "#98FB98"
COLOR_BUTTON_STOP = "#FF9999"
class ClipboardLooper:
def init(self, master):
self.master = master
self.config = load_config()
master.title("剪切板循环粘贴工具")
master.configure(bg=COLOR_BG)
master.geometry(self.config.get("window_size", "320x300"))
if self.config.get("window_x") and self.config.get("window_y"):
master.geometry(f"+{self.config['window_x']}+{self.config['window_y']}")
self.running = False
self.loop_thread = None
self.create_widgets()
master.protocol("WM_DELETE_WINDOW", self.on_closing)
self.register_hotkey()
def create_widgets(self):
"""创建界面组件"""
main_frame = tk.Frame(self.master, bg=COLOR_BG)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
删除空项选项
self.delete_var = tk.BooleanVar(value=self.config.get("delete_empty", True))
delete_cb = tk.Checkbutton(
main_frame, text="删除空段落/空单元格",
variable=self.delete_var, bg=COLOR_BG, font=DEFAULT_FONT
)
delete_cb.pack(anchor=tk.W, pady=2)
开始延时
delay_frame = tk.Frame(main_frame, bg=COLOR_BG)
delay_frame.pack(fill=tk.X, pady=5)
tk.Label(delay_frame, text="开始延时(秒):", bg=COLOR_BG, font=DEFAULT_FONT).pack(side=tk.LEFT)
self.start_delay_var = tk.StringVar(value=str(self.config.get("start_delay", 0.3)))
tk.Entry(delay_frame, textvariable=self.start_delay_var, width=8, font=DEFAULT_FONT).pack(side=tk.LEFT, padx=5)
间隔延时
interval_frame = tk.Frame(main_frame, bg=COLOR_BG)
interval_frame.pack(fill=tk.X, pady=5)
tk.Label(interval_frame, text="间隔延时(秒):", bg=COLOR_BG, font=DEFAULT_FONT).pack(side=tk.LEFT)
self.interval_delay_var = tk.StringVar(value=str(self.config.get("interval_delay", 0.2)))
tk.Entry(interval_frame, textvariable=self.interval_delay_var, width=8, font=DEFAULT_FONT).pack(side=tk.LEFT, padx=5)
间隔操作
action_frame = tk.Frame(main_frame, bg=COLOR_BG)
action_frame.pack(fill=tk.X, pady=5)
tk.Label(action_frame, text="间隔操作:", bg=COLOR_BG, font=DEFAULT_FONT).pack(side=tk.LEFT)
self.action_var = tk.StringVar(value=self.config.get("action_key", "enter"))
action_combo = tk.OptionMenu(action_frame, self.action_var, "enter", "tab", "space", "none")
action_combo.config(font=DEFAULT_FONT, bg=COLOR_BG)
action_combo.pack(side=tk.LEFT, padx=5)
循环次数
loop_frame = tk.Frame(main_frame, bg=COLOR_BG)
loop_frame.pack(fill=tk.X, pady=5)
tk.Label(loop_frame, text="循环次数(0=自动):", bg=COLOR_BG, font=DEFAULT_FONT).pack(side=tk.LEFT)
self.loop_count_var = tk.StringVar(value=str(self.config.get("loop_count", 0)))
tk.Entry(loop_frame, textvariable=self.loop_count_var, width=8, font=DEFAULT_FONT).pack(side=tk.LEFT, padx=5)
预览区域
preview_frame = tk.LabelFrame(main_frame, text="剪切板预览", bg=COLOR_BG, font=DEFAULT_FONT)
preview_frame.pack(fill=tk.BOTH, expand=True, pady=5)
self.preview_text = tk.Text(preview_frame, height=6, width=35, font=DEFAULT_FONT, wrap=tk.WORD)
self.preview_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
刷新预览按钮
refresh_btn = tk.Button(
preview_frame, text="刷新预览", bg=COLOR_BUTTON_START,
font=DEFAULT_FONT, command=self.refresh_preview
)
refresh_btn.pack(pady=2)
提示信息
tip_label = tk.Label(
main_frame,
text="提示: 按 Ctrl+B 开始/停止循环粘贴\n\n"
"使用说明:\n"
"1. 复制文本或表格内容到剪切板\n"
"2. 选中文件后按F2进入改名状态\n"
"3. 按Ctrl+B开始自动粘贴\n"
"4. 再次按Ctrl+B或点击停止按钮结束",
bg=COLOR_BG, font=DEFAULT_FONT, justify=tk.LEFT
)
tip_label.pack(pady=5)
按钮框架
button_frame = tk.Frame(main_frame, bg=COLOR_BG)
button_frame.pack(fill=tk.X, pady=10)
self.start_btn = tk.Button(
button_frame, text="开始", bg=COLOR_BUTTON_START,
font=DEFAULT_FONT, command=self.start_loop, width=8
)
self.start_btn.pack(side=tk.LEFT, padx=10)
self.stop_btn = tk.Button(
button_frame, text="停止", bg=COLOR_BUTTON_STOP,
font=DEFAULT_FONT, command=self.stop_loop, width=8
)
self.stop_btn.pack(side=tk.LEFT, padx=10)
状态显示
self.status_var = tk.StringVar(value="就绪")
status_label = tk.Label(
main_frame, textvariable=self.status_var,
bg=COLOR_BG, font=DEFAULT_FONT, fg="blue"
)
status_label.pack(pady=5)
初始预览
self.refresh_preview()
def refresh_preview(self):
"""刷新剪切板预览"""
if not PYPERCLIP_AVAILABLE:
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(1.0, "pyperclip未安装")
return
content = pyperclip.paste()
if content:
显示前500字符
preview = content[:500]
if len(content) > 500:
preview += "..."
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(1.0, preview)
else:
self.preview_text.delete(1.0, tk.END)
self.preview_text.insert(1.0, "剪切板为空")
def register_hotkey(self):
"""注册全局热键Ctrl+B"""
if not PYNPUT_AVAILABLE:
self.status_var.set("提示: 未安装pynput,热键不可用")
return
try:
def on_activate():
self.master.after(0, self.toggle_loop)
self.hotkey = keyboard.GlobalHotKeys({
'<ctrl>+b': on_activate
})
self.hotkey.start()
except Exception as e:
print(f"注册热键失败: {e}")
self.status_var.set("热键注册失败")
def toggle_loop(self):
"""切换运行状态"""
if self.running:
self.stop_loop()
else:
self.start_loop()
def remove_surrounding_quotes(self, text):
"""删除开头和结尾的双引号(保留中间的双引号)"""
if not text or text == "":
return ""
result = text
循环删除开头的双引号
while result and result[0] == '"':
result = result[1:]
循环删除结尾的双引号
while result and result[-1] == '"':
result = result[:-1]
return result
def clean_cell_for_paste(self, cell):
"""清理单元格内容,准备粘贴(删除双引号,保留换行符)"""
if cell is None:
return ""
cell_str = str(cell)
第一步:删除开头和结尾的双引号(处理CSV包裹标记)
cell_str = self.remove_surrounding_quotes(cell_str)
第二步:处理内部的双引号转义(CSV中两个双引号代表一个)
cell_str = cell_str.replace('""', '"')
第三步:再次删除可能残留的开头结尾双引号
cell_str = self.remove_surrounding_quotes(cell_str)
第四步:如果单元格内容以引号开头但内容很短,可能是空单元格标记
if cell_str == '"' or cell_str == '""':
cell_str = ""
return cell_str
def parse_table_with_quotes(self, content):
"""正确解析包含引号和换行符的表格内容"""
cells = []
current_cell = ""
in_quotes = False
i = 0
length = len(content)
while i < length:
char = content[i]
if char == '"':
检查是否是转义引号(两个连续引号)
if i + 1 < length and content[i + 1] == '"':
两个连续引号是转义,代表一个引号字符
current_cell += '"'
i += 2
continue
else:
切换引号状态(开始或结束引号区域)
in_quotes = not in_quotes
i += 1
continue
if char == '\t' and not in_quotes:
制表符分隔单元格(在引号外)
cleaned = self.clean_cell_for_paste(current_cell)
cells.append(cleaned)
current_cell = ""
i += 1
continue
if char in ('\n', '\r', '\x0a', '\x0d') and not in_quotes:
换行符在引号外:表格模式下,换行符表示行结束
将当前单元格添加到列表中
cleaned = self.clean_cell_for_paste(current_cell)
cells.append(cleaned)
current_cell = ""
处理Windows换行符(\r\n)
if char == '\r' and i + 1 < length and content[i + 1] == '\n':
i += 2
else:
i += 1
continue
普通字符(包括引号内的换行符),添加到当前单元格
current_cell += char
i += 1
添加最后一个单元格
cleaned = self.clean_cell_for_paste(current_cell)
cells.append(cleaned)
移除末尾可能产生的空单元格(如果原内容末尾有换行符)
while len(cells) > 1 and cells[-1] == "" and content.rstrip('\n\r').endswith(('\n', '\r')):
保留由连续制表符产生的空单元格,只移除末尾多余的
pass
return cells
def parse_clipboard_content(self):
"""解析剪切板内容,返回分组列表"""
if not PYPERCLIP_AVAILABLE:
messagebox.showerror("错误", "未安装pyperclip库,无法读取剪切板")
return []
content = pyperclip.paste()
if not content:
return []
lines = content.splitlines()
has_tabs = any('\t' in line for line in lines if line.strip())
has_quotes = any('"' in line for line in lines)
if has_tabs or has_quotes:
表格模式(制表符分隔或CSV格式)
使用自定义解析器正确处理引号内的换行符
cells = self.parse_table_with_quotes(content)
return cells
else:
文本模式:按段落分组(保留空行)
paragraphs = []
for line in content.splitlines():
paragraphs.append(line)
return paragraphs
def send_key(self, key_name):
"""发送按键"""
try:
import pyautogui
if key_name == "enter":
pyautogui.press('enter')
elif key_name == "tab":
pyautogui.press('tab')
elif key_name == "space":
pyautogui.press('space')
return True
except ImportError:
try:
import keyboard
if key_name == "enter":
keyboard.press_and_release('enter')
elif key_name == "tab":
keyboard.press_and_release('tab')
elif key_name == "space":
keyboard.press_and_release('space')
return True
except ImportError:
self.status_var.set("错误: 请安装pyautogui或keyboard库")
return False
def paste_text(self, text):
"""粘贴文本"""
if not PYPERCLIP_AVAILABLE:
return False
清理文本:删除开头和结尾的双引号
if text and text.startswith('"'):
text = text[1:]
if text and text.endswith('"'):
text = text[:-1]
保存原剪切板内容
old_content = pyperclip.paste()
设置新内容并粘贴
pyperclip.copy(text)
try:
import pyautogui
pyautogui.hotkey('ctrl', 'v')
except ImportError:
try:
import keyboard
keyboard.press_and_release('ctrl+v')
except ImportError:
self.status_var.set("错误: 请安装pyautogui或keyboard库")
return False
恢复原剪切板内容
pyperclip.copy(old_content)
return True
def delete_current(self):
"""删除当前选中的内容(按Delete键)"""
try:
import pyautogui
pyautogui.press('delete')
except ImportError:
try:
import keyboard
keyboard.press_and_release('delete')
except ImportError:
return False
return True
def run_loop(self):
"""运行循环粘贴主逻辑"""
解析剪切板内容
items = self.parse_clipboard_content()
if not items:
self.master.after(0, lambda: self.status_var.set("错误: 剪切板为空或无法解析"))
self.master.after(0, self.stop_loop)
return
显示解析结果
item_count = len(items)
non_empty_count = len([i for i in items if i and i.strip()])
self.master.after(0, lambda: self.status_var.set(f"解析完成: 共{item_count}项, 非空{non_empty_count}项"))
确定循环次数
try:
loop_count = int(self.loop_count_var.get())
except ValueError:
loop_count = 0
if loop_count <= 0:
loop_count = len(items)
获取参数
try:
interval_delay = float(self.interval_delay_var.get())
except ValueError:
interval_delay = 0.2
action_key = self.action_var.get()
delete_empty = self.delete_var.get()
self.master.after(0, lambda: self.status_var.set(f"开始循环,共{loop_count}项"))
for i in range(loop_count):
if not self.running:
break
获取当前项
current_item = items[i % len(items)]
显示当前进度
preview = current_item[:25] + "..." if len(current_item) > 25 else current_item
preview = preview.replace('\n', '↵').replace('\r', '↵')
self.master.after(0, lambda idx=i+1, total=loop_count, p=preview:
self.status_var.set(f"处理 {idx}/{total}: [{p}]"))
步骤1:首先执行删除
self.delete_current()
步骤2:按照间隔延时时间
if not self.running:
break
time.sleep(interval_delay)
步骤3:执行粘贴判断
is_empty = (current_item == "" or current_item is None)
if not is_empty:
有内容则粘贴
self.paste_text(current_item)
如果是空内容,不再执行额外删除(已经删除过了)
if not self.running:
break
time.sleep(interval_delay)
if action_key != "none" and self.running:
self.send_key(action_key)
if not self.running:
break
time.sleep(interval_delay)
if self.running:
self.master.after(0, lambda: self.status_var.set("循环完成"))
self.master.after(0, self.stop_loop)
def start_loop(self):
"""开始循环粘贴"""
if self.running:
return
try:
start_delay = float(self.start_delay_var.get())
except ValueError:
start_delay = 0.3
self.running = True
self.start_btn.config(state=tk.DISABLED)
self.status_var.set(f"开始延时 {start_delay} 秒...")
def delayed_start():
if self.running:
self.loop_thread = threading.Thread(target=self.run_loop, daemon=True)
self.loop_thread.start()
self.master.after(int(start_delay * 1000), delayed_start)
def stop_loop(self):
"""停止循环粘贴"""
self.running = False
self.start_btn.config(state=tk.NORMAL)
self.status_var.set("已停止")
def on_closing(self):
"""窗口关闭时保存设置"""
self.config["window_size"] = self.master.geometry().split('+')[0]
self.config["window_x"] = self.master.winfo_x()
self.config["window_y"] = self.master.winfo_y()
self.config["delete_empty"] = self.delete_var.get()
self.config["start_delay"] = float(self.start_delay_var.get() or 0.3)
self.config["interval_delay"] = float(self.interval_delay_var.get() or 0.2)
self.config["action_key"] = self.action_var.get()
self.config["loop_count"] = int(self.loop_count_var.get() or 0)
save_config(self.config)
self.running = False
if hasattr(self, 'hotkey') and self.hotkey:
try:
self.hotkey.stop()
except:
pass
self.master.destroy()
def main():
root = tk.Tk()
app = ClipboardLooper(root)
root.mainloop()
if name == "main":
main()