比较具有相同内容的XLSX文件之间的MD5哈希值

我们有一个内部Web应用程序,它接受来自用户的不同格式的文件,以便将大量数据导入到我们的系统中。

我们实施的最新升级之一是添加一种方法来检测文件是否先前已上传,如果是,则向用户显示警告和选项以重新提交文件或取消上载。

为了达到这个目的,我们计算上传文件的MD5 ,并将其与包含以前上传的文件信息的数据库表进行比较,以确定它是否是重复的。 如果MD5匹配,则显示警告,否则将新的文件信息插入表中进行文件处理。

以下是用于生成MD5哈希的C#代码:

 private static string GetHash(byte[] input) { using (MD5 md5 = MD5.Create()) { byte[] data = md5.ComputeHash(input); StringBuilder bob = new StringBuilder(); for (int i = 0; i < data.Length; i++) bob.Append(data[i].ToString("x2").ToUpper()); return bob.ToString(); } } 

一切工作正常,还有一个例外。

用户可以为这个过程上传.xlsx文件,不幸的是,这种文件types也会在文件内容中存储文件的元数据。 (通过将.xlsx文件的扩展名更改为.zip并提取内容,可以很容易地看到这一点(见下文)。)

Excel元数据

因此, .xlsx文件的MD5哈希值随每个后续保存而改变,即使文件内容相同(只需打开并保存文件,不进行任何修改将刷新元数据并导致不同的MD5哈希) 。

在这种情况下,具有相同logging但在不同时间或由不同用户创build的文件将滑过重复文件检测并被处理。

我的问题:有没有一种方法来确定.xlsx文件的内容是否与前一个文件的内容相匹配而不存储文件内容? 换句话说:有没有办法生成一个.xlsx文件内容MD5散列?

在计算散列之前,您可以从文档中删除不应影响散列的文件。

这可以通过将Open XML包的所有部分提取到单个XML文档中,删除不需要的节点并计算生成的XML文档的散列来实现。 请注意,您将不得不重新计算已经上传的Excel文件的哈希值,因为哈希现在不再基于二进制文件内容。

这里是一个简单的示例程序(添加对WindowsBase.dll的引用):

 using System; using System.IO; using System.IO.Packaging; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Xml.Linq; internal class Program { private static readonly XNamespace dcterms = "http://purl.org/dc/terms/"; private static void Main(string[] args) { var fileName = args[0]; // open the ZIP package var package = Package.Open(fileName); // convert the package to a single XML document var xdoc = OpcToFlatOpc(package); // remove the nodes we are not interested in // here you can add other nodes as well xdoc.Descendants(dcterms + "modified").Remove(); // get a stream of the XML and compute the hash using (var ms = new MemoryStream()) { xdoc.Save(ms); ms.Position = 0; string md5 = GetHash(ms); Console.WriteLine(md5); } } private static string GetHash(Stream stream) { using (var md5 = MD5.Create()) { var data = md5.ComputeHash(stream); var bob = new StringBuilder(); for (int i = 0; i < data.Length; i++) { bob.Append(data[i].ToString("X2")); } return bob.ToString(); } } private static XDocument OpcToFlatOpc(Package package) { XNamespace pkg = "http://schemas.microsoft.com/office/2006/xmlPackage"; var declaration = new XDeclaration("1.0", "UTF-8", "yes"); var doc = new XDocument( declaration, new XProcessingInstruction("mso-application", "progid=\"Word.Document\""), new XElement( pkg + "package", new XAttribute(XNamespace.Xmlns + "pkg", pkg.ToString()), package.GetParts().Select(GetContentsAsXml))); return doc; } private static XElement GetContentsAsXml(PackagePart part) { XNamespace pkg = "http://schemas.microsoft.com/office/2006/xmlPackage"; if (part.ContentType.EndsWith("xml")) { using (var partstream = part.GetStream()) { using (var streamReader = new StreamReader(partstream)) { string streamString = streamReader.ReadToEnd(); if (!string.IsNullOrEmpty(streamString)) { var newXElement = new XElement( pkg + "part", new XAttribute(pkg + "name", part.Uri), new XAttribute(pkg + "contentType", part.ContentType), new XElement(pkg + "xmlData", XElement.Parse(streamString))); return newXElement; } return null; } } } using (var str = part.GetStream()) { using (var binaryReader = new BinaryReader(str)) { int len = (int)binaryReader.BaseStream.Length; var byteArray = binaryReader.ReadBytes(len); // the following expression creates the base64String, then chunks // it to lines of 76 characters long string base64String = Convert.ToBase64String(byteArray) .Select((c, i) => new { Character = c, Chunk = i / 76 }) .GroupBy(c => c.Chunk) .Aggregate( new StringBuilder(), (s, i) => s.Append( i.Aggregate( new StringBuilder(), (seed, it) => seed.Append(it.Character), sb => sb.ToString())) .Append(Environment.NewLine), s => s.ToString()); return new XElement( pkg + "part", new XAttribute(pkg + "name", part.Uri), new XAttribute(pkg + "contentType", part.ContentType), new XAttribute(pkg + "compression", "store"), new XElement(pkg + "binaryData", base64String)); } } } }