《还在一个个手动添加Excel图片批注?python实现Excel图片批注自动化处理,让你的工作效率原地起飞!》-附EXE,小白也能用~
- 2026-11-12 12:19:46



“带你横跨办公自动化的数据江海”
@摸鱼
闻道有先后,术业有专攻。各位大佬们大家新年好!我依旧是你们的老朋友摸鱼~,本人在十多年的日常工作中摸爬滚打攒了不少Python办公自动化的实用项目技巧,自创立"码海听潮"公众号以来,已经陆续分享近80篇原创干货文章啦!里面满满的办公实操干货,希望能与各位大佬共同探讨办公效率提升之道,实现不加班自由。
好叻,多了不说,少了不唠,咱直接上干货。
办公需求场景
从崩溃到优雅的进化
有一个神秘的Excel采购单据表格,里面记录了图片文件名和图片中对应的采购相关数据信息,现在的需求是把这些对应的图片文件当作批注插到对应表格中方便核对,确保账实相符,要是这种类似的需求你的Big Boss安排你去完成,请问阁下该如何应对?
需求的图片文件夹和Excel表格如下图:
需求的Excel表格

需求的图片文件夹

办公痛点分析

01
痛点1:操作繁琐,效率极低
重复劳动:每一行数据都需要手动执行 “插入批注→编辑批注→插入图片→调整大小→定位” 等一系列步骤,完全是机械重复。
时间成本高:如果表格有几百上千行,这个过程会耗费数小时甚至数天,严重拖慢工作进度。
02
痛点2:容易出错,核对风险大
张冠李戴:手动操作极易将图片文件名和对应行的采购单号、产地等信息弄混,导致 “账实不符”,反而增加了核对风险。
遗漏与重复:在大量操作中,很容易漏掉某些行,或者对同一行重复插入图片,造成数据混乱。
由此可见若操作成百上千张图片批注的话整个操作流程繁琐且耗时,高频次的鼠标点击和键盘输入使操作者手指疲劳,堪称"键盘敲冒烟"式的体力劳动,加上人工疲劳操作极易导致遗漏文件夹。于是乎这时候,按以往的 “解题套路”,Python 的专属 BGM 该响起来了 ——go~ go~ go~,救苦救难的大救星这不就来了!!
@摸鱼
问题拆解思路
1.打开需求的excel表格→
2.遍历图片文件夹→
下面,我就用python代码让excel见识一下,什么叫"传统文化遇上赛博效率"(仅展示部分代码,非完整代码,需完整代码看文章末尾说明~)
import tkinter as tkfrom tkinter import ttkclass ExcelImageMatcherPro:def __init__(self, root):self.root = rootself.root.title("Excel批量添加图片批注(欢迎关注微信公众号:码海听潮)")self.root.geometry("900x650")self.root.resizable(False, False)# 设置中文字体self.font_family = ("SimHei", "WenQuanYi Micro Hei", "Heiti TC")# 创建UIself.create_widgets()def create_widgets(self):# 主框架 - 使用grid作为根布局main_frame = tk.Frame(self.root, bg="#f0f0f0")main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)# 配置grid行列权重main_frame.grid_rowconfigure(1, weight=1) # 中间行可扩展main_frame.grid_columnconfigure(0, weight=1)# ==================== 顶部控制区域 ====================top_frame = tk.Frame(main_frame, bg="#f0f0f0", height=210)top_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10))top_frame.grid_propagate(False)# 第一行控制 - 图片文件夹选择folder_frame = ttk.LabelFrame(top_frame, text=" 图片文件夹 ", padding=(5, 5, 5, 5))folder_frame.pack(fill=tk.X, pady=(5, 0))folder_label = tk.Label(folder_frame, text="图片文件夹:", bg="#f0f0f0", font=(self.font_family[0], 11))folder_label.pack(side=tk.LEFT, padx=(0, 5))self.folder_path_var = tk.StringVar()folder_entry = tk.Entry(folder_frame, textvariable=self.folder_path_var, font=(self.font_family[0], 10),width=40)folder_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))browse_folder_btn = tk.Button(folder_frame, text="浏览", command=self.select_folder,font=(self.font_family[0], 10), cursor="hand2", padx=10)browse_folder_btn.pack(side=tk.LEFT)load_btn = tk.Button(folder_frame, text="加载图片", command=self.load_images_from_entry,font=(self.font_family[0], 10), bg="#4CAF50", fg="white",cursor="hand2", padx=10)load_btn.pack(side=tk.RIGHT, padx=(5, 0))# 第二行控制 - 模式选择和参数设置(紧凑布局)control_row2 = ttk.LabelFrame(top_frame, text=" 匹配设置 ", padding=(5, 3, 5, 3))control_row2.pack(fill=tk.X, pady=(5, 0))settings_container = tk.Frame(control_row2, bg="#f0f0f0")settings_container.pack(fill=tk.X, padx=3, pady=3)# 模式选择mode_frame = tk.Frame(settings_container, bg="#f0f0f0")mode_frame.pack(side=tk.LEFT, padx=(0, 8))mode_label = tk.Label(mode_frame, text="匹配模式:", font=(self.font_family[0], 11))mode_label.pack(side=tk.LEFT, padx=(0, 5))self.mode_var = tk.StringVar(value="列匹配模式")mode_combo = ttk.Combobox(mode_frame, textvariable=self.mode_var, values=["列匹配模式", "行匹配模式"],width=12, state="readonly")mode_combo.pack(side=tk.LEFT)mode_combo.bind("<<ComboboxSelected>>", self.on_mode_change)# 参数区域 - 紧凑排列params_frame = tk.Frame(settings_container, bg="#f0f0f0")params_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)# ========== 列模式参数 ==========self.col_param_frame = tk.Frame(params_frame, bg="#f0f0f0")# 匹配列col_match_frame = tk.Frame(self.col_param_frame, bg="#f0f0f0")col_match_frame.pack(side=tk.LEFT, padx=2)ttk.Label(col_match_frame, text="匹配列:", font=(self.font_family[0], 10))\.pack(side=tk.LEFT, padx=0, pady=0)self.col_match_entry = ttk.Entry(col_match_frame, width=4)self.col_match_entry.pack(side=tk.LEFT, padx=1)self.col_match_entry.insert(0, "A")# 插入列col_insert_frame = tk.Frame(self.col_param_frame, bg="#f0f0f0")col_insert_frame.pack(side=tk.LEFT, padx=2)ttk.Label(col_insert_frame, text="插入列:", font=(self.font_family[0], 10))\.pack(side=tk.LEFT, padx=0, pady=0)self.col_insert_entry = ttk.Entry(col_insert_frame, width=4)self.col_insert_entry.pack(side=tk.LEFT, padx=1)self.col_insert_entry.insert(0, "B")# 边距col_margin_frame = tk.Frame(self.col_param_frame, bg="#f0f0f0")col_margin_frame.pack(side=tk.LEFT, padx=2)ttk.Label(col_margin_frame, text="边距:", font=(self.font_family[0], 10))\.pack(side=tk.LEFT, padx=0, pady=0)self.col_margin_entry = ttk.Entry(col_margin_frame, width=4)self.col_margin_entry.pack(side=tk.LEFT, padx=1)self.col_margin_entry.insert(0, "2")# 批注大小设置comment_size_frame = tk.Frame(self.col_param_frame, bg="#f0f0f0")comment_size_frame.pack(side=tk.LEFT, padx=5)ttk.Label(comment_size_frame, text="批注宽:", font=(self.font_family[0], 10))\.pack(side=tk.LEFT, padx=0, pady=0)self.comment_width_entry = ttk.Entry(comment_size_frame, width=4)self.comment_width_entry.pack(side=tk.LEFT, padx=1)self.comment_width_entry.insert(0, "200")ttk.Label(comment_size_frame, text="高:", font=(self.font_family[0], 10))\.pack(side=tk.LEFT, padx=0, pady=0)self.comment_height_entry = ttk.Entry(comment_size_frame, width=4)self.comment_height_entry.pack(side=tk.LEFT, padx=1)self.comment_height_entry.insert(0, "150")# ========== 行模式参数 ==========self.row_param_frame = tk.Frame(params_frame, bg="#f0f0f0")# 匹配行row_match_frame = tk.Frame(self.row_param_frame, bg="#f0f0f0")row_match_frame.pack(side=tk.LEFT, padx=2)ttk.Label(row_match_frame, text="匹配行:", font=(self.font_family[0], 10))\.pack(side=tk.LEFT, padx=0, pady=0)self.row_match_entry = ttk.Entry(row_match_frame, width=4)self.row_match_entry.pack(side=tk.LEFT, padx=1)self.row_match_entry.insert(0, "1")# 插入行row_insert_frame = tk.Frame(self.row_param_frame, bg="#f0f0f0")row_insert_frame.pack(side=tk.LEFT, padx=2)ttk.Label(row_insert_frame, text="插入行:", font=(self.font_family[0], 10))\.pack(side=tk.LEFT, padx=0, pady=0)self.row_insert_entry = ttk.Entry(row_insert_frame, width=4)self.row_insert_entry.pack(side=tk.LEFT, padx=1)self.row_insert_entry.insert(0, "2")# 边距row_margin_frame = tk.Frame(self.row_param_frame, bg="#f0f0f0")row_margin_frame.pack(side=tk.LEFT, padx=2)ttk.Label(row_margin_frame, text="边距:", font=(self.font_family[0], 10))\.pack(side=tk.LEFT, padx=0, pady=0)self.row_margin_entry = ttk.Entry(row_margin_frame, width=4)self.row_margin_entry.pack(side=tk.LEFT, padx=1)self.row_margin_entry.insert(0, "2")# 批注大小设置(行模式也添加)row_comment_size_frame = tk.Frame(self.row_param_frame, bg="#f0f0f0")row_comment_size_frame.pack(side=tk.LEFT, padx=5)ttk.Label(row_comment_size_frame, text="批注宽:", font=(self.font_family[0], 10))\.pack(side=tk.LEFT, padx=0, pady=0)self.row_comment_width_entry = ttk.Entry(row_comment_size_frame, width=4)self.row_comment_width_entry.pack(side=tk.LEFT, padx=1)self.row_comment_width_entry.insert(0, "200")ttk.Label(row_comment_size_frame, text="高:", font=(self.font_family[0], 10))\.pack(side=tk.LEFT, padx=0, pady=0)self.row_comment_height_entry = ttk.Entry(row_comment_size_frame, width=4)self.row_comment_height_entry.pack(side=tk.LEFT, padx=1)self.row_comment_height_entry.insert(0, "150")# 默认显示列模式参数self.col_param_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)self.row_param_frame.pack_forget()# 第三行控制 - 执行按钮control_row3 = ttk.LabelFrame(top_frame, text=" 执行 ", padding=(5, 5, 5, 5))control_row3.pack(fill=tk.X, pady=(5, 0))self.execute_btn = tk.Button(control_row3, text="插入图片批注", command=self.run_matching,font=(self.font_family[0], 10), bg="#2196F3", fg="white",cursor="hand2", padx=10, pady=8)self.execute_btn.pack(side=tk.TOP, padx=(10, 0))# ==================== 中间内容区域 ====================middle_frame = tk.Frame(main_frame, bg="#ffffff", bd=1, relief=tk.SUNKEN)middle_frame.grid(row=1, column=0, sticky="nsew", pady=(0, 10))# 左侧图片预览区域preview_frame = tk.Frame(middle_frame, bg="#ffffff")preview_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)self.preview_label = tk.Label(preview_frame, text="图片预览", bg="#e0e0e0", font=(self.font_family[0], 12))self.preview_label.pack(fill=tk.X, pady=(0, 5))self.image_container = tk.Frame(preview_frame, bg="#f5f5f5")self.image_container.pack(fill=tk.BOTH, expand=True)self.image_label = tk.Label(self.image_container, bg="#f5f5f5")self.image_label.pack(expand=True)# 右侧图片列表区域list_frame = tk.Frame(middle_frame, bg="#ffffff", width=250)list_frame.pack(side=tk.RIGHT, fill=tk.BOTH, pady=5)list_frame.pack_propagate(False)list_header = tk.Frame(list_frame, bg="#e0e0e0", height=25)list_header.pack(fill=tk.X, pady=(0, 5))list_header.pack_propagate(False)list_label = tk.Label(list_header, text="图片列表", bg="#e0e0e0", font=(self.font_family[0], 12))list_label.pack(side=tk.LEFT, padx=(80, 0))# 全选复选框self.select_all_var = tk.BooleanVar(value=False)select_all_check = tk.Checkbutton(list_header, text="全选",variable=self.select_all_var, font=(self.font_family[0], 10),bg="#e0e0e0", cursor="hand2", command=self.toggle_select_all)select_all_check.pack(side=tk.RIGHT, padx=(0, 5))# 创建Canvas和Scrollbarself.canvas = tk.Canvas(list_frame, bg="#ffffff", highlightthickness=0)scrollbar = tk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.canvas.yview)self.canvas.configure(yscrollcommand=scrollbar.set)scrollbar.pack(side=tk.RIGHT, fill=tk.Y)self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)# 创建Canvas中的Frame用于放置图片项self.images_frame = tk.Frame(self.canvas, bg="#ffffff")self.canvas_frame = self.canvas.create_window((0, 0), window=self.images_frame, anchor="nw")# 绑定Canvas的大小变化事件self.images_frame.bind("<Configure>", self.on_frame_configure)self.canvas.bind("<Configure>", self.on_canvas_configure)# ==================== 底部状态区域 ====================bottom_frame = tk.Frame(main_frame, bg="#f0f0f0", height=60)bottom_frame.grid(row=2, column=0, sticky="ew")bottom_frame.grid_propagate(False)# 进度条self.progress_var = tk.DoubleVar()progress_bar = ttk.Progressbar(bottom_frame, variable=self.progress_var,length=100, mode='determinate')progress_bar.pack(fill=tk.X, padx=10, pady=(10, 5))# 状态标签self.status_var = tk.StringVar(value="就绪")status_label = tk.Label(bottom_frame, textvariable=self.status_var, bg="#f0f0f0",font=(self.font_family[0], 10))status_label.pack(pady=(0, 5))def on_frame_configure(self, event):"""当Frame大小变化时,更新Canvas的滚动区域"""self.canvas.configure(scrollregion=self.canvas.bbox("all"))def on_canvas_configure(self, event):"""当Canvas大小变化时,调整内部Frame的宽度"""self.canvas.itemconfig(self.canvas_frame, width=event.width)def select_folder(self):"""选择图片文件夹"""passdef load_images_from_entry(self):"""从输入框加载图片"""passdef on_mode_change(self, event=None):"""模式切换事件"""selected_mode = self.mode_var.get()if selected_mode == "列匹配模式":self.row_param_frame.pack_forget()self.col_param_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)else:self.col_param_frame.pack_forget()self.row_param_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)def toggle_select_all(self):"""全选/取消全选"""passdef run_matching(self):"""执行匹配插入"""passif __name__ == "__main__":root = tk.Tk()app = ExcelImageMatcherPro(root)root.mainloop()
最终将所有的图片按名称匹配插入了图片批注,鼠标悬停就能看到图片,完美实现了之前既定的需求...

通过上面Python自动化脚本,仅用几分钟的时间就完成原需手动操作数小时甚至数天的工作任务。从最初准备手动人工机械操作的麻木到用python实现高效自动化的畅快,工作效率获得指数级提升,终于实现了不加班熬夜的自由!
大佬们也可以举一反三,参照上面的代码思路根据自己工作中的实际情况来具体问题具体分析,实现自己定制化的需求。
结语
当Python遇见办公,牛马打工人终于笑出了猪叫声
【职场人必看】每天早上一睁眼,想到又要面对:
1.📊 堆积如山的Excel表格
2.📑 机械重复的复制粘贴
3.✍️ 永远改不完的各类文档
4.诸如此类的更多........
是不是连Ctrl+Alt+Delete的心都有了?
别慌!别急,摸鱼这位“职场外挂”已经带着Python代码来拯救你了!
友情提示:考虑到没有python环境的朋友需要打包好的成品exe,摸鱼早已贴心打包好,本篇文章代码打包的exe截图如下:

另外,《码海听潮》公众号所有文章码和exe程序已打包好上传绿联nas私有云,有需要的大佬扫一扫上面博主的个人微信二维码,需要的大佬需支付9.9元永久拥有公众号资源(写原创干货费时费力,属实不易),邀请您进入社区群获取下载链接!!,群内提供python办公自动化交流问题,解决问题,且码海听潮微信公众号文章发布会第一时间会更新到群里,非诚勿扰哈!

码海听潮官方社区群如下:


赶紧微信扫一扫下方二维码添加摸鱼君微信
