当前位置:首页>python>《"告别手动保存!用Python打造支持Word/Excel/ZIP的批量图片提取工具"》

《"告别手动保存!用Python打造支持Word/Excel/ZIP的批量图片提取工具"》

  • 2026-10-11 08:12:34
《"告别手动保存!用Python打造支持Word/Excel/ZIP的批量图片提取工具"》

“带你横跨办公自动化的数据江海”

@摸鱼

闻道有先后,术业有专攻。

各位大佬朋友们好

~我依旧是你们的老朋友摸鱼~

♥ 1

在职场摸爬滚打的这十多年里,我用Python悄悄干了不少“正事”——不知不觉攒下了一整套办公自动化的实用项目技巧。去年10月初创立了公众号 「码海听潮」 ,初衷很简单:把重复的劳动交给代码,把摸鱼的时间留给生活。

目前已经吭哧吭哧更新了100多篇原创文章,每一篇都是实操干货,不讲虚的,只聊怎么用代码真正解放双手,帮大家早点下班、准点摸鱼

♥ 2

♥ 3好叻,多了不说,少了不唠,咱直接上干货。

办公需求场景

从崩溃到优雅的进化

有一个神秘的根目录文件夹,其中包含多个子文件夹以及多种类型的文档(如 Word、Excel、ZIP 压缩包)。这些文档内部都包含图片。现在的需求是将该文件夹下所有文档中的图片批量提取出来,并按原文档结构或统一目录进行归档保存。若这种类似的需求你的Big Boss安排大佬你去完成,请问阁下该如何应对?

  • 需求文件夹列表:

  • 需求图片文件:

办公痛点分析

01

 痛点1:操作繁琐且耗时巨大

    • 人工操作需要逐一打开每个Word、Excel文件,手动定位图片,然后右键“另存为”到本地。如果是ZIP压缩包,还得多一步“解压 → 打开 → 提取 → 再压缩/删临时文件”的流程。当文件数量成百上千时,这种纯手工点击会占用数小时甚至数天的工作时间。

    02

     痛点2:人工操作易出错

      • 在反复的点开、另存、关闭文件过程中,人眼很容易疲劳,导致漏掉某些页面角落的小图片,或者把图片存错了文件夹(例如把“报告A”的图存到了“报告B”目录下)。此外,有些文档里可能还嵌着图片格式的图表,手动操作时容易因疏忽而只提取了一部分。

      03

      痛点3:无法高效处理压缩包嵌套

      • ZIP文件里可能还包含其他ZIP文件或带有图片的文档。人工处理时,需要手动解压多层、逐级查看。一旦嵌套层次较多(比如压缩包套文档、文档里又套OLE对象),普通办公人员几乎无法完整且不遗漏地提取出所有图片,导致数据资产丢失。

      由此可见,若操作成千上百个word/excel/zip文件的话,整个操作流程繁琐且耗时,高频次的鼠标点击和键盘输入使操作者手指疲劳,堪称"键盘敲冒烟"式的体力劳动,加上人工疲劳操作极易导致遗漏文件夹。于是乎这时候,按以往的 “解题套路”,Python 的专属 BGM 该响起来了 ——go~ go~ go~,救苦救难的大救星这不就来了!!勇士战歌搞起来.......

      @摸鱼

      问题拆解思路

      1. 遍历根文件夹,筛选过滤word/excel/zip文档组成文件列表

      2. 循环文件列表,对不同格式的文件采用不同提取图片的方法

      3. 将提取出来的图片进行重新保存到文件夹

      下面,我就用python代码让各位大佬见识一下,什么叫"传统文化遇上赛博效率"(仅展示部分代码,非完整代码,需完整代码看文章末尾说明~)

      import tkinter as tkfrom tkinter import ttk, filedialog, messageboxfrom tkinter.font import Fontclass ImageUnzipTool:    def __init__(self, root):        self.root = root        self.root.title("zip/word/excel多文档图片批量提取工具(欢迎关注微信公众号:码海听潮)")        self.root.geometry("760x615")        self.root.resizable(False, False)        # 设置主题风格        self.style = ttk.Style()        self.style.theme_use('clam')        # 自定义字体        self.title_font = Font(family="微软雅黑", size=16, weight="bold")        self.label_font = Font(family="微软雅黑", size=10)        self.button_font = Font(family="微软雅黑", size=10, weight="bold")        # 初始化变量        self.input_path = tk.StringVar()        self.output_dir = tk.StringVar()        self.quality = tk.StringVar(value="95")        # 默认开启递归处理,保持目录结构        self.is_recursive = True        self.preserve_structure = True        self.selected_files = []        self.total_files = 0        self.processed_files = 0        self.is_processing = False        self.temp_zip_paths = []        self.supported_extensions = ('.zip', '.xlsx', '.xls', '.docx', '.doc')        # 创建UI        self.create_widgets()        # 应用配色方案        self.apply_styles()    def apply_styles(self):        # 主色调        primary_color = "#4A7ABC"        secondary_color = "#E0E7FF"        accent_color = "#6366F1"        neutral_color = "#F5F7FA"        # 配置样式        self.style.configure('TFrame', background=neutral_color)        self.style.configure('TLabel', background=neutral_color, font=self.label_font)        self.style.configure('Title.TLabel', font=self.title_font, background=primary_color, foreground='white')        self.style.configure('TButton', font=self.button_font, background=primary_color, foreground='white')        self.style.configure('Accent.TButton', font=self.button_font, background=accent_color, foreground='white')        self.style.configure('TProgressbar', background=accent_color)        self.style.configure('Header.TFrame', background=primary_color)        self.style.configure('FileList.TFrame', background=secondary_color)        self.style.configure('Control.TFrame', background=secondary_color)    def create_widgets(self):        # 主内容区域        main_frame = ttk.Frame(self.root, padding="20")        main_frame.pack(expand=True, fill=tk.BOTH)        # 输入文件夹选择区域        input_frame = ttk.LabelFrame(main_frame, text="选择要处理的文件夹", padding=10)        input_frame.pack(fill=tk.X, pady=5)        # 文件夹路径显示        path_frame = ttk.Frame(input_frame)        path_frame.pack(fill=tk.X)        path_entry = ttk.Entry(path_frame, textvariable=self.input_path, width=50)        path_entry.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)        # 为文件夹路径输入框添加工具提示        ToolTip(path_entry, "选择包含ZIP、Excel或Word文件的文件夹,支持递归搜索子文件夹")        select_folder_btn = ttk.Button(            path_frame,             text="选择文件夹",             command=self.select_folder,            style='Accent.TButton',            width=15        )        select_folder_btn.pack(side=tk.RIGHT, padx=5)        # 输出目录选择        output_frame = ttk.LabelFrame(main_frame, text="输出目录", padding=10)        output_frame.pack(fill=tk.X, pady=5)        output_entry = ttk.Entry(output_frame, textvariable=self.output_dir, width=50)        output_entry.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)        # 为输出目录输入框添加工具提示        ToolTip(output_entry, "设置提取图片的保存位置,如不设置,将自动在源文件夹同级创建extracted_images文件夹")        output_btn = ttk.Button(output_frame, text="选择目录", command=self.select_output_dir)        output_btn.pack(side=tk.RIGHT, padx=5)        # 图片质量设置        quality_frame = ttk.LabelFrame(main_frame, text="图片质量设置", padding=10)        quality_frame.pack(fill=tk.X, pady=5)        quality_control_frame = ttk.Frame(quality_frame)        quality_control_frame.pack(fill=tk.X)        quality_label = ttk.Label(quality_control_frame, text="保存质量 (1-100):")        quality_label.pack(side=tk.LEFT, padx=5)        self.quality_entry = ttk.Entry(quality_control_frame, textvariable=self.quality, width=20, font=('微软雅黑', 10))        self.quality_entry.pack(side=tk.LEFT, padx=10)        # 为质量输入框添加工具提示        ToolTip(self.quality_entry, "低质量(60)-中等质量(80)\n高质量(95)-无损(100)")        # 验证输入的质量值        def validate_quality(*args):            try:                value = self.quality.get().strip()                if value:                    quality_value = int(value)                    if quality_value < 1 or quality_value > 100:                        self.quality.set("95")                else:                    self.quality.set("95")            except ValueError:                self.quality.set("95")        # 绑定验证事件        self.quality.trace_add("write", validate_quality)        # 文件列表区域 - 可以滚动和调整大小        list_frame = ttk.LabelFrame(main_frame, text="发现的文件及图片预览", padding=10)        list_frame.pack(fill=tk.BOTH, expand=True, pady=5)  # 改为 expand=True        list_frame.pack_propagate(False)  # 禁止自动调整大小        list_frame.configure(height=250)  # 设置最小高度        # 创建Canvas和滚动条        canvas = tk.Canvas(list_frame, bg="#F0F0F0", highlightthickness=0)        scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)        self.file_list_frame = ttk.Frame(canvas, style='FileList.TFrame')        # 设置Canvas滚动区域        self.file_list_frame.bind(            "<Configure>",            lambda e: canvas.configure(                scrollregion=canvas.bbox("all")            )        )        # 鼠标滚轮支持        def on_mousewheel(event):            canvas.yview_scroll(int(-1*(event.delta/120)), "units")        canvas.bind_all("<MouseWheel>", on_mousewheel)        canvas.create_window((0, 0), window=self.file_list_frame, anchor="nw")        canvas.configure(yscrollcommand=scrollbar.set)        canvas.pack(side="left", fill="both", expand=True)        scrollbar.pack(side="right", fill="y")        # 开始批量提取按钮 - 直接在主框架中居中        self.process_btn = ttk.Button(            main_frame,             text="🚀 开始批量提取",             command=self.process_files,            style='Accent.TButton',            width=20        )        self.process_btn.pack(pady=10, ipadx=20)        # 为处理按钮添加工具提示        ToolTip(self.process_btn, "开始批量提取所有找到的图片文件\n将会保持原有的文件夹结构")        # 统计信息        stats_frame = ttk.Frame(main_frame)        stats_frame.pack(fill=tk.X, pady=5)        self.stats_var = tk.StringVar(value="")        stats_label = ttk.Label(stats_frame, textvariable=self.stats_var, foreground="#666666")        stats_label.pack(side=tk.RIGHT)    def select_folder(self):        """选择文件夹"""        self.cleanup_temp_files()        folder_path = filedialog.askdirectory(title="选择包含文件的文件夹")        if folder_path:            self.input_path.set(folder_path)            self.load_files_from_folder(folder_path)            # 自动设置输出目录(在输入文件夹同级创建输出文件夹)            if not self.output_dir.get():                default_output = os.path.join(os.path.dirname(folder_path), "extracted_images")                self.output_dir.set(default_output)    def load_files_from_folder(self, folder_path):        """从文件夹加载所有支持的文件"""        self.clear_file_list()        self.selected_files = []        files = []        try:            # 递归遍历文件夹(默认开启)            for root, dirs, filenames in os.walk(folder_path):                for filename in filenames:                    if filename.lower().endswith(self.supported_extensions):                        full_path = os.path.join(root, filename)                        files.append(full_path)            if files:                self.load_files(files, folder_path)                self.update_stats()            else:                ttk.Label(self.file_list_frame, text="未找到支持的文件格式 (.zip, .xlsx, .xls, .docx, .doc)").pack(pady=10)                self.stats_var.set("")        except Exception as e:            messagebox.showerror("错误", f"加载文件夹失败: {str(e)}")    def load_files(self, file_paths, base_folder=None):        """加载文件列表并预览内容"""        self.clear_file_list()        self.selected_files = []        for file_path in file_paths:            if not os.path.exists(file_path):                continue            file_ext = os.path.splitext(file_path)[1].lower()            relative_path = ""            if base_folder:                relative_path = os.path.relpath(file_path, base_folder)            try:                # 确定要使用的ZIP路径                if file_ext in ['.xlsx', '.docx']:                    # 创建临时ZIP文件                    temp_dir = tempfile.mkdtemp()                    temp_zip_path = os.path.join(temp_dir, f"temp_{os.getpid()}_{len(self.temp_zip_paths)}.zip")                    shutil.copy2(file_path, temp_zip_path)                    self.temp_zip_paths.append(temp_zip_path)                    zip_to_check = temp_zip_path                elif file_ext == '.zip':                    zip_to_check = file_path                else:                    continue                # 检查ZIP中的图片文件                with ZipFile(zip_to_check) as zf:                    image_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff')                    image_files = [item for item in zf.filelist if item.filename.lower().endswith(image_extensions)]                    if image_files:                        # 创建文件组显示                        self.create_file_group_display(file_path, relative_path, image_files)                        self.selected_files.append({                            'original_path': file_path,                            'zip_path': zip_to_check,                            'images': image_files,                            'relative_path': relative_path                        })            except Exception as e:                print(f"警告: 无法读取文件 {file_path}: {str(e)}")                continue    def create_file_group_display(self, file_path, relative_path, image_files):        """创建文件组显示"""        # 文件组容器        group_frame = ttk.Frame(self.file_list_frame, style='FileList.TFrame', relief='solid', borderwidth=1)        group_frame.pack(fill=tk.X, pady=3, padx=5)        # 文件头部信息        header_frame = ttk.Frame(group_frame)        header_frame.pack(fill=tk.X, padx=10, pady=5)        # 文件类型图标        file_ext = os.path.splitext(file_path)[1].lower()        if file_ext == '.zip':            icon = "📦"        elif file_ext in ['.xlsx', '.xls']:            icon = "📊"        elif file_ext in ['.docx', '.doc']:            icon = "📝"        else:            icon = "📄"        icon_label = ttk.Label(header_frame, text=icon, font=('微软雅黑', 12))        icon_label.pack(side=tk.LEFT, padx=5)        # 文件信息        info_frame = ttk.Frame(header_frame)        info_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)        file_name = os.path.basename(file_path)        name_label = ttk.Label(info_frame, text=file_name, font=('微软雅黑', 10, 'bold'))        name_label.pack(anchor=tk.W)        if relative_path:            path_label = ttk.Label(info_frame, text=f"📂 {relative_path}", font=('微软雅黑', 8), foreground="#666666")            path_label.pack(anchor=tk.W)        # 图片数量统计        stats_frame = ttk.Frame(header_frame)        stats_frame.pack(side=tk.RIGHT, padx=5)        count_label = ttk.Label(stats_frame, text=f"🖼️ {len(image_files)} 张图片", font=('微软雅黑', 9, 'bold'), foreground="#4A7ABC")        count_label.pack()        # 预览图片列表(折叠显示)        if len(image_files) > 0:            preview_frame = ttk.Frame(group_frame)            preview_frame.pack(fill=tk.X, padx=20, pady=(0, 5))            # 显示前3张图片            for i, img in enumerate(image_files[:3]):                img_name = os.path.basename(img.filename)                img_label = ttk.Label(preview_frame, text=f"├─ {img_name}", font=('微软雅黑', 8))                img_label.pack(anchor=tk.W)            if len(image_files) > 3:                more_label = ttk.Label(preview_frame, text=f"└─ ... 还有 {len(image_files) - 3} 张图片", font=('微软雅黑', 8), foreground="#666666")                more_label.pack(anchor=tk.W)    def update_stats(self):        """更新统计信息"""        total_files = len(self.selected_files)        total_images = sum(len(item['images']) for item in self.selected_files)        # 统计文件类型        file_types = {}        for item in self.selected_files:            ext = os.path.splitext(item['original_path'])[1].lower()            if ext in file_types:                file_types[ext] += 1            else:                file_types[ext] = 1        stats_text = f"统计: {total_files} 个文件 | {total_images} 张图片 "        for ext, count in file_types.items():            stats_text += f"| {ext}: {count} "        self.stats_var.set(stats_text)    def clear_file_list(self):        """清空文件列表"""        for widget in self.file_list_frame.winfo_children():            widget.destroy()    def select_output_dir(self):        """选择输出目录"""        dir_path = filedialog.askdirectory(title="选择输出目录")        if dir_path:            self.output_dir.set(dir_path)    def get_quality_value(self):        """获取有效的质量值"""        try:            value = self.quality.get().strip()            if value:                quality = int(value)                if 1 <= quality <= 100:                    return quality        except ValueError:            pass        return 95    def process_files(self):        """开始处理文件"""        if self.is_processing:            return        output_path = self.output_dir.get()        if not self.selected_files:            messagebox.showwarning("警告", "没有找到可处理的文件,请先选择文件夹")            return        if not output_path:            # 如果没有设置输出目录,自动创建            input_folder = self.input_path.get()            output_path = os.path.join(os.path.dirname(input_folder), "extracted_images")            self.output_dir.set(output_path)        # 验证质量值        quality_value = self.get_quality_value()        self.quality.set(str(quality_value))        total_images = sum(len(item['images']) for item in self.selected_files)        # 确认对话框        confirm_msg = f"""准备开始批量提取图片:📁 源文件夹: {self.input_path.get()}📂 输出目录: {output_path}📦 文件总数: {len(self.selected_files)}🖼️ 图片总数: {total_images}🔄 递归处理: 是📊 保持目录结构: 是🎨 图片质量: {quality_value}%是否继续?"""        if messagebox.askyesno("确认批量处理", confirm_msg):            self.is_processing = True            self.process_btn.config(state=tk.DISABLED)            self.processed_files = 0            thread = threading.Thread(target=self.process_files_thread)            thread.daemon = True            thread.start()    def process_files_thread(self):        """处理文件的线程"""        try:            quality = self.get_quality_value()            output_path = self.output_dir.get()            for file_item in self.selected_files:                if not self.is_processing:                    break                try:                    zip_path = file_item['zip_path']                    images = file_item['images']                    relative_path = file_item['relative_path']                    source_filename = os.path.splitext(os.path.basename(file_item['original_path']))[0]                    # 保持原目录结构                    if relative_path:                        # 获取文件所在目录的相对路径                        base_dir = os.path.dirname(relative_path)                        file_output_dir = os.path.join(output_path, base_dir, source_filename)                    else:                        file_output_dir = os.path.join(output_path, source_filename)                    # 创建输出目录                    os.makedirs(file_output_dir, exist_ok=True)                    with ZipFile(zip_path) as zf:                        for image_item in images:                            if not self.is_processing:                                break                            try:                                image_name = image_item.filename                                safe_name = os.path.basename(image_name)                                # 清理文件名                                safe_name = "".join(c for c in safe_name if c.isalnum() or c in "._- ")                                output_file = os.path.join(file_output_dir, safe_name)                                # 如果文件已存在,添加数字后缀                                output_file = self.get_unique_filename(output_file)                                # 读取并保存图片                                img_data = zf.read(image_name)                                img = Image.open(BytesIO(img_data))                                # 转换RGBA到RGB(如果需要)                                if img.mode == 'RGBA' and img.format and img.format.lower() == 'jpeg':                                    img = img.convert('RGB')                                if img.format and img.format.lower() == 'jpeg':                                    img.save(output_file, quality=quality, optimize=True)                                else:                                    img.save(output_file)                                self.processed_files += 1                                time.sleep(0.01)                            except Exception as e:                                print(f"处理图片 {image_name} 时出错: {str(e)}")                                continue                except Exception as e:                    print(f"处理文件 {file_item['original_path']} 时出错: {str(e)}")                    continue            if self.is_processing:                # 处理完成                success_msg = f"""批量提取完成!📊 处理结果:✅ 成功提取: {self.processed_files} 张图片📁 源文件数: {len(self.selected_files)}📂 输出目录: {output_path}🎨 使用质量: {quality}%图片已按原目录结构保存。"""                messagebox.showinfo("处理完成", success_msg)                # 询问是否打开输出文件夹                if messagebox.askyesno("提示", "是否打开输出文件夹?"):                    os.startfile(output_path)        except Exception as e:            messagebox.showerror("错误", f"处理过程中出现错误: {str(e)}")        finally:            self.is_processing = False            self.process_btn.config(state=tk.NORMAL)            self.cleanup_temp_files()    def get_unique_filename(self, filepath):        """如果文件已存在,添加数字后缀"""        if not os.path.exists(filepath):            return filepath        directory = os.path.dirname(filepath)        filename = os.path.basename(filepath)        name, ext = os.path.splitext(filename)        counter = 1        while True:            new_filename = f"{name}_{counter}{ext}"            new_filepath = os.path.join(directory, new_filename)            if not os.path.exists(new_filepath):                return new_filepath            counter += 1    def cleanup_temp_files(self):        """清理所有临时文件"""        for temp_path in self.temp_zip_paths:            if temp_path and os.path.exists(temp_path):                try:                    temp_dir = os.path.dirname(temp_path)                    shutil.rmtree(temp_dir)                except Exception as e:                    print(f"警告: 清理临时文件失败: {str(e)}")        self.temp_zip_paths = []if __name__ == "__main__":    root = tk.Tk()    app = ImageUnzipTool(root)    # 设置窗口图标    try:        root.iconbitmap("icon.ico")    except:        pass    root.mainloop()

      运行程序后所有的word/excel/zip文档都提取出了所有图片且自动归类,完美实现了之前既定的需求.......

      通过上面Python自动化脚本,仅用十分钟的时间就完成原需手动操作数小时甚至数天的工作任务。从最初准备手动人工机械操作的麻木到用python实现高效自动化的畅快,工作效率获得指数级提升,终于实现了不加班熬夜的自由!

      大佬们也可以举一反三,参照上面的代码思路根据自己工作中的实际情况来具体问题具体分析,实现自己定制化的需求。

      结语

      当Python遇见办公,牛马打工人终于笑出了猪叫声

      【职场人必看】每天早上一睁眼,想到又要面对:

      1.📊 堆积如山的Excel表格

      2.📑 机械重复的复制粘贴

      3.✍️ 永远改不完的各类文档

      4.诸如此类的更多........

      是不是连Ctrl+Alt+Delete的心都有了?

      别慌!别急,摸鱼这位“职场外挂”已经带着Python代码来拯救你了!

      友情提示:考虑到没有python环境的朋友需要打包好的成品exe,摸鱼早已贴心打包好,本篇文章代码打包的exe截图如下:

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

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

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

      最新文章

      随机文章