上周同事拿着一份Excel文件来找我,说工作表被加了保护,密码怎么都想不起来了。他自己上网找了一圈“密码移除工具”,不是要关注公众号就是得付费,还有的下载下来直接给电脑塞了一堆捆绑软件。我当时正好手头不忙,就花了二十来分钟写了一个Python小脚本:拖进去一个xlsx,回车,保护直接清掉,全程不碰密码,也不需要暴力破解。这篇文章就把这个工具的完整思路、代码和几个容易踩的坑都记录下来,给同样被“忘了工作表密码”困扰的人一个可靠的解决方案。
先说清楚:这个工具处理的是工作表级别的密码保护,也就是Excel里“审阅 -> 保护工作表”加的那道锁。它和“打开文件需要密码”是两回事,后者是对整个文件做加密,处理逻辑完全不同,这篇不涉及。另外,在使用这个工具前请确认你拥有该文件的合法处理权限,它最适合的场景就是“文件是自己建的,密码忘了”这种。
1. 先搞懂原理:xlsx加密保护的真相
1.1 工作表密码保护与“文件打开密码”是两码事
很多人一听到“Excel密码”就混为一谈,其实Excel里有好几层完全不同的密码机制:
- 打开密码:对文件本体做加密,不知道密码,Excel连文件内容都读不出来。这属于加密技术范畴,想绕过的唯一途径就是暴力破解或字典攻击,计算量大,耗时不稳定。
- 工作表保护:在Excel界面里通过“审阅 -> 保护工作表”设置。文件本身并没有被加密,Excel完全可以正常打开,只是在修改单元格、插入行列、删除内容等操作上做了权限限制,弹窗提示“工作表已受到保护”。
- 工作簿结构保护:防止用户对工作表进行“重命名、移动、删除、隐藏”等结构操作,密码写在workbook.xml里。
- VBA工程密码:锁的是宏代码,靠修改XML的简单方案无法处理。
本文的“一键移除”针对的是第二种,工作表保护。因为这种情况下,密码信息并没有对Excel文件做真正意义上的加密,它只是写在文件内部的XML结构里。这就给了解除保护一个很大的操作空间。
1.2 藏在XML里的“开关”:sheetProtection到底是什么
xlsx文件本质上是一个zip压缩包。你可以把后缀改成.zip然后直接解压,会看到里面有一堆XML文件、关系文件、资源文件夹。每个工作表对应的内容都在xl/worksheets/目录下,通常是sheet1.xml、sheet2.xml这样的命名。
当你对一个工作表启用保护时,Excel就会在这个sheet对应的XML里写入一个<sheetProtection>节点。正常未加保护的工作表,这个节点是不存在的。
我用一个加了密码的测试文件实际看了一下,保护节点的内容大致长这样:
<sheetProtection sheet="1" objects="1" scenarios="1" password="A5B2C3D4" algorithmName="SHA-512" hashValue="..." saltValue="..." spinCount="100000"/>其中sheet="1"表示“允许编辑”的开关被关掉了,objects="1"表示锁定图形对象,scenarios="1"表示锁定方案。后面的password或hashValue、saltValue、spinCount则是密码的哈希信息,用来验证你输入的密码是否正确。
所以移除工作表保护的思路很直接:找到这些XML文件里对应的<sheetProtection>节点,把它删掉,重新打包成xlsx,Excel打开后就不会再有任何保护限制。
整个过程不需要去逆向Excel的加密算法,也不需要解开密码哈希。它有点像门锁密码忘了,但你不去撬锁,而是直接把那扇门的锁舌拆了——因为门本来就是开着的,锁只是装饰。
1.3 为什么不直接“猜密码”:哈希校验与破解成本
可能有人会问:既然都拿到哈希了,为什么不直接暴力破解密码?这里需要了解一下Excel的密码哈希机制。
旧版Excel(2007之前)使用一种相对简单的XOR哈希算法,网上确实有一些小工具直接算。但从新版Office开始,密码哈希换成了类似PBKDF2的迭代算法:用salt加密码做SHA-512,连续迭代10万次,再把结果存进XML里。这意味着破解一个稍微像样的密码,要么靠GPU跑很久,要么靠字典撞,完全不可控。
而移除<sheetProtection>节点这个操作,时间和密码复杂程度完全无关。哪怕是8位以上的强密码,处理时长也基本相同。这就是“移除保护”和“找密码”在工程成本上的根本区别。
2. 核心实现:从zip入手绕过保护
2.1 为什么不用openpyxl直接改
说到用Python处理Excel,大部分人第一反应是openpyxl。它确实能读写xlsx,也提供了sheet.protection.sheet = False这类解除保护的API,官方文档写得明明白白。那为什么我这里不推荐直接用openpyxl写这个工具?
问题出在openpyxl的保存机制上。openpyxl在读取文件时,会把所有工作表、样式、公式、图片引用、数据透视表、图表等信息解析成Python对象;保存时,再把这堆对象重新序列化成XML。这个过程即便你只改动了一个地方,也可能给文件带来大量不可见的“副作用”:
- 宏文件(.xlsm):直接打开再保存,vbaProject.bin有概率丢失或被破坏。
- 复杂图表、数据透视表缓存:重新序列化后,部分高级结构会损坏或失去原有格式。
- 条件格式、数据验证、自定义样式:某些边缘语法在openpyxl的解析和写出之间会发生微妙变化,导致Excel打开时提示“文件损坏,是否尝试恢复”。
- 最近使用的字体、主题、打印设置:重新保存后可能被重置。
也就是说,openpyxl能处理90%的普通文件,但对于内部结构复杂的文件,一个看起来无关紧要的保存动作,就是给自己的数据埋雷。
我们要做的操作其实非常“外科手术化”:只找出<sheetProtection>节点,把它从XML里摘掉,其他所有内容原封不动。既然这样,最安全的方式就是在zip字节层面直接操作,而不是把整个文件解析重建一遍。
2.2 代码整体结构:解压、手术、回填
整个工具的流程可以拆成四步:
- 用
zipfile打开xlsx文件,遍历所有内部条目,定位到xl/worksheets/目录下的XML文件。 - 读取XML内容,用正则表达式匹配
<sheetProtection ... />节点(注意节点可能带一堆属性,也可能是自闭合标签形式)。 - 对匹配到的节点做移除操作。
- 重新写一个新的zip文件,把处理后的XML回填进去,其他文件保持原样。
这里有一个容易被新手忽略的细节:写新zip时,每个文件条目的压缩方式最好沿用原文件的压缩方式。如果你把原来用ZIP_STORED(不压缩)存储的条目全改成ZIP_DEFLATED,Excel一般也能打开,但某些第三方工具对zip内部结构比较敏感,稳妥起见,尊重原始压缩方式是对的。
2.3 关键代码段逐一拆解(附完整脚本)
先看完整脚本,只有不到50行:
import re import zipfile import shutil from pathlib import Path def remove_sheet_protection(src_path, dst_path=None): src_path = Path(src_path) if dst_path is None: dst_path = src_path.with_name(src_path.stem + "_unlocked" + src_path.suffix) tmp_path = dst_path.with_suffix(".tmp.zip") with zipfile.ZipFile(src_path, "r") as zin: with zipfile.ZipFile(tmp_path, "w") as zout: for item in zin.infolist(): data = zin.read(item.filename) if item.filename.startswith("xl/worksheets/") and item.filename.endswith(".xml"): text = data.decode("utf-8", errors="ignore") new_text, count = re.subn( r'<sheetProtection\b[^>]*?(?:/>|>\s*</sheetProtection>)', '', text, flags=re.IGNORECASE | re.DOTALL ) if count: print(f"[移除] {item.filename}: 删除 {count} 处保护节点") data = new_text.encode("utf-8") zout.writestr(item, data) shutil.move(str(tmp_path), dst_path) return dst_path if __name__ == "__main__": import sys for f in sys.argv[1:]: out = remove_sheet_protection(f) print(f"已生成: {out}")这段代码里最核心的是那个正则表达式:
r'<sheetProtection\b[^>]*?(?:/>|>\s*</sheetProtection>)'解释一下:
<sheetProtection\b:精确匹配标签名的开头,\b防止匹配到<sheetProtectionExtra这类名字。[^>]*?:匹配标签内部属性,非贪婪,直到遇到>。(?:/>|>\s*</sheetProtection>):两种结束形式都要考虑。一种是自闭合写法<sheetProtection ... />,另一种是成对写法<sheetProtection ...></sheetProtection>。实际文件中绝大多数是自闭合,但保险起见两种都处理。
另外用re.subn而不是re.sub,是为了拿到替换次数,方便在命令行输出里确认“这个工作表确实处理过了”。
关于编码:xlsx里的XML默认是UTF-8,之所以加上errors="ignore",是为了防止个别文件里包含特殊二进制字符导致decode直接抛异常。因为我们要处理的只是XML文本,忽略少量异常字符影响不大。
写完这个脚本后,用我们前面那个测试文件跑一下,输出效果是这样的:
[移除] xl/worksheets/sheet1.xml: 删除 1 处保护节点 已生成: test_unlocked.xlsx用Excel打开test_unlocked.xlsx,直接就能编辑单元格了,保护弹窗消失。再用zip解压看里面的XML,<sheetProtection>节点确实已经不在了。
3. 一键体验:从命令行到拖拽GUI
3.1 批处理模式:一次清空整个文件夹
上面的脚本已经支持命令行传多个文件:
python remove_protect.py 文件1.xlsx 文件2.xlsx 文件3.xlsx但日常最方便的是“拖拽式操作”,把多个文件直接拖到脚本或批处理文件上。Windows下可以写一个.bat文件:
@echo off cd /d %~dp0 python remove_protect.py %* pause把remove_protect.py和这个.bat放在同一个目录,以后把xlsx文件拖到这个.bat上,松开鼠标,就会自动调用Python脚本逐个处理。所有输出文件都以原文件名_unlocked.xlsx的格式保存在同目录,不会覆盖原始文件,这就天然带了一层“安全备份”的属性。
如果还想更彻底一些,可以在脚本里加一个参数来控制是否覆盖原文件。不过我的习惯是永远保留一份原文件,万一移除后Excel仍提示异常,还能切回原始文件排查,不至于把自己逼进死胡同。
3.2 用tkinter写个简单的窗体
命令行工具对习惯图形界面的同事来说还是有点门槛。如果想要一个真正的“一键移除”体验,可以用Python自带的tkinter写一个极简GUI:一个按钮选文件,一个按钮开始处理,一个文本框输出日志。
import tkinter as tk from tkinter import filedialog, messagebox from remove_protect import remove_sheet_protection def pick_files(): files = filedialog.askopenfilenames( title="选择要解锁的Excel文件", filetypes=[("Excel工作簿", "*.xlsx *.xlsm")] ) log_text.delete("1.0", tk.END) for f in files: try: out = remove_sheet_protection(f) log_text.insert(tk.END, f"成功: {f} -> {out}\n") except Exception as e: log_text.insert(tk.END, f"失败: {f} -> {e}\n") if files: messagebox.showinfo("完成", "处理结束,请查看下方日志。") root = tk.Tk() root.title("Excel工作表保护一键移除") btn = tk.Button(root, text="选择文件", command=pick_files, width=30, height=3) btn.pack(pady=20) log_text = tk.Text(root, height=12, width=80) log_text.pack(padx=10, pady=10) root.mainloop()界面上就一个按钮,选择文件后自动批量处理,日志区会显示每个文件的处理结果。这个GUI对不懂命令行的用户已经足够友好。
3.3 打包成exe给同事用
如果不希望依赖Python环境,可以用PyInstaller打包:
pip install pyinstaller pyinstaller -F -w remove_protect_gui.py-F表示打成一个单独的exe,-w表示运行时不弹出黑色控制台窗口(因为GUI模式下不需要)。打包完在dist目录下就能找到exe文件。
这里有两个实操提醒:
- 杀毒软件误报。这类带有“移除密码”功能的工具容易被杀软判定为风险程序,因为行为特征和某些破解工具相似。自用的没问题,发给同事如果被杀软拦截,可以考虑让同事加白名单,或者干脆直接分享源码让对方在本地跑。
- 体积问题。PyInstaller打的exe通常有20到50MB,因为内置了Python解释器和相关库。如果介意体积,建议用conda或venv创建一个精简环境再打包;如果只是自己用,直接跑源码反而最省事。
4. 边界、坑与安全提醒
4.1 老版.xls文件为什么不管用
细心的读者会发现,脚本里我只处理了.xlsx和.xlsm,没有处理老版本的.xls。原因是.xls是Excel 97-2003时代的二进制格式(BIFF),根本不是zip结构,内部也没有sheetProtection这种XML节点。
如果手头只有.xls文件,可以先在Excel里另存为.xlsx格式,再用脚本移除保护。注意,另存为的操作要求你先能打开文件——.xls的工作表保护不会阻止文件打开,所以这点是可以做到的。批量场景下,也可以用LibreOffice的命令行参数做格式转换:
soffice --headless --convert-to xlsx "旧文件.xls"转换完成后再跑脚本,虽然多了一步,但总算把老格式也覆盖到了。
4.2 容易被误解的“保护”边界
这篇工具处理的是工作表级别保护,对应的是xlsx内部xl/worksheets/sheetN.xml里的节点。有几类情况它处理不了:
- 工作簿结构保护:密码信息在
xl/workbook.xml里的<workbookProtection>节点。如果你想禁止别人重命名、删除、隐藏工作表,那属于结构保护,需要修改的是另一个文件。思路类似,但节点位置不同。 - 共享工作簿保护:在“审阅 -> 共享工作簿”里设置的保护,处理逻辑更复杂,而且Excel 365已经在逐步移除相关功能。
- VBA工程密码:宏代码保护写在
vbaProject.bin内部,靠修改XML是不行的,需要专门的VBA密码处理手段。 - 打开文件密码:这是对整个文件流的加密,zip解压出来的内容本身就是密文,连
sheet1.xml都读不到。
很多人拿着这个脚本去处理“打开时需要密码”的Excel,然后发现毫无效果,其实就是没弄明白这几个保护层级之间的区别。建议在使用工具前先确认清楚:你的文件能正常打开、能看见数据,只是单元格不让编辑,那才是这个工具的适用范围。
4.3 几个容易忽略的细节坑
实际测试过程中,我遇到过几个不在预料之内的情况,记录一下:
- 空工作表节点。如果某个工作表名对应的XML里没有
<sheetProtection>节点,正则替换次数为0,脚本会跳过,不会报错。这是正常的。 - 文件中多个工作表受保护。
xl/worksheets/目录下每个sheet对应的XML都会检查,因此支持批量移除同一份文件里多个工作表的保护。找不到就是没有。 - 主控XML里也可能带保护信息。如果你用了开发者工具自定义控件,或者在
xl/drawings/、xl/charts/等位置也存在与受保护工作表相关的缓存定义,可能需要一并检查。不过就常规场景而言,工作表保护就存在sheetN.xml里,上面的脚本已经覆盖到了。 - 超大文件的内存占用。
zin.read(item.filename)会把整个文件条目读进内存,一般几十MB的Excel没什么压力。但如果遇到几十万行、几百MB的超大工作簿,内存会比较高,建议在脚本里用item.file_size大致判断一下再决定是否读取。
4.4 安全与合规提醒
严格来说,这个工具移除的是“保护开关”,并不是“找回密码”。它不产生任何密码,也不会让你知道原来设置的密码是什么。它适合的文件,是你自己设置过密码又忘记的,或者是已经获得权限可以解除保护的文档。
出于安全习惯,建议处理前先复制一份原文件备份,防止操作过程中文件损坏。虽然字节级修改已经很保守,但备份是零成本的,万一出现意外也不会慌。
5. 还能怎么扩展:从移除到改密
5.1 保留保护但替换密码的玩法
有时候你不是想完全移除保护,而是想“换一个自己记得住的密码”。这种情况下,没必要先移除再重新保护,因为那样会产生两次文件修改流程。更优雅的做法是:在XML里直接替换掉<sheetProtection>节点的哈希属性。
旧版Excel的密码哈希算法相对简单,如果文件里的<sheetProtection>用的是password="XXXX"这种四字符哈希格式,可以直接用Python计算新密码的哈希值然后替换:
def legacy_hash(pwd): h = 0 for ch in pwd: h = h ^ ((h << 5) & 0xffffffff) + ord(ch) return format(h & 0xffff, '04X')不过新版Office插入了saltValue和spinCount机制,直接手工构造比较麻烦。更省事的方案是:先用上面的工具移除保护,再用openpyxl重新设置一个你自己记得住的密码:
from openpyxl import load_workbook wb = load_workbook("test_unlocked.xlsx") ws = wb.active ws.protection.sheet = True ws.protection.password = "新密码" wb.save("test_relocked.xlsx")这个做法会引入openpyxl重新保存的格式风险,所以我的建议是:如果文件结构简单、没有大量宏和图表,直接这么干没问题;如果文件很复杂,还是保留“移除保护”的版本,平时通过文件系统的权限管理来控制访问。
5.2 更进一步:把脚本打包成“右键菜单”工具
前面提到了拖拽到.bat和GUI两种交互方式,其实Windows下还有一个体验更好做法:把自己写的脚本注册到资源管理器的“发送到”菜单里,这样在任意Excel文件上右键选择“发送到 -> 移除工作表保护”,就能直接处理,连拖拽都省了。
在资源管理器地址栏输入shell:sendto,打开“发送到”目录,把脚本的快捷方式放进去即可。如果是脚本,需要手动创建一个.cmd批处理文件放在那里:
python "D:\scripts\remove_protect.py" %1 pause之后每次右键文件,都能直接把文件路径传进去。这个操作对Windows 10和Windows 11都有效。
5.3 日志与错误输出
如果要把工具交给不太会用电脑的人,建议在批处理和GUI版本里都加上日志功能,哪怕是简单的打印输出也行。别小看这行日志,它能帮你判断文件是不是真的处理成功。比如某些文件虽然扩展名是xlsx,但内部可能有损坏,zipfile读取时会报错,有日志你就能定位是哪一步出了问题。
我在使用中积累的习惯是:处理一份文件的时间记录一下,如果某个文件处理时长明显异常,多半是文件内部有我们没预料的特殊结构,需要单独检查。
这个工具从写出来到现在,我自己用了很多次,也被同事借走了好几回,处理过最大的一个文件是一份带数据透视表和条件格式的19MB工作簿,用ZipFile方式全程只用了不到3秒,文件打开完全正常。相比网上下载那些收费工具的提心吊胆,自己写脚本控制全过程,至少所有的改变都由自己掌控。如果有这方面需求,建议直接拿代码去跑一下,改动不多,却能省下大量重复劳动。