【办公工具】用于文本格式处理或文件格式处理的一些Python脚本
任何关于代码的改进不胜感激。
脚本处理后的结果记得核对一下。
目录
写一个Python脚本删除一个.py文件的所有注释
点击查看代码
// 也可以尝试一下 https://python-minifier.com/
import re
def remove_comments(file_path):
with open(file_path, 'r') as file:
content = file.read()
# First, find and store string assignments
protected_strings = {}
counter = 0
def protect_string_assignments(match):
nonlocal counter
var_name, string_content = match.groups()
key = f'PROTECTED_STRING_{counter}'
protected_strings[key] = match.group(0)
counter += 1
return key
# Protect strings that are part of assignments
protected_content = re.sub(
r'([a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\')',
protect_string_assignments,
content
)
# Remove docstring comments (triple-quoted strings not part of assignments)
cleaned_content = re.sub(
r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'',
'',
protected_content
)
# Remove single-line comments and empty lines
lines = []
for line in cleaned_content.split('\n'):
# Remove inline comments
line = re.sub(r'#.*$', '', line)
if line.strip():
lines.append(line)
# Restore protected strings
final_content = '\n'.join(lines)
for key, value in protected_strings.items():
final_content = final_content.replace(key, value)
# Write back to file
with open(file_path, 'w') as file:
file.write(final_content)
# Example usage:
remove_comments('your_script.py')
行内公式使用$$而不是$$$$
使用cloud-document-converter下载的markdown文件中,行内公式显示不正常,你可能需要这个脚本。
点击查看代码
import re
from datetime import datetime
def replace_formula_delimiters_in_md(input_file):
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = f"{input_file.rsplit('.', 1)[0]}_{timestamp}.md"
with open(input_file, 'r', encoding='utf-8') as file:
lines = file.readlines()
new_lines = []
pattern = r'^\${2}(.+?)\${2}$' # Matches lines with only $$content$$
replace_pattern = r'\${2}(.+?)\${2}' # Matches $$content$$ anywhere
replacement_count = 0
print("Changes to be made:")
for line in lines:
stripped_line = line.strip()
if re.match(pattern, stripped_line):
print(f"Keeping unchanged: {stripped_line}")
new_lines.append(line)
else:
matches = list(re.finditer(replace_pattern, line))
if matches:
replacement_count += len(matches)
new_line = re.sub(replace_pattern, r'$\1$', line)
print(f"Changing: {line.strip()} → {new_line.strip()}")
new_lines.append(new_line)
else:
new_lines.append(line)
with open(output_file, 'w', encoding='utf-8') as file:
file.writelines(new_lines)
print(f"\nCreated new file: {output_file}")
print(f"Converted {replacement_count} formulas from $$ to $")
except FileNotFoundError:
print(f"Error: File '{input_file}' not found")
except Exception as e:
print(f"An error occurred: {str(e)}")
if __name__ == "__main__":
input_md_file = "your_file.md"
replace_formula_delimiters_in_md(input_md_file)
给文字加上反爬水印
点击查看代码
import random
import re
# Base templates for the reminder sentence variations
templates = [
"【防盗链提醒:爬虫是吧?原贴在:<a href=\"{url}\" target=\"_blank\">{url}</a>】",
"【防爬提示:你是爬虫吗?原文地址:<a href=\"{url}\" target=\"_blank\">{url}</a>】",
"【反爬警告:爬虫别乱来!原帖链接:<a href=\"{url}\" target=\"_blank\">{url}</a>】",
"【防盗链通知:爬虫注意了,原文在:<a href=\"{url}\" target=\"_blank\">{url}</a>】",
"【反爬提醒:是爬虫的话,原文见:<a href=\"{url}\" target=\"_blank\">{url}</a>】",
"【防爬虫提示:爬虫请止步,原文链接:<a href=\"{url}\" target=\"_blank\">{url}</a>】",
"【盗链警告:爬虫你好,原文地址:<a href=\"{url}\" target=\"_blank\">{url}</a>】",
"【防爬声明:爬虫请看,原文出处:<a href=\"{url}\" target=\"_blank\">{url}</a>】",
"【反盗链提醒:爬虫注意,原链接:<a href=\"{url}\" target=\"_blank\">{url}</a>】",
"【爬虫警告:想爬吗?原文在这:<a href=\"{url}\" target=\"_blank\">{url}</a>】",
]
# URL to use
url = "https://www.cnblogs.com/yhm138/p/18549168"
# Function to read content and split into sentences
def split_into_sentences(content):
# Split on sentence endings, keeping the delimiter
sentences = re.split(r'([.!?。!?\n])\s+', content)
# Combine delimiter with preceding text to form complete sentences
result = []
for i in range(0, len(sentences) - 1, 2):
result.append(sentences[i] + (sentences[i + 1] if i + 1 < len(sentences) else ""))
if len(sentences) % 2 != 0: # Handle odd number of splits
result.append(sentences[-1])
return [s.strip() for s in result if s.strip()] # Remove empty lines and strip whitespace
# Read input markdown file
input_file = "original_mdfile.md"
try:
with open(input_file, "r", encoding="utf-8") as f:
original_content = f.read()
except FileNotFoundError:
print(f"Error: File '{input_file}' not found! Creating a sample input.")
original_content = "This is sentence one. Here's sentence two! # Header here\nAnother sentence? Yes, indeed."
# Split content into sentences
sentences = split_into_sentences(original_content)
# Determine valid insertion points (between sentences, excluding before '#')
valid_insertion_points = []
for i in range(len(sentences) - 1):
next_sentence = sentences[i + 1]
if not next_sentence.strip().startswith('#'):
valid_insertion_points.append(i)
if not valid_insertion_points:
print("No valid insertion points found (all subsequent sentences start with '#'). Output will be unchanged.")
content = original_content
else:
# Decide how many insertions (up to 10 or available valid points)
num_insertions = min(10, len(valid_insertion_points))
# Randomly select insertion points from valid ones
insertion_indices = random.sample(valid_insertion_points, num_insertions)
insertion_indices.sort() # Sort to maintain order
# Shuffle templates and select needed number
random.shuffle(templates)
selected_templates = templates[:num_insertions]
# Build new content with insertions
content = ""
template_idx = 0
for i, sentence in enumerate(sentences):
content += sentence
if i in insertion_indices and template_idx < num_insertions:
content += " " + selected_templates[template_idx].format(url=url) + " "
print("insert.")
template_idx += 1
# Preserve line breaks after sentences
if i < len(sentences) - 1 and original_content[original_content.index(sentence) + len(sentence):].startswith('\n'):
content += "\n"
# Write to output markdown file
output_file = "original_mdfile_with_reminders.md"
with open(output_file, "w", encoding="utf-8") as f:
f.write(content)
print(f"Markdown file '{output_file}' has been generated with up to {num_insertions} insertions!")
PyPDF2 顺时针旋转一个PDF的所有页面并保存
点击查看代码
import PyPDF2
# 打开原始PDF文件
with open('input.pdf', 'rb') as file:
reader = PyPDF2.PdfReader(file)
writer = PyPDF2.PdfWriter()
# 遍历所有页面
for page in reader.pages:
# 顺时针旋转90度
rotated_page = page.rotate(90)
writer.add_page(rotated_page)
# 保存旋转后的PDF
with open('output.pdf', 'wb') as output_file:
writer.write(output_file)
多张图片拼接为一张长图
点击查看代码
# Requirements: pip install pillow tkinterdnd2 requests
from PIL import Image, ImageTk, ImageGrab
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog, Menu
from tkinterdnd2 import DND_FILES, TkinterDnD, DND_TEXT
import os
import tempfile
import requests
import uuid
import math
class ImageStitcher:
def __init__(self, root):
self.root = root
self.root.title("Image Stitcher — Drag/URL • Zoom • Rotate • Pin • Preview")
self.root.geometry("860x640")
self.root.minsize(780, 540)
# Each image now has 'zoom' and 'rotate' (degrees)
self.images = [] # [{'path':str, 'zoom':float, 'rotate':float, 'thumb':PhotoImage, 'temp':bool}]
self.direction = tk.StringVar(value="horizontal")
self.always_on_top = tk.BooleanVar(value=False)
self.temp_dir = tempfile.mkdtemp(prefix="imgstitch_")
self.base_thumb_size = 80
self._init_ui()
self._setup_reorder_drag()
self._setup_paste_menu()
self.root.bind("<Control-v>", self.paste_from_clipboard)
self.always_on_top.trace("w", self._update_on_top)
def _init_ui(self):
top_frame = tk.Frame(self.root)
top_frame.pack(fill="x", padx=10, pady=(6,0))
tk.Label(top_frame, text="Image Stitcher", font=("Segoe UI", 11, "bold")).pack(side="left")
pin_btn = ttk.Checkbutton(
top_frame,
text="Pin 📌",
variable=self.always_on_top,
)
pin_btn.pack(side="right")
main = tk.Frame(self.root)
main.pack(fill="both", expand=True, padx=12, pady=6)
left = tk.Frame(main, width=340)
left.pack(side="left", fill="y", padx=(0,12))
tk.Label(left, text="Images (drag files/browser/URL, right-click or Ctrl+V paste,\ndouble-click → set zoom % + rotation °)",
font=("Segoe UI", 10)).pack(anchor="w")
self.listbox = tk.Listbox(left, font=("Consolas", 10), height=22, selectmode="single")
self.listbox.pack(fill="both", expand=True, pady=(4,0))
scrollbar = ttk.Scrollbar(left, orient="vertical", command=self.listbox.yview)
self.listbox.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side="right", fill="y")
btn_frame = tk.Frame(left)
btn_frame.pack(fill="x", pady=8)
ttk.Button(btn_frame, text="Clear", width=8, command=self.clear_all).pack(side="left", padx=4)
ttk.Button(btn_frame, text="Remove", width=10, command=self.remove_selected).pack(side="left", padx=4)
settings = tk.Frame(left)
settings.pack(fill="x", pady=(6,0))
tk.Label(settings, text="Stitch:").pack(side="left")
ttk.Radiobutton(settings, text="Horizontal", variable=self.direction, value="horizontal").pack(side="left", padx=8)
ttk.Radiobutton(settings, text="Vertical", variable=self.direction, value="vertical").pack(side="left")
ttk.Button(left, text="Stitch & Save", command=self.stitch_and_save).pack(pady=12, fill="x")
right = tk.Frame(main)
right.pack(side="right", fill="both", expand=True)
tk.Label(right, text="Preview (click to see large stitched result)", font=("Segoe UI", 10)).pack(anchor="w")
self.preview_canvas = tk.Canvas(right, bg="#f9f9fa", highlightthickness=1, relief="ridge")
self.preview_canvas.pack(fill="both", expand=True, pady=(4,0))
self.preview_canvas.bind("<Button-1>", self._show_large_preview)
self.listbox.drop_target_register(DND_FILES, DND_TEXT)
self.listbox.dnd_bind('<<Drop>>', self.on_drop)
def _setup_paste_menu(self):
self.context_menu = Menu(self.root, tearoff=0)
self.context_menu.add_command(label="Paste from Clipboard", command=self.paste_from_clipboard)
def show_context_menu(event):
try:
self.context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()
self.listbox.bind("<Button-3>", show_context_menu)
self.root.bind("<Button-3>", show_context_menu)
self.root.bind_class("Tk", "<Button-3>", show_context_menu, add="+")
def _update_on_top(self, *args):
self.root.attributes("-topmost", self.always_on_top.get())
def _show_large_preview(self, event=None):
stitched = self._build_stitched_image()
if stitched is None:
return
preview_win = tk.Toplevel(self.root)
preview_win.title("Stitched Preview")
preview_win.geometry("900x700")
preview_win.transient(self.root)
canvas = tk.Canvas(preview_win, bg="#f0f0f0")
canvas.pack(fill="both", expand=True)
max_w, max_h = 880, 640
scale = min(1.0, max_w / stitched.width, max_h / stitched.height)
if scale < 1:
disp = stitched.resize((int(stitched.width * scale), int(stitched.height * scale)), Image.Resampling.LANCZOS)
else:
disp = stitched
photo = ImageTk.PhotoImage(disp)
canvas.create_image(10, 10, image=photo, anchor="nw")
canvas.image = photo
canvas.configure(scrollregion=(0, 0, disp.width + 20, disp.height + 20))
ttk.Button(preview_win, text="Close", command=preview_win.destroy).pack(pady=8)
def _build_stitched_image(self):
if not self.images:
messagebox.showinfo("Nothing loaded", "No images.")
return None
try:
pil_images = []
for d in self.images:
img = Image.open(d["path"]).convert("RGBA")
# Apply zoom
if d["zoom"] != 1.0:
new_size = (int(img.width * d["zoom"]), int(img.height * d["zoom"]))
img = img.resize(new_size, Image.Resampling.LANCZOS)
# Apply rotation
if d["rotate"] % 360 != 0:
img = img.rotate(d["rotate"], expand=True, resample=Image.Resampling.BICUBIC)
pil_images.append(img)
except Exception as e:
messagebox.showerror("Error", f"Load/resize/rotate failed:\n{e}")
return None
is_h = self.direction.get() == "horizontal"
if is_h:
max_h = max(img.height for img in pil_images)
total_w = sum(img.width for img in pil_images)
size = (total_w, max_h)
get_pos = lambda off, w, h: (off, (max_h - h) // 2)
else:
max_w = max(img.width for img in pil_images)
total_h = sum(img.height for img in pil_images)
size = (max_w, total_h)
get_pos = lambda off, w, h: ((max_w - w) // 2, off)
result = Image.new("RGBA", size, (0,0,0,0))
offset = 0
for img in pil_images:
w, h = img.size
pos = get_pos(offset, w, h)
result.paste(img, pos)
offset += w if is_h else h
return result
def stitch_and_save(self):
result = self._build_stitched_image()
if result is None:
return
first = os.path.splitext(os.path.basename(self.images[0]["path"]))[0]
default = f"stitched_{first}_[{len(self.images)}]_{self.direction.get()}"
path = filedialog.asksaveasfilename(
defaultextension=".png",
filetypes=[("PNG","*.png"), ("JPEG","*.jpg"), ("WebP","*.webp")],
initialfile=default
)
if not path: return
try:
if path.lower().endswith((".jpg",".jpeg")):
result = result.convert("RGB")
result.save(path, quality=92)
messagebox.showinfo("Saved", f"Output:\n{path}")
except Exception as e:
messagebox.showerror("Save failed", str(e))
def _edit_properties(self, event):
idx = self.listbox.nearest(event.y)
if idx < 0 or idx >= len(self.images):
return
d = self.images[idx]
name = os.path.basename(d["path"])
dialog = tk.Toplevel(self.root)
dialog.title(f"Properties — {name}")
dialog.transient(self.root)
dialog.grab_set()
tk.Label(dialog, text=f"Image: {name}", font=("Segoe UI", 10, "bold")).pack(pady=(10,5))
# Zoom
zoom_frame = tk.Frame(dialog)
zoom_frame.pack(fill="x", padx=20, pady=5)
tk.Label(zoom_frame, text="Zoom (%):").pack(side="left")
zoom_var = tk.DoubleVar(value=d["zoom"] * 100)
tk.Entry(zoom_frame, textvariable=zoom_var, width=10).pack(side="left", padx=10)
# Rotation
rot_frame = tk.Frame(dialog)
rot_frame.pack(fill="x", padx=20, pady=5)
tk.Label(rot_frame, text="Rotate (°):").pack(side="left")
rot_var = tk.DoubleVar(value=d["rotate"])
tk.Entry(rot_frame, textvariable=rot_var, width=10).pack(side="left", padx=10)
def apply_changes():
try:
new_zoom = zoom_var.get() / 100.0
new_rot = rot_var.get()
if new_zoom < 0.1 or new_zoom > 5.0:
raise ValueError("Zoom must be between 10% and 500%")
d["zoom"] = new_zoom
d["rotate"] = new_rot % 360
self._regen_thumbnail(idx)
self._update_listbox()
self.update_preview()
dialog.destroy()
except Exception as e:
messagebox.showerror("Invalid input", str(e))
ttk.Button(dialog, text="Apply", command=apply_changes).pack(pady=15)
ttk.Button(dialog, text="Cancel", command=dialog.destroy).pack()
def _regen_thumbnail(self, index):
d = self.images[index]
try:
pil_img = Image.open(d["path"]).convert("RGBA")
# Apply zoom for thumbnail
scale = max(0.5, min(3.0, d["zoom"]))
thumb_size = int(self.base_thumb_size * scale)
# Resize first (zoom)
if d["zoom"] != 1.0:
new_size = (int(pil_img.width * d["zoom"]), int(pil_img.height * d["zoom"]))
pil_img = pil_img.resize(new_size, Image.Resampling.LANCZOS)
# Then rotate
if d["rotate"] % 360 != 0:
pil_img = pil_img.rotate(d["rotate"], expand=True, resample=Image.Resampling.BICUBIC)
thumb = pil_img.copy()
thumb.thumbnail((thumb_size, thumb_size), Image.Resampling.LANCZOS)
d["thumb"] = ImageTk.PhotoImage(thumb)
except:
pass
def _setup_reorder_drag(self):
self.listbox.bind("<Button-1>", self._start_drag)
self.listbox.bind("<B1-Motion>", self._drag_motion)
self.listbox.bind("<ButtonRelease-1>", self._drop_reorder)
self.listbox.bind("<Double-Button-1>", self._edit_properties) # ← changed
self.drag_data = {"index": None}
def _start_drag(self, event):
idx = self.listbox.nearest(event.y)
if 0 <= idx < self.listbox.size():
self.drag_data["index"] = idx
self.listbox.selection_clear(0, tk.END)
self.listbox.selection_set(idx)
def _drag_motion(self, event):
if self.drag_data["index"] is None: return
target = self.listbox.nearest(event.y)
if target == self.drag_data["index"] or target < 0 or target >= self.listbox.size():
return
text = self.listbox.get(self.drag_data["index"])
self.listbox.delete(self.drag_data["index"])
self.listbox.insert(target, text)
self.drag_data["index"] = target
self.listbox.selection_clear(0, tk.END)
self.listbox.selection_set(target)
def _drop_reorder(self, event):
if self.drag_data["index"] is None: return
new_order = []
for i in range(self.listbox.size()):
fname = self.listbox.get(i).split(" (")[0]
for img in self.images:
if os.path.basename(img["path"]) == fname:
new_order.append(img)
break
self.images = new_order
self.update_preview()
self.drag_data["index"] = None
def on_drop(self, event):
data = event.data.strip()
added = 0
if data.startswith("{") or os.path.sep in data or data.lower().startswith(("file://", "file:")):
paths = self.root.tk.splitlist(data)
for raw in paths:
path = raw.strip('{}').strip()
if self._add_image_from_path(path):
added += 1
elif data.startswith("http"):
urls = [u.strip() for u in data.split() if u.startswith("http")]
for url in urls:
if self._add_image_from_url(url):
added += 1
if added > 0:
self._update_listbox()
self.update_preview()
def _add_image_from_path(self, path):
if not os.path.isfile(path): return False
if not path.lower().endswith(('.png','.jpg','.jpeg','.webp','.bmp','.gif','.tif','.tiff')): return False
if any(d["path"] == path for d in self.images): return False
return self._load_and_add(path, is_temp=False)
def _add_image_from_url(self, url):
if any(d["path"] == url for d in self.images): return False
try:
resp = requests.get(url, timeout=8)
if resp.status_code != 200 or 'image' not in resp.headers.get('content-type', ''):
return False
ct = resp.headers.get('content-type', '')
ext = '.png'
if 'jpeg' in ct: ext = '.jpg'
elif 'webp' in ct: ext = '.webp'
elif 'gif' in ct: ext = '.gif'
temp_path = os.path.join(self.temp_dir, f"dl_{uuid.uuid4().hex[:8]}{ext}")
with open(temp_path, "wb") as f:
f.write(resp.content)
return self._load_and_add(temp_path, is_temp=True)
except:
return False
def paste_from_clipboard(self, event=None):
try:
img = ImageGrab.grabclipboard()
if img is None or not isinstance(img, Image.Image):
messagebox.showinfo("Clipboard", "No image found in clipboard.")
return False
temp_path = os.path.join(self.temp_dir, f"clip_{uuid.uuid4().hex[:8]}.png")
img.save(temp_path, "PNG")
if self._load_and_add(temp_path, is_temp=True):
self._update_listbox()
self.update_preview()
return True
else:
if os.path.isfile(temp_path):
os.remove(temp_path)
return False
except Exception as e:
messagebox.showwarning("Clipboard", f"Failed to paste:\n{str(e)}")
return False
def _load_and_add(self, path, is_temp=False):
try:
pil_img = Image.open(path)
thumb = pil_img.copy()
thumb.thumbnail((self.base_thumb_size, self.base_thumb_size), Image.Resampling.LANCZOS)
tk_thumb = ImageTk.PhotoImage(thumb)
self.images.append({
"path": path,
"zoom": 1.0,
"rotate": 0.0,
"thumb": tk_thumb,
"temp": is_temp
})
return True
except:
if is_temp and os.path.isfile(path):
try: os.remove(path)
except: pass
return False
def _update_listbox(self):
self.listbox.delete(0, tk.END)
for img in self.images:
name = os.path.basename(img["path"])
parts = []
if img["zoom"] != 1.0:
parts.append(f"{int(img['zoom']*100)}%")
if img["rotate"] % 360 != 0:
rot = int(img["rotate"] % 360)
parts.append(f"rot {rot}°")
extra = f" ({', '.join(parts)})" if parts else ""
self.listbox.insert(tk.END, name + extra)
def update_preview(self):
self.preview_canvas.delete("all")
if not self.images: return
x, y = 12, 12
spacing = 16
for img_dict in self.images:
thumb = img_dict["thumb"]
w, h = thumb.width(), thumb.height()
self.preview_canvas.create_image(x + w//2, y + h//2, image=thumb)
x += w + spacing
if x > self.preview_canvas.winfo_width() - 140:
x = 12
y += max(90, h + 24)
self.preview_canvas.configure(scrollregion=self.preview_canvas.bbox("all"))
def remove_selected(self):
sel = self.listbox.curselection()
if not sel: return
idx = sel[0]
item = self.images.pop(idx)
if item["temp"] and os.path.isfile(item["path"]):
try: os.remove(item["path"])
except: pass
self._update_listbox()
self.update_preview()
def clear_all(self):
for item in self.images:
if item["temp"] and os.path.isfile(item["path"]):
try: os.remove(item["path"])
except: pass
self.images.clear()
self.listbox.delete(0, tk.END)
self.preview_canvas.delete("all")
def __del__(self):
for item in self.images:
if item.get("temp") and os.path.isfile(item["path"]):
try: os.remove(item["path"])
except: pass
try: os.rmdir(self.temp_dir)
except: pass
if __name__ == "__main__":
root = TkinterDnD.Tk()
app = ImageStitcher(root)
root.mainloop()
Python打包tkinter单文件代码及依赖为dist/目录下的单个exe
pyinstaller --noconfirm --onefile --windowed --collect-all tkinterdnd2 your_single_file.py

浙公网安备 33010602011771号