随手造的实用小工具&项目合集
FlowerAccepted · · 科技·工程
:::align{center}
0x-1. 前言
:::
可能更好的阅读体验
提供源代码直接复制,但是在文章这里不太方便吧。
如有访问困难可先在网上搜索 github 国内代理网站代替官网,实在不行可以考虑私信我解决。
我最近遇到一些小问题,比如拼接几个图片啦,画几个分子图啦,模糊一下图片啦,总之最后都是跟 copilot 聊了几句就完事了。
最近有人问我要这些,也为了方便大家,那我就放出来罢。
后续会更新。 :::align{center}
0x0. 环境配置
:::
基本所有项目均使用 Python。
咱先下个 Python,请在命令行依次全部输入
py --version
python --version
python3 --version
如果有任何一个返回形如 Python 3.<x>.<y> 并且 <x> 为大于等于 10 的整数,就说明你的环境初步好了。
如果没成功,可以访问Python 官网(这个国内太慢,但是若有一周天数根棍棍是可以 10 秒内下完的),点击 Download,然后点击页面中间偏左的金色大按钮就可以开始下载了。下载完打开包安装。装完了输入 python3 --version 测试一下。
着急也没有棍可以考虑 anaconda,虽然包很大,有 1GB,但是一两分钟就下下来了。下完了打开文件安装,遇到让你输入就输回车。安完了开个新的命令行窗口,前面会有一个 (base)。现在你的 Python 环境就配置好了。装完了输入 python --version 测试一下。
这里有许多高级选项,
下一步装包,每个小项目需要的包的安装代码会在后续给出。
特别提示:为了提升包下载速度(当然你有棍子可以不管),建议运行下列命令,设置镜像源:
python -m pip install --upgrade pip
pip config set global.index-url https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
代码运行方式:命令行 <你测试可用的那个 py/python/python3> <文件路径> 别加尖括号哈 。
:::align{center}
0x1. 图片混淆器
:::
简介
这玩意功能很简单啊,也实用,适合用来【】CCF。
亲测对于笔迹效果极好,可以让人看出笔迹而 AI 不能,建议搭配画图软件或 OneNote 与一个好使的触控板或数位板。
依赖包安装
命令行运行以下代码:
pip install gradio opencv-python
代码
github 仓库
:::info[源代码]
import cv2
import numpy as np
import gradio as gr
def add_random_noise(img, noise_level=30):
# img: numpy array, BGR
noise = np.random.randint(-noise_level, noise_level + 1, img.shape, dtype='int16')
noisy_img = img.astype('int16') + noise
noisy_img = np.clip(noisy_img, 0, 255).astype('uint8')
return noisy_img
def blur_edges(img, kernel_size=15):
# 创建掩码,仅模糊边缘
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 100, 200)
mask = cv2.dilate(edges, None, iterations=3)
mask = cv2.GaussianBlur(mask, (kernel_size, kernel_size), 0)
mask = mask / 255.0
blurred = cv2.GaussianBlur(img, (kernel_size, kernel_size), 0)
mask = mask[..., np.newaxis]
result = img * (1 - mask) + blurred * mask
return result.astype('uint8')
def adjust_contrast(img, contrast):
# contrast: 0.5~2.0,1.0为原始对比度
img = img.astype(np.float32)
img = (img - 127.5) * contrast + 127.5
img = np.clip(img, 0, 255).astype(np.uint8)
return img
def remove_border(img, tol=10):
"""
自动去除图片四周的纯色边框(如白边、黑边等)。
tol: 容差,越大越宽松。
"""
if img.ndim == 3:
mask = (np.abs(img - img[0,0]).sum(axis=2) > tol)
else:
mask = (np.abs(img - img[0,0]) > tol)
coords = np.argwhere(mask)
if coords.size == 0:
return img # 全图纯色
y0, x0 = coords.min(axis=0)
y1, x1 = coords.max(axis=0) + 1
cropped = img[y0:y1, x0:x1]
return cropped
def process_image(input_img, noise_level, kernel_size, contrast, remove_border_flag):
img = cv2.cvtColor(input_img, cv2.COLOR_RGB2BGR)
if remove_border_flag:
img = remove_border(img)
img = adjust_contrast(img, contrast)
noisy_img = add_random_noise(img, noise_level)
result_img = blur_edges(noisy_img, kernel_size)
return cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
demo = gr.Interface(
fn=process_image,
inputs=[
gr.Image(type="numpy", label="上传图片"),
gr.Slider(0, 255, value=60, label="噪声强度"),
gr.Slider(1, 51, value=15, step=2, label="模糊卷积核大小"), # 起始值改为1
gr.Slider(0.5, 2.0, value=1.0, step=0.05, label="对比度调整"),
gr.Checkbox(label="去除图片边框等非图片本身信息")
],
outputs=gr.Image(type="numpy", label="处理后图片"),
title="图片随机噪声与边缘模糊工具",
description="上传图片,插入随机噪声、模糊边缘、可调整对比度,并可自动去除图片边框等非图片本身信息。"
)
if __name__ == "__main__":
demo.launch()
::: 运行一会后会弹出一个链接,在浏览器上打开它。 :::align{center}
0x2. 图片锐化器
:::
简介
这是一个本地应用,使用 PyQT5创建,可以提升图片锐度对比度。
真正的小应用 be like。
依赖包安装
命令行运行以下代码:
pip install PyQT5 opencv-python
代码
github 仓库
:::info[源代码]
import sys
import cv2
import numpy as np
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel, QFileDialog
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtCore import Qt
class ImageSharpener(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.image = None
self.sharpened_image = None
def initUI(self):
self.setWindowTitle('Image Sharpener')
layout = QVBoxLayout()
self.imageLabel = QLabel(self)
self.imageLabel.setAlignment(Qt.AlignCenter)
layout.addWidget(self.imageLabel)
loadButton = QPushButton('Load Image', self)
loadButton.clicked.connect(self.loadImage)
layout.addWidget(loadButton)
sharpenButton = QPushButton('Sharpen Image', self)
sharpenButton.clicked.connect(self.sharpenImage)
layout.addWidget(sharpenButton)
saveButton = QPushButton('Save Image', self)
saveButton.clicked.connect(self.saveImage)
layout.addWidget(saveButton)
self.setLayout(layout)
self.resize(400, 400)
def loadImage(self):
fileName, _ = QFileDialog.getOpenFileName(self, "Open Image", "", "Image Files (*.png *.jpg *.bmp)")
if fileName:
self.image = cv2.imread(fileName)
self.displayImage(self.image)
self.sharpened_image = None
def sharpenImage(self):
if self.image is not None:
kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])
self.sharpened_image = cv2.filter2D(self.image, -1, kernel)
self.displayImage(self.sharpened_image)
def saveImage(self):
if self.sharpened_image is not None:
fileName, _ = QFileDialog.getSaveFileName(self, "Save Image", "", "PNG (*.png);;JPEG (*.jpg *.jpeg);;All Files (*)")
if fileName:
cv2.imwrite(fileName, self.sharpened_image)
def displayImage(self, img):
qformat = QImage.Format_RGB888
outImage = QImage(img.data, img.shape[1], img.shape[0], img.strides[0], qformat)
outImage = outImage.rgbSwapped()
pixmap = QPixmap.fromImage(outImage)
self.imageLabel.setPixmap(pixmap.scaled(self.imageLabel.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = ImageSharpener()
ex.show()
sys.exit(app.exec_())
依赖包安装
命令行运行以下代码:
pip install gradio opencv-python
::: :::align{center}
0x3. 图片拼接小工具
:::
简介
结果,“在新标签页打开图片”截图:
依赖包安装
命令行运行以下代码:
pip install gradio opencv-python
代码
github 仓库
:::info[源代码]
import gradio as gr
from PIL import Image, UnidentifiedImageError
from typing import List, Union, Tuple
import re
import math
def hex_to_rgb(color) -> Tuple[int,int,int]:
"""兼容多种颜色输入:
- '#rrggbb' 或 '#rgb'
- 'rgb(r,g,b)' 或 'rgba(r,g,b,a)'
- (r,g,b) 或 [r,g,b]
返回 (r,g,b),解析失败时返回白色 (255,255,255)
"""
if not color:
return (255, 255, 255)
# 已经是序列
if isinstance(color, (tuple, list)):
try:
return tuple(int(max(0, min(255, int(c)))) for c in color[:3])
except Exception:
return (255, 255, 255)
# 字符串处理
if isinstance(color, str):
s = color.strip()
# 十六进制
if s.startswith("#"):
s = s.lstrip("#")
if len(s) == 3:
s = "".join([c*2 for c in s])
if len(s) == 6:
try:
return tuple(int(s[i:i+2], 16) for i in (0, 2, 4))
except Exception:
return (255, 255, 255)
# rgb(...) 或 rgba(...)
if s.lower().startswith("rgb"):
nums = re.findall(r"(-?\d+)", s)
if len(nums) >= 3:
try:
return tuple(min(255, max(0, int(n))) for n in nums[:3])
except Exception:
return (255, 255, 255)
# 不能解析时回退白色
return (255, 255, 255)
def open_images(files: Union[List[str], str]):
if not files:
return []
if isinstance(files, str):
files = [files]
images = []
try:
for fp in files:
img = Image.open(fp).convert("RGB")
images.append(img)
except UnidentifiedImageError:
raise ValueError("上传的文件中包含无法识别为图片的文件。")
except Exception as e:
raise ValueError(f"读取图片出错:{e}")
return images
def resize_keep_aspect(img: Image.Image, target_w: int = None, target_h: int = None):
w, h = img.size
if target_w is None and target_h is None:
return img
if target_h is None:
scale = target_w / w
elif target_w is None:
scale = target_h / h
else:
scale_w = target_w / w
scale_h = target_h / h
scale = min(scale_w, scale_h)
new_size = (max(1, int(w * scale)), max(1, int(h * scale)))
return img.resize(new_size, Image.LANCZOS)
def layout_grid_metrics(images: List[Image.Image], cols: int, spacing: int):
# images in list order -> fill rows left-to-right, top-to-bottom
n = len(images)
rows = math.ceil(n / cols)
# compute column widths and row heights
col_widths = [0] * cols
row_heights = [0] * rows
for idx, im in enumerate(images):
r = idx // cols
c = idx % cols
col_widths[c] = max(col_widths[c], im.width)
row_heights[r] = max(row_heights[r], im.height)
total_w = sum(col_widths) + spacing * (cols - 1)
total_h = sum(row_heights) + spacing * (rows - 1)
return total_w, total_h, col_widths, row_heights
def render_grid(images: List[Image.Image], cols: int, spacing: int, bg_rgb: Tuple[int,int,int]):
n = len(images)
rows = math.ceil(n / cols)
total_w, total_h, col_widths, row_heights = layout_grid_metrics(images, cols, spacing)
out = Image.new("RGB", (total_w, total_h), bg_rgb)
# compute x offsets for columns and y offsets for rows
x_offsets = []
x = 0
for w in col_widths:
x_offsets.append(x)
x += w + spacing
y_offsets = []
y = 0
for h in row_heights:
y_offsets.append(y)
y += h + spacing
# paste each image centered in its cell
for idx, im in enumerate(images):
r = idx // cols
c = idx % cols
cell_x = x_offsets[c]
cell_y = y_offsets[r]
cell_w = col_widths[c]
cell_h = row_heights[r]
paste_x = cell_x + (cell_w - im.width) // 2
paste_y = cell_y + (cell_h - im.height) // 2
out.paste(im, (paste_x, paste_y))
return out, total_w, total_h
def find_best_grid(images: List[Image.Image], spacing: int, bg_rgb: Tuple[int,int,int],
max_cols_search: int, objective: str, order_strategy: str):
n = len(images)
if max_cols_search <= 0:
max_cols_search = n
max_cols_search = min(max_cols_search, n)
# generate different orderings to try
def order_images(strategy):
if strategy == "original":
return images[:]
if strategy == "width":
return sorted(images, key=lambda im: im.width, reverse=True)
if strategy == "height":
return sorted(images, key=lambda im: im.height, reverse=True)
if strategy == "area":
return sorted(images, key=lambda im: im.width*im.height, reverse=True)
return images[:]
best = None # tuple (metric_value, out_image, cols, w, h, order_name)
strategies = [order_strategy] if order_strategy != "all" else ["original","width","height","area"]
for cols in range(1, max_cols_search+1):
for strat in strategies:
seq = order_images(strat)
out_img, w, h = render_grid(seq, cols, spacing, bg_rgb)[0:3]
if objective == "area":
metric = w * h
elif objective == "maxside":
metric = max(w, h)
elif objective == "width":
metric = w
elif objective == "height":
metric = h
else:
metric = w * h
if best is None or metric < best[0]:
best = (metric, out_img, cols, w, h, strat)
return best
def concat_images(files: Union[List[str], str],
mode: str = "scale_height",
orientation: str = "horizontal",
spacing: int = 0,
bg_color: str = "#ffffff",
size_ref: str = "max",
auto_grid: bool = False,
max_grid_cols: int = 0,
grid_objective: str = "area",
grid_order: str = "height"):
"""
新增参数:
- auto_grid: 是否启用自动网格布局(多行多列)以尽量减小画布
- max_grid_cols: 搜索最大列数(0 表示最大为图片数)
- grid_objective: 'area' | 'maxside' | 'width' | 'height'
- grid_order: 'original'|'width'|'height'|'area'|'all'
"""
try:
images = open_images(files)
except ValueError as e:
return str(e)
if not images:
return None
# 先按已有模式处理每张图片(scale/fit/stretch/none)
widths = [im.width for im in images]
heights = [im.height for im in images]
max_w, max_h = max(widths), max(heights)
min_w, min_h = min(widths), min(heights)
processed = []
if mode == "none":
if len(set(widths)) > 1 or len(set(heights)) > 1:
return "所有图片尺寸必须一致,或选择其他处理模式。"
processed = images
elif mode == "scale_height":
target_h = max_h if size_ref == "max" else min_h
for im in images:
processed.append(resize_keep_aspect(im, target_h=target_h))
elif mode == "scale_width":
target_w = max_w if size_ref == "max" else min_w
for im in images:
processed.append(resize_keep_aspect(im, target_w=target_w))
elif mode == "fill":
for im in images:
w, h = im.size
scale = max(max_w / w, max_h / h)
new_size = (max(1, int(w * scale)), max(1, int(h * scale)))
resized = im.resize(new_size, Image.LANCZOS)
canvas = Image.new("RGB", (max_w, max_h), hex_to_rgb(bg_color))
offset_x = (max_w - resized.width) // 2
offset_y = (max_h - resized.height) // 2
canvas.paste(resized, (offset_x, offset_y))
processed.append(canvas)
elif mode == "stretch":
for im in images:
processed.append(im.resize((max_w, max_h), Image.LANCZOS))
else:
return "未知的处理模式。"
# 如果启用自动网格布局 -> 在 processed 上搜索最优网格
bg_rgb = hex_to_rgb(bg_color)
if auto_grid:
best = find_best_grid(processed, spacing, bg_rgb, max_grid_cols, grid_objective, grid_order)
if best is None:
return "无法生成网格布局。"
_, out_img, best_cols, best_w, best_h, used_order = best
return out_img
# 否则,按横向或纵向线性拼接(保持原逻辑)
widths = [im.width for im in processed]
heights = [im.height for im in processed]
n = len(processed)
if orientation == "horizontal":
total_w = sum(widths) + spacing * (n - 1)
total_h = max(heights)
out = Image.new("RGB", (total_w, total_h), bg_rgb)
x = 0
for im in processed:
y = (total_h - im.height) // 2
out.paste(im, (x, y))
x += im.width + spacing
else:
total_w = max(widths)
total_h = sum(heights) + spacing * (n - 1)
out = Image.new("RGB", (total_w, total_h), bg_rgb)
y = 0
for im in processed:
x = (total_w - im.width) // 2
out.paste(im, (x, y))
y += im.height + spacing
return out
with gr.Blocks() as demo:
gr.Markdown("# 图片拼接小工具(支持不同尺寸 & 自动网格布局)\n上传多张图片,选择处理模式与拼接方式。")
with gr.Row():
file_input = gr.File(label="上传图片", file_count="multiple", type="filepath")
mode = gr.Radio(choices=[
("等比缩放到相同高度", "scale_height"),
("等比缩放到相同宽度", "scale_width"),
("等比缩放并填充到最大尺寸", "fill"),
("拉伸到相同尺寸(可能变形)", "stretch"),
("不处理(要求相同尺寸)", "none")
], value="scale_height", label="处理模式")
with gr.Row():
size_ref = gr.Radio(choices=[("以最大尺寸为准", "max"), ("以最小尺寸为准", "min")], value="max", label="参考尺寸(用于缩放)")
orientation = gr.Radio(choices=[("横向拼接", "horizontal"), ("纵向拼接", "vertical")], value="horizontal", label="拼接方向(线性)")
with gr.Row():
spacing = gr.Slider(minimum=0, maximum=200, value=0, step=1, label="图片间距(像素)")
bg_color = gr.ColorPicker(value="#ffffff", label="背景颜色")
with gr.Row():
auto_grid = gr.Checkbox(label="启用自动网格布局(让画布尽量小)", value=False)
max_grid_cols = gr.Slider(minimum=0, maximum=20, step=1, value=0, label="网格最大列数(0=自动到图片数)")
with gr.Row():
grid_objective = gr.Radio(choices=[
("最小面积", "area"),
("最小最大边(minimize max(width,height))", "maxside"),
("最小宽度", "width"),
("最小高度", "height")
], value="area", label="网格优化目标")
grid_order = gr.Radio(choices=[
("只用当前顺序", "original"),
("按宽降序", "width"),
("按高降序", "height"),
("按面积降序", "area"),
("全部策略尝试", "all")
], value="height", label="排序策略")
output = gr.Image(type="pil", label="拼接结果")
btn = gr.Button("开始拼接")
btn.click(fn=concat_images, inputs=[file_input, mode, orientation, spacing, bg_color, size_ref, auto_grid, max_grid_cols, grid_objective, grid_order], outputs=output)
if __name__ == "__main__":
demo.launch()
::: :::align{center}
0x4. 题解创建器
:::
简介
又一个迷你应用。输入题号格式和主站“题目跳转”格式相同(输入数字直接跳转 P 题库)。
依赖包安装
命令行运行以下代码:
pip install gradio opencv-python
代码
github 仓库
:::info[源代码]
import gradio as gr
def f(s):
if '0' <= s[0] <= '9':
s = 'P' + s
return f"[https://www.luogu.com.cn/article/_new?problem={s}](https://www.luogu.com.cn/article/_new?problem={s})"
with gr.Blocks() as app:
id = gr.Textbox(label="洛谷题目编号")
sub = gr.Button("生成题解模版")
out = gr.Markdown("")
sub.click(f, id, outputs=out)
app.launch()
::: :::align{center}
0x5. 分子画图器
:::
简介
很实用的分子图绘画器,输入 SMILES 代码,输出分子图,还提供 SVG 下载和存档!
关于 SMILES 代码,可以参考官方文档或者百度搜索。
请将代码放在专用目录下运行,因为这个有存档
依赖包安装
命令行运行以下代码:
pip install gradio rdkit pillow
代码
请将代码放在专用目录下运行,因为这个有存档
github 仓库
:::info[源代码]
import os
import io
import csv
import uuid
from datetime import datetime
from rdkit import Chem
from rdkit.Chem import Draw
# try multiple import paths for rdMolDraw2D for RDKit distribution differences
try:
from rdkit.Chem import rdMolDraw2D # common in some builds
except Exception:
try:
from rdkit.Chem.Draw import rdMolDraw2D # alternative location
except Exception:
# last attempt: maybe accessible as attribute
rdMolDraw2D = getattr(Draw, "rdMolDraw2D", None)
from PIL import Image
import gradio as gr
# Paths
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
IMAGES_DIR = os.path.join(ROOT_DIR, "images")
ARCHIVE_PATH = os.path.join(ROOT_DIR, "smiles_archive.csv")
os.makedirs(IMAGES_DIR, exist_ok=True)
# Ensure CSV has full header (id,timestamp,smiles,saved_image,notes)
if not os.path.exists(ARCHIVE_PATH):
with open(ARCHIVE_PATH, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["id", "timestamp", "smiles", "saved_image", "notes"])
def mol_image_from_smiles(smiles: str, width: int = 300, height: int = 300):
if not smiles or not smiles.strip():
return None, "Empty SMILES input."
mol = Chem.MolFromSmiles(smiles.strip())
if mol is None:
return None, "无法解析 SMILES(无效格式)."
try:
img = Draw.MolToImage(mol, size=(int(width), int(height)))
if not isinstance(img, Image.Image):
img = Image.fromarray(img)
return img, None
except Exception as e:
return None, f"绘制分子时出错: {e}"
def mol_svg_from_smiles(smiles: str, width: int = 300, height: int = 300):
if not smiles or not smiles.strip():
return None, "Empty SMILES input."
mol = Chem.MolFromSmiles(smiles.strip())
if mol is None:
return None, "无法解析 SMILES(无效格式)."
if rdMolDraw2D is None:
# rdMolDraw2D not available in this RDKit build
return None, ("当前 RDKit 构建中缺少 rdMolDraw2D(无法生成 SVG)。"
" 建议使用 conda 安装 RDKit(例如:conda install -c conda-forge rdkit),"
" 或选择 PNG 输出格式。")
try:
drawer = rdMolDraw2D.MolDraw2DSVG(int(width), int(height))
rdMolDraw2D.PrepareAndDrawMolecule(drawer, mol)
drawer.FinishDrawing()
svg = drawer.GetDrawingText()
return svg, None
except Exception as e:
return None, f"生成 SVG 时出错: {e}"
def handle_draw(smiles, width, height, fmt):
if fmt == "SVG":
svg, err = mol_svg_from_smiles(smiles, width, height)
if err:
return None, err
# Gradio Image expects PIL for preview; convert SVG to PNG in-memory for preview
try:
# Try render SVG to PNG using PIL + frombuffer via cairosvg if available
# If cairosvg isn't installed, we fallback to returning a small raster via RDKit PNG
try:
import cairosvg
png_bytes = cairosvg.svg2png(bytestring=svg.encode("utf-8"), output_width=int(width), output_height=int(height))
img = Image.open(io.BytesIO(png_bytes)).convert("RGBA")
except Exception:
# fallback: rasterize via RDKit's MolToImage
img, err2 = mol_image_from_smiles(smiles, width, height)
if err2:
return None, err2
return img, f"绘制成功({fmt})"
except Exception as e:
return None, f"预览 SVG 转换失败: {e}"
else:
img, err = mol_image_from_smiles(smiles, width, height)
if err:
return None, err
return img, f"绘制成功({fmt})"
def handle_download(smiles, width, height, fmt):
if fmt == "SVG":
svg, err = mol_svg_from_smiles(smiles, width, height)
if err:
return None, err
filename = f"mol_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}.svg"
path = os.path.join(IMAGES_DIR, filename)
try:
with open(path, "w", encoding="utf-8") as f:
f.write(svg)
except Exception as e:
return None, f"保存 SVG 失败: {e}"
return path, f"已保存为 {filename}(SVG),可点击下载。"
else:
img, err = mol_image_from_smiles(smiles, width, height)
if err:
return None, err
filename = f"mol_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}.png"
path = os.path.join(IMAGES_DIR, filename)
try:
img.save(path, format="PNG")
except Exception as e:
return None, f"保存图片失败: {e}"
return path, f"已保存为 {filename}(PNG),可点击下载。"
def handle_archive(smiles, width, height, notes, fmt):
mol = Chem.MolFromSmiles(smiles.strip() if smiles else "")
if mol is None:
return None, "无法解析 SMILES(无效格式),未归档。"
saved_image_name = ""
# Save chosen format preview for archive
if fmt == "SVG":
svg, err = mol_svg_from_smiles(smiles, width, height)
if svg:
saved_image_name = f"arch_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}.svg"
img_path = os.path.join(IMAGES_DIR, saved_image_name)
try:
with open(img_path, "w", encoding="utf-8") as f:
f.write(svg)
except Exception:
saved_image_name = ""
else:
img, err = mol_image_from_smiles(smiles, width, height)
if img:
saved_image_name = f"arch_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}.png"
img_path = os.path.join(IMAGES_DIR, saved_image_name)
try:
img.save(img_path, format="PNG")
except Exception:
saved_image_name = ""
entry_id = uuid.uuid4().hex
ts = datetime.now().isoformat()
try:
with open(ARCHIVE_PATH, "a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([entry_id, ts, smiles.strip(), saved_image_name, notes or ""])
except Exception as e:
return None, f"追加归档失败: {e}"
return ARCHIVE_PATH, "SMILES 已归档并写入 CSV(可点击下载或在表格中查看)。"
def read_archive_rows():
rows = []
if not os.path.exists(ARCHIVE_PATH):
return rows
with open(ARCHIVE_PATH, "r", newline="", encoding="utf-8") as f:
reader = csv.reader(f)
header = next(reader, None)
for r in reader:
# ensure length
if len(r) < 5:
r += [""] * (5 - len(r))
rows.append(r)
return rows
def handle_get_archive_table():
rows = read_archive_rows()
# Return header + rows for gr.Dataframe display (we'll return rows only)
return rows
def handle_delete_entry(entry_id):
if not entry_id:
return False, "未选择 ID。"
rows = read_archive_rows()
new_rows = []
deleted = False
deleted_image = ""
for r in rows:
if r[0] == entry_id:
deleted = True
deleted_image = r[3]
continue
new_rows.append(r)
if not deleted:
return False, "未找到对应条目。"
# Write back CSV with header
try:
with open(ARCHIVE_PATH, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["id", "timestamp", "smiles", "saved_image", "notes"])
writer.writerows(new_rows)
except Exception as e:
return False, f"更新归档文件失败: {e}"
# Attempt to delete image file
if deleted_image:
p = os.path.join(IMAGES_DIR, deleted_image)
try:
if os.path.exists(p):
os.remove(p)
except Exception:
pass
return True, "已删除该条目(如有关联图片已尝试删除)。"
def handle_download_entry_image(entry_id):
if not entry_id:
return None, "未选择 ID。"
rows = read_archive_rows()
for r in rows:
if r[0] == entry_id:
img_name = r[3]
if not img_name:
return None, "该条目无保存的图片。"
p = os.path.join(IMAGES_DIR, img_name)
if os.path.exists(p):
return p, "找到并准备下载图片。"
else:
return None, "关联图片文件不存在。"
return None, "未找到对应条目。"
def handle_download_entry_row(entry_id):
if not entry_id:
return None, "未选择 ID。"
rows = read_archive_rows()
for r in rows:
if r[0] == entry_id:
# write single-row CSV to temp path
fname = f"entry_{entry_id}.csv"
p = os.path.join(IMAGES_DIR, fname)
try:
with open(p, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["id", "timestamp", "smiles", "saved_image", "notes"])
writer.writerow(r)
return p, "单条记录 CSV 已准备好下载。"
except Exception as e:
return None, f"导出单条 CSV 失败: {e}"
return None, "未找到对应条目。"
# Build Gradio UI
with gr.Blocks(title="SMILES 分子绘制器(RDKit + Gradio)") as demo:
gr.Markdown("## SMILES 分子绘制器\n输入 SMILES,调整画布大小,选择导出格式(PNG/SVG),添加备注后可归档;界面可查看/下载/删除归档条目。")
with gr.Row():
with gr.Column(scale=2):
smiles_input = gr.Textbox(label="SMILES", placeholder="例如:CCO 或 c1ccccc1", lines=1)
with gr.Row():
width_input = gr.Number(value=300, label="画布宽 (px)", precision=0)
height_input = gr.Number(value=300, label="画布高 (px)", precision=0)
fmt_input = gr.Radio(["PNG", "SVG"], value="PNG", label="输出格式")
notes_input = gr.Textbox(label="备注 / 标签(可选)", placeholder="例如:synth-1, interesting")
with gr.Row():
draw_btn = gr.Button("Draw")
download_btn = gr.Button("Download (save file)")
archive_btn = gr.Button("Archive SMILES")
status = gr.Textbox(label="状态 / 信息", interactive=False)
with gr.Column(scale=1):
preview = gr.Image(label="预览", type="pil")
gr.Markdown("### 归档管理")
with gr.Row():
with gr.Column(scale=2):
refresh_btn = gr.Button("刷新归档表格")
archive_table = gr.Dataframe(headers=["id", "timestamp", "smiles", "saved_image", "notes"], interactive=False)
with gr.Row():
select_id = gr.Textbox(label="选择条目 ID(用于下载/删除)", placeholder="在表格中复制 ID 到此处")
download_entry_img_btn = gr.Button("下载条目图片")
download_entry_row_btn = gr.Button("下载条目 CSV")
delete_entry_btn = gr.Button("删除条目")
archive_file = gr.File(label="下载文件")
with gr.Column(scale=1):
archive_download_btn = gr.Button("下载完整归档 CSV(最新)")
# Wire events
draw_btn.click(fn=handle_draw, inputs=[smiles_input, width_input, height_input, fmt_input], outputs=[preview, status])
download_btn.click(fn=handle_download, inputs=[smiles_input, width_input, height_input, fmt_input], outputs=[archive_file, status])
archive_btn.click(fn=handle_archive, inputs=[smiles_input, width_input, height_input, notes_input, fmt_input], outputs=[archive_file, status])
refresh_btn.click(fn=handle_get_archive_table, inputs=None, outputs=[archive_table])
delete_entry_btn.click(fn=handle_delete_entry, inputs=[select_id], outputs=[status, archive_file]).then(
fn=handle_get_archive_table, inputs=None, outputs=[archive_table]
)
download_entry_img_btn.click(fn=handle_download_entry_image, inputs=[select_id], outputs=[archive_file, status])
download_entry_row_btn.click(fn=handle_download_entry_row, inputs=[select_id], outputs=[archive_file, status])
# also allow one-click download of whole archive
def get_archive_download():
if os.path.exists(ARCHIVE_PATH):
return ARCHIVE_PATH, "完整归档 CSV(最新)"
else:
return None, "归档文件不存在。"
archive_download_btn.click(fn=get_archive_download, inputs=None, outputs=[archive_file, status])
if __name__ == "__main__":
demo.launch()
::: :::align{center}
0x6. 电子木鱼
:::
简介
全文唯一纯 c++ & 唯一纯我手肝
呃呃,有一点炸。
仅适用于类 Linux 系统!
代码
请将代码放在专用目录下运行,因为这个有存档
:::info[源代码]
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
int main() {
while (1) {
cout << "电子木鱼,修改text.txt中的文字以修改内容。\n[ ]启动\n[v]vim text.sh\n[q]退出\n[c]显示信息\n";
char ch = getchar();
if (ch == ' ') {
freopen("text.txt", "r", stdin);
string s;
getline(cin, s);
fclose(stdin);
cout << "请输入敲木鱼次数,谨慎输入:\n";
int n;
cin >> n;
while (n --) {
cout << s << '\n';
}
} else if (ch == 'v') {
cout << "记住按[i]键入,按[<esc>:wq]退出\n";
system("vim text.txt");
} else if (ch == 'q') {
return 0;
} else if (ch == 'c') {
cout << "编写:@FlowerAccepted (洛谷 UID 1023732), 更新时间:2025-5-8 17:20\n";
}
getchar();
}
}
::: :::align{center}
0x7. 数据生成驱动
:::
简介
很实用,放在一个地方便可以驱动所有测试数据的生成。你需要标程可执行文件和Python数据生成器。
使用方法
- 将此脚本保存为 worker.py,放在你的所有题目工作目录下
- 将文件结构调整为:
. ——— worker.py ——— <题目1目录> —————— <程序 IO 名>.py —————— <程序 IO 名>(c++编译的可执行文件) ——— <题目2目录> ——— ... - cd 至你的题目目录
- 运行
../worker.py, 输入程序 IO 名(不带扩展名)、测试点名前缀和数据数量 - 数据将生成在 ./data 目录下, 文件名格式为 <测试点名前缀><编号>.in 和 <测试点名前缀><编号>.ans
- 每组数据会调用标准解程序,生成对应的输出文件
- 生成完成后会打印每组数据的文件路径
- 确保你的程序 IO 名对应的 Python 脚本能正确生成输入文件,
并且标准解程序能正确处理输入并生成输出,且确保更新标准解程序
代码
请将代码放在你所有题目工作目录的上级
github 仓库
:::info[源代码]
'''
使用方法:
1. 将此脚本保存为 worker.py,放在你的所有题目工作目录下
2. 将文件结构调整为:
.
├── worker.py
├── <题目1目录>
├──├── <程序 IO 名>.py
├──├── <程序 IO 名>(c++编译的可执行文件)
├── <题目2目录>
├── ...
3. cd 至你的题目目录
4. 运行 `../worker.py`,
输入程序 IO 名(不带扩展名)、测试点名前缀和数据数量
5. 数据将生成在 ./data 目录下,
文件名格式为 <测试点名前缀><编号>.in 和 <测试点名前缀><编号>.ans
6. 每组数据会调用标准解程序,生成对应的输出文件
7. 生成完成后会打印每组数据的文件路径
8. 确保你的程序 IO 名对应的 Python 脚本能正确生成输入文件,
并且标准解程序能正确处理输入并生成输出,且确保更新标准解程序
'''
import os
import shutil
import subprocess
def main():
s = input("请输入程序 IO 名(不带扩展名): ").strip()
t = input("请输入测试点名前缀: ").strip()
n = int(input("请输入数据数量: ").strip())
data_dir = "./data"
os.makedirs(data_dir, exist_ok=True)
for i in range(1, n + 1):
# 生成数据
subprocess.run(["python", f"./{s}.py"], check=True)
# 调用标准解
subprocess.run([f"./{s}"], check=True)
# 文件名
in_src = f"./{s}.in"
out_src = f"./{s}.out"
in_dst = os.path.join(data_dir, f"{t}{i}.in")
ans_dst = os.path.join(data_dir, f"{t}{i}.ans")
# 移动并重命名
shutil.move(in_src, in_dst)
shutil.move(out_src, ans_dst)
print(f"第{i}组数据生成完成: {in_dst}, {ans_dst}")
if __name__ == "__main__":
main()
:::