Excel歪门邪道(1) — 用C#解决Excel修复文件后批注被删除的问题
今天是2024年9月18日,中秋节刚过,狠下心来决定把原来发在CSDN的文章全部搬过来,倒不是因为文章被CSDN拿去白嫖或者是担心CSDN倒闭,而是觉得自己过去的29年生活自己活的像个小丑,如今已到而立之年被社会彻底淘汰已成定局,因此在离开这个世界之前,我还是决定把过往的一些没什么用的经验重新用文字记录在博客园,哪天我真的死了,我起码在园子发过一点稍微有用的东西,至于有多少用,就很难讲了,毕竟有了AI以后,AI势必会接管一切,但是学习知识的目的却从未变过,因为当你有互联网时你能用AI,等到你成单机了的时候,你连百度都用不了了,一些很简单的问题你也束手无策的时候,那才是AI战胜人类真正完成闭环。
本篇是Excel歪门邪道第一篇,问题要追溯到我第一份工作时遇到的问题,当然,文章本身并无意义,因为现在普遍用WPS的情况下,批注的问题已经被WPS消灭了,本文探讨的是问题出现的原因以及用Winform C#程序完成该操作的思路
首先批注被删的表现形式:其一是常见于Excel2007打开别人发的Excel工作簿文件时,显示文件有错误,修复文件后显示已删除批注,然后别人写的批注全都没了;其二是xlsx文件都有该问题,但是xls文件安然无恙
这里就要提到两个问题:第一为什么批注会被删,第二为什么xls文件不会有这个报错
批注被删的主要原因是Excel2007无法识别批注,具体点就是Excel2007程序无法正常识别之后版本的一些表格外的对象(object)包括但不限于:批注、图形、数据验证等等,这和微软OpenXML标准也有关系,从Excel2007以后,微软开始用了基于OpenXML的一中新的文件格式:xlsx,同样的Word的叫docx,PowerPoint叫pptx,三者其实都是基于一个技术的产物,然而微软在迭代Office用的OpenXML标准的时候并没有向后兼容的问题,这就为早期版本的Office套件不兼容标准埋下了祸根。
按照Excelhome论坛用户jaffedream在论坛回帖中的说法,Excel2007开始识别批注异常的问题要追溯到批注在XML文件中的记录方式,在Excel2007以后的版本中,comments*.xml文件中对批注记录多了一个ShapeId="0"的节点,这就导致Excel2007等版本中,解析器在解析XML文件时无法识别到这个节点,因此会提示用户文件有错误,询问是否修复。但是这里肯定是不能点确定去修复的,因为一旦让Excel修复,由于Excel还是不认识这个节点,最后为了文件能被正常读取,还是会直接删掉整个批注在的项,等于没有修复,关键的信息还是丢失了。
至于xls文件为什么不会有这个报错,因为xls本来就是为了兼容97-2003期间的标准,这个标准已经定型不会发生任何变化了,也因为这个原因,xls兼容性很强,很难出现文件异常的问题,但这并不代表xls就是万用解,首先xls文件不支持后续的一些功能,例如切片器、一些后来更新的公式等等,甚至是条件格式、表格样式也会发生变化1,其次是表格范围,xls的列最大只能支持到256列,行最大只能支持到65536行,而xlsx的这两项分别最大支持16384列和1048576行,这也是为什么xls文件中如果引用xlsx文件的列直接用类似$A:$A之类的方法引用时,会提示无效引用,该文件版本包含的公式中不能引用范围外区域的问题,然后是宏的问题,xlsx默认是不允许宏(marco)存在的,如果要允许使用宏必须另存为xlsm文件,而xls是允许嵌入宏代码的,而这并不安全,因为如果你的电脑默认是允许宏启动的(虽然微软知道宏不安全默认给关了),如果xls被加入了恶意的宏代码,那么随着文件打开就会自动运行宏病毒,但你说xlsx不允许宏存在是不是就万事大吉了呢?未必——因为CSDN上的有人已经发现了瞒天过海的方法,这个有机会再说。
所以讲了一大通废话,关键要修复这个问题就几步(甚至可以自己手动改过来):
1.解压缩文件结构,把comments文件解压出来
2.删除comments文件中所有的ShapeId="0"
3.把修改过的comments文件重新打回压缩包(至于为什么不能新建一个压缩包把所有文件直接打进去,因为Excel工作簿和压缩包打包方式不一样,这样做Excel程序识别不了)
下面是实现的代码,解压缩模块用到了SharpZip,因为是刚学不久,又抄了别人不少的实现逻辑,代码的可读性就不用苛求了,能跑就行
using System;
using System.Text;
using System.Windows.Forms;
using System.IO.Compression;
using System.IO;
using CZip = ICSharpCode.SharpZipLib.Zip;
private void Button1_Click(object sender, EventArgs e)
{
int i = 0;
OpenFileDialog path = new OpenFileDialog();
//文件类型过滤
path.Filter = @"待修复Excel表格|*.xlsx";
//展开文件选择对话框
DialogResult result = path.ShowDialog();
if (result == DialogResult.OK)
{
//获取导出文件夹目录
string expath = path.FileName + @"_Fix";
if(Directory.Exists(expath))
{
DialogResult fg= MessageBox.Show("导出数据已存在!是否覆盖?覆盖后将失去所有已导出数据!", "警告", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (fg == DialogResult.Yes)//确认删除
{
//对重复文件夹进行删除操作
DirectoryInfo dl = new DirectoryInfo(expath);
dl.Delete(true);
DialogResult qr= MessageBox.Show("删除命令已执行,是否继续?", "提示", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (qr == DialogResult.Yes)
{
//继续执行命令
}
else
{
if (qr == DialogResult.No)//删除文件夹后不继续
{
i = 1;
}
}
}
else
{
if (fg == DialogResult.No)//确认不删除
{
i = 1;
}
}
}
//强制用户确认后才进行解压操作
if (i == 0)
{
//获取打开文件的路径(全路径,含文件名)
int fnlength = path.ToString().LastIndexOf("FileName");
string sFile = path.ToString().Substring(fnlength + 9, path.ToString().Length - fnlength - 9);
//获取打开文件所在目录,传递至sourcepath
string sourcepath = Path.GetDirectoryName(sFile);
//开始解压文件
ZipFile.ExtractToDirectory(path.FileName, expath);
//跳转至xl文件夹下
string copath = expath + "\\xl\\";
//获取xl文件夹下所有带有comments字符的xml文件路径
string[] pathFile = Directory.GetFiles(copath, "comments*.xml", SearchOption.TopDirectoryOnly);
//将中间文件命名为cobak.xml
string strcon = copath + @"cobak.xml";
//暂时将替换容器内容清空
string con = "";
foreach (string str in pathFile)
{
StreamReader reader = new StreamReader(str, Encoding.UTF8);
//读取至文件尾
con = reader.ReadToEnd();
//替换shapeId="0"为空值
con = con.Replace(@"shapeId=""0""", "");
//替换Excel2013的不正确的XML代码块
con = con.Replace(@"</commment></commentList></comments>", "");
//开始以UTF-8编码开始写操作
StreamWriter writer = new StreamWriter(strcon, false, Encoding.UTF8);
//写入已替换的内容
writer.Write(con);
//释放写入缓冲区,暂时关闭写入器
writer.Flush();
writer.Close();
reader.Close();
//将写入文件复制为原文件,删除写入文件
File.Copy(strcon, str, true);
File.Delete(strcon);
}
//到此替换写入操作已完成
//开始修改文件扩展名
string nFile = Path.ChangeExtension(sFile, ".zip");
FileInfo fi = new FileInfo(sFile);
fi.MoveTo(nFile);
//开始Czip操作
CZip.ZipFile zip = new CZip.ZipFile(nFile);
zip.BeginUpdate();
//获取每个comments.xml文件的目录
foreach (string str in pathFile)
{
string filename = Path.GetFileName(str);
zip.Add(str,@"/xl/" + filename);
}
zip.CommitUpdate();
//关闭Czip
zip.Close();
//将文件扩展名改回xlsx
string endFile =Path.ChangeExtension(nFile, ".xlsx");
//删除临时目录
fi.CopyTo(endFile, true);
DirectoryInfo dl = new DirectoryInfo(expath);
dl.Delete(true);
//删除临时文件
File.Delete(nFile);
System.Diagnostics.Process.Start("explorer.exe", sourcepath);
}
}
项目在Github仓库的release下载地址:
https://github.com/InfinityEx/Excel-Comments-Fixer/releases/tag/Debug
勘误清单: