OpenXml Excel: 在邮件地址后的任何单词中抛出错误
OpenXml Excel: throw error in any word after mail address
我使用 OpenXml 读取了 Excel 个文件。一切正常,但如果电子表格包含一个单元格,其中有一个地址邮件,然后是 space 和另一个词,例如:
abc@abc.com abc
它在打开电子表格时立即抛出异常:
var _doc = SpreadsheetDocument.Open(_filePath, false);
异常:
DocumentFormat.OpenXml.Packaging.OpenXmlPackageException
Additional information:
Invalid Hyperlink: Malformed URI is embedded as a
hyperlink in the document.
我没有使用过 OpenXml,但如果没有使用它的具体原因,那么我强烈推荐 LinqToExcel 的 LinqToExcel。代码示例在这里:
var sheet = new ExcelQueryFactory("filePath");
var allRows = from r in sheet.Worksheet() select r;
foreach (var r in allRows) {
var cella = r["Header"].ToString();
}
OpenXml 论坛上有一个与此问题相关的未解决问题:Malformed Hyperlink causes exception
在 post 中,他们讨论了在 Word 文档中遇到格式错误的 "mailto:" 超链接的问题。
他们在这里提出了一个解决方法:Workaround for malformed hyperlink exception
解决方法本质上是一个小型控制台应用程序,它可以找到无效的 URL 并将其替换为硬编码值;这是他们进行替换的样本中的代码片段;您可以扩充此代码以尝试更正传递的 brokenUri:
private static Uri FixUri(string brokenUri)
{
return new Uri("http://broken-link/");
}
我遇到的问题实际上是 Excel 文档(就像你一样),它与格式错误的 http URL 有关;我惊喜地发现他们的代码与我的 Excel 文件一起工作得很好。
这是整个变通源代码,以防万一这些链接之一在将来消失:
void Main(string[] args)
{
var fileName = @"C:\temp\corrupt.xlsx";
var newFileName = @"c:\temp\Fixed.xlsx";
var newFileInfo = new FileInfo(newFileName);
if (newFileInfo.Exists)
newFileInfo.Delete();
File.Copy(fileName, newFileName);
WordprocessingDocument wDoc;
try
{
using (wDoc = WordprocessingDocument.Open(newFileName, true))
{
ProcessDocument(wDoc);
}
}
catch (OpenXmlPackageException e)
{
e.Dump();
if (e.ToString().Contains("The specified package is not valid."))
{
using (FileStream fs = new FileStream(newFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
UriFixer.FixInvalidUri(fs, brokenUri => FixUri(brokenUri));
}
}
}
}
private static Uri FixUri(string brokenUri)
{
brokenUri.Dump();
return new Uri("http://broken-link/");
}
private static void ProcessDocument(WordprocessingDocument wDoc)
{
var elementCount = wDoc.MainDocumentPart.Document.Descendants().Count();
Console.WriteLine(elementCount);
}
}
public static class UriFixer
{
public static void FixInvalidUri(Stream fs, Func<string, Uri> invalidUriHandler)
{
XNamespace relNs = "http://schemas.openxmlformats.org/package/2006/relationships";
using (ZipArchive za = new ZipArchive(fs, ZipArchiveMode.Update))
{
foreach (var entry in za.Entries.ToList())
{
if (!entry.Name.EndsWith(".rels"))
continue;
bool replaceEntry = false;
XDocument entryXDoc = null;
using (var entryStream = entry.Open())
{
try
{
entryXDoc = XDocument.Load(entryStream);
if (entryXDoc.Root != null && entryXDoc.Root.Name.Namespace == relNs)
{
var urisToCheck = entryXDoc
.Descendants(relNs + "Relationship")
.Where(r => r.Attribute("TargetMode") != null && (string)r.Attribute("TargetMode") == "External");
foreach (var rel in urisToCheck)
{
var target = (string)rel.Attribute("Target");
if (target != null)
{
try
{
Uri uri = new Uri(target);
}
catch (UriFormatException)
{
Uri newUri = invalidUriHandler(target);
rel.Attribute("Target").Value = newUri.ToString();
replaceEntry = true;
}
}
}
}
}
catch (XmlException)
{
continue;
}
}
if (replaceEntry)
{
var fullName = entry.FullName;
entry.Delete();
var newEntry = za.CreateEntry(fullName);
using (StreamWriter writer = new StreamWriter(newEntry.Open()))
using (XmlWriter xmlWriter = XmlWriter.Create(writer))
{
entryXDoc.WriteTo(xmlWriter);
}
}
}
}
}
不幸的是,您必须以 zip 格式打开文件并替换损坏的超链接的解决方案对我没有帮助。
我只是想知道当您的目标框架是 4.0 时,即使您只安装了 .Net Framework 的版本是 4.7.2,它怎么可能正常工作。
我发现 System.UriParser
中有一个私有静态字段,用于选择 URI 的 RFC 规范版本。因此可以将它设置为 V2,因为它是为 .net 4.0 和更低版本的 .Net Framework 设置的。唯一的问题是 private static readonly
.
也许有人会想要为整个应用程序全局设置它。但是我写了 UriQuirksVersionPatcher
来更新这个版本并在 Dispose 方法中恢复它。它显然不是线程安全的,但对我来说是可以接受的。
using System;
using System.Diagnostics;
using System.Reflection;
namespace BarCap.RiskServices.RateSubmissions.Utility
{
#if (NET20 || NET35 || NET40)
public class UriQuirksVersionPatcher : IDisposable
{
public void Dispose()
{
}
}
#else
public class UriQuirksVersionPatcher : IDisposable
{
private const string _quirksVersionFieldName = "s_QuirksVersion"; //See Source\ndp\fx\src\net\System\_UriSyntax.cs in NexFX sources
private const string _uriQuirksVersionEnumName = "UriQuirksVersion";
/// <code>
/// private enum UriQuirksVersion
/// {
/// V1 = 1, // RFC 1738 - Not supported
/// V2 = 2, // RFC 2396
/// V3 = 3, // RFC 3986, 3987
/// }
/// </code>
private const string _oldQuirksVersion = "V2";
private static readonly Lazy<FieldInfo> _targetFieldInfo;
private static readonly Lazy<int?> _patchValue;
private readonly int _oldValue;
private readonly bool _isEnabled;
static UriQuirksVersionPatcher()
{
var targetType = typeof(UriParser);
_targetFieldInfo = new Lazy<FieldInfo>(() => targetType.GetField(_quirksVersionFieldName, BindingFlags.Static | BindingFlags.NonPublic));
_patchValue = new Lazy<int?>(() => GetUriQuirksVersion(targetType));
}
public UriQuirksVersionPatcher()
{
int? patchValue = _patchValue.Value;
_isEnabled = patchValue.HasValue;
if (!_isEnabled) //Disabled if it failed to get enum value
{
return;
}
int originalValue = QuirksVersion;
_isEnabled = originalValue != patchValue;
if (!_isEnabled) //Disabled if value is proper
{
return;
}
_oldValue = originalValue;
QuirksVersion = patchValue.Value;
}
private int QuirksVersion
{
get
{
return (int)_targetFieldInfo.Value.GetValue(null);
}
set
{
_targetFieldInfo.Value.SetValue(null, value);
}
}
private static int? GetUriQuirksVersion(Type targetType)
{
int? result = null;
try
{
result = (int)targetType.GetNestedType(_uriQuirksVersionEnumName, BindingFlags.Static | BindingFlags.NonPublic)
.GetField(_oldQuirksVersion, BindingFlags.Static | BindingFlags.Public)
.GetValue(null);
}
catch
{
#if DEBUG
Debug.WriteLine("ERROR: Failed to find UriQuirksVersion.V2 enum member.");
throw;
#endif
}
return result;
}
public void Dispose()
{
if (_isEnabled)
{
QuirksVersion = _oldValue;
}
}
}
#endif
}
用法:
using(new UriQuirksVersionPatcher())
{
using(var document = SpreadsheetDocument.Open(fullPath, false))
{
//.....
}
}
P.S。后来发现有人已经实现了这个pathcher:https://github.com/google/google-api-dotnet-client/blob/master/Src/Support/Google.Apis.Core/Util/UriPatcher.cs
@RMD 的修复效果很好。我已经使用它很多年了。但是有一个新的修复。
中看到修复
将 OpenXML 升级到 2.12.0。
右键单击解决方案并 select 管理 NuGet 包。
实施修复
- 进行单元测试很有帮助。使用错误的电子邮件地址创建一个 excel 文件,例如 test@gmail,com。 (注意逗号而不是点)。
- 确保您打开的流和对 SpreadsheetDocument.Open 的调用允许读写。
- 您需要实现一个RelationshipErrorHandlerFactory 并在您打开时在选项中使用它。这是我使用的代码:
public class UriRelationshipErrorHandler : RelationshipErrorHandler
{
public override string Rewrite(Uri partUri, string id, string uri)
{
return "https://broken-link";
}
}
- 那么打开文档的时候就需要这样使用了:
var openSettings = new OpenSettings
{
RelationshipErrorHandlerFactory = package =>
{
return new UriRelationshipErrorHandler();
}
};
using var document = SpreadsheetDocument.Open(stream, true, openSettings);
此解决方案的优点之一是它不需要您创建文件的临时“固定”版本,而且代码少得多。
我使用 OpenXml 读取了 Excel 个文件。一切正常,但如果电子表格包含一个单元格,其中有一个地址邮件,然后是 space 和另一个词,例如:
abc@abc.com abc
它在打开电子表格时立即抛出异常:
var _doc = SpreadsheetDocument.Open(_filePath, false);
异常:
DocumentFormat.OpenXml.Packaging.OpenXmlPackageException
Additional information:
Invalid Hyperlink: Malformed URI is embedded as a hyperlink in the document.
我没有使用过 OpenXml,但如果没有使用它的具体原因,那么我强烈推荐 LinqToExcel 的 LinqToExcel。代码示例在这里:
var sheet = new ExcelQueryFactory("filePath");
var allRows = from r in sheet.Worksheet() select r;
foreach (var r in allRows) {
var cella = r["Header"].ToString();
}
OpenXml 论坛上有一个与此问题相关的未解决问题:Malformed Hyperlink causes exception
在 post 中,他们讨论了在 Word 文档中遇到格式错误的 "mailto:" 超链接的问题。
他们在这里提出了一个解决方法:Workaround for malformed hyperlink exception
解决方法本质上是一个小型控制台应用程序,它可以找到无效的 URL 并将其替换为硬编码值;这是他们进行替换的样本中的代码片段;您可以扩充此代码以尝试更正传递的 brokenUri:
private static Uri FixUri(string brokenUri)
{
return new Uri("http://broken-link/");
}
我遇到的问题实际上是 Excel 文档(就像你一样),它与格式错误的 http URL 有关;我惊喜地发现他们的代码与我的 Excel 文件一起工作得很好。
这是整个变通源代码,以防万一这些链接之一在将来消失:
void Main(string[] args)
{
var fileName = @"C:\temp\corrupt.xlsx";
var newFileName = @"c:\temp\Fixed.xlsx";
var newFileInfo = new FileInfo(newFileName);
if (newFileInfo.Exists)
newFileInfo.Delete();
File.Copy(fileName, newFileName);
WordprocessingDocument wDoc;
try
{
using (wDoc = WordprocessingDocument.Open(newFileName, true))
{
ProcessDocument(wDoc);
}
}
catch (OpenXmlPackageException e)
{
e.Dump();
if (e.ToString().Contains("The specified package is not valid."))
{
using (FileStream fs = new FileStream(newFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
UriFixer.FixInvalidUri(fs, brokenUri => FixUri(brokenUri));
}
}
}
}
private static Uri FixUri(string brokenUri)
{
brokenUri.Dump();
return new Uri("http://broken-link/");
}
private static void ProcessDocument(WordprocessingDocument wDoc)
{
var elementCount = wDoc.MainDocumentPart.Document.Descendants().Count();
Console.WriteLine(elementCount);
}
}
public static class UriFixer
{
public static void FixInvalidUri(Stream fs, Func<string, Uri> invalidUriHandler)
{
XNamespace relNs = "http://schemas.openxmlformats.org/package/2006/relationships";
using (ZipArchive za = new ZipArchive(fs, ZipArchiveMode.Update))
{
foreach (var entry in za.Entries.ToList())
{
if (!entry.Name.EndsWith(".rels"))
continue;
bool replaceEntry = false;
XDocument entryXDoc = null;
using (var entryStream = entry.Open())
{
try
{
entryXDoc = XDocument.Load(entryStream);
if (entryXDoc.Root != null && entryXDoc.Root.Name.Namespace == relNs)
{
var urisToCheck = entryXDoc
.Descendants(relNs + "Relationship")
.Where(r => r.Attribute("TargetMode") != null && (string)r.Attribute("TargetMode") == "External");
foreach (var rel in urisToCheck)
{
var target = (string)rel.Attribute("Target");
if (target != null)
{
try
{
Uri uri = new Uri(target);
}
catch (UriFormatException)
{
Uri newUri = invalidUriHandler(target);
rel.Attribute("Target").Value = newUri.ToString();
replaceEntry = true;
}
}
}
}
}
catch (XmlException)
{
continue;
}
}
if (replaceEntry)
{
var fullName = entry.FullName;
entry.Delete();
var newEntry = za.CreateEntry(fullName);
using (StreamWriter writer = new StreamWriter(newEntry.Open()))
using (XmlWriter xmlWriter = XmlWriter.Create(writer))
{
entryXDoc.WriteTo(xmlWriter);
}
}
}
}
}
不幸的是,您必须以 zip 格式打开文件并替换损坏的超链接的解决方案对我没有帮助。
我只是想知道当您的目标框架是 4.0 时,即使您只安装了 .Net Framework 的版本是 4.7.2,它怎么可能正常工作。
我发现 System.UriParser
中有一个私有静态字段,用于选择 URI 的 RFC 规范版本。因此可以将它设置为 V2,因为它是为 .net 4.0 和更低版本的 .Net Framework 设置的。唯一的问题是 private static readonly
.
也许有人会想要为整个应用程序全局设置它。但是我写了 UriQuirksVersionPatcher
来更新这个版本并在 Dispose 方法中恢复它。它显然不是线程安全的,但对我来说是可以接受的。
using System;
using System.Diagnostics;
using System.Reflection;
namespace BarCap.RiskServices.RateSubmissions.Utility
{
#if (NET20 || NET35 || NET40)
public class UriQuirksVersionPatcher : IDisposable
{
public void Dispose()
{
}
}
#else
public class UriQuirksVersionPatcher : IDisposable
{
private const string _quirksVersionFieldName = "s_QuirksVersion"; //See Source\ndp\fx\src\net\System\_UriSyntax.cs in NexFX sources
private const string _uriQuirksVersionEnumName = "UriQuirksVersion";
/// <code>
/// private enum UriQuirksVersion
/// {
/// V1 = 1, // RFC 1738 - Not supported
/// V2 = 2, // RFC 2396
/// V3 = 3, // RFC 3986, 3987
/// }
/// </code>
private const string _oldQuirksVersion = "V2";
private static readonly Lazy<FieldInfo> _targetFieldInfo;
private static readonly Lazy<int?> _patchValue;
private readonly int _oldValue;
private readonly bool _isEnabled;
static UriQuirksVersionPatcher()
{
var targetType = typeof(UriParser);
_targetFieldInfo = new Lazy<FieldInfo>(() => targetType.GetField(_quirksVersionFieldName, BindingFlags.Static | BindingFlags.NonPublic));
_patchValue = new Lazy<int?>(() => GetUriQuirksVersion(targetType));
}
public UriQuirksVersionPatcher()
{
int? patchValue = _patchValue.Value;
_isEnabled = patchValue.HasValue;
if (!_isEnabled) //Disabled if it failed to get enum value
{
return;
}
int originalValue = QuirksVersion;
_isEnabled = originalValue != patchValue;
if (!_isEnabled) //Disabled if value is proper
{
return;
}
_oldValue = originalValue;
QuirksVersion = patchValue.Value;
}
private int QuirksVersion
{
get
{
return (int)_targetFieldInfo.Value.GetValue(null);
}
set
{
_targetFieldInfo.Value.SetValue(null, value);
}
}
private static int? GetUriQuirksVersion(Type targetType)
{
int? result = null;
try
{
result = (int)targetType.GetNestedType(_uriQuirksVersionEnumName, BindingFlags.Static | BindingFlags.NonPublic)
.GetField(_oldQuirksVersion, BindingFlags.Static | BindingFlags.Public)
.GetValue(null);
}
catch
{
#if DEBUG
Debug.WriteLine("ERROR: Failed to find UriQuirksVersion.V2 enum member.");
throw;
#endif
}
return result;
}
public void Dispose()
{
if (_isEnabled)
{
QuirksVersion = _oldValue;
}
}
}
#endif
}
用法:
using(new UriQuirksVersionPatcher())
{
using(var document = SpreadsheetDocument.Open(fullPath, false))
{
//.....
}
}
P.S。后来发现有人已经实现了这个pathcher:https://github.com/google/google-api-dotnet-client/blob/master/Src/Support/Google.Apis.Core/Util/UriPatcher.cs
@RMD 的修复效果很好。我已经使用它很多年了。但是有一个新的修复。
中看到修复将 OpenXML 升级到 2.12.0。
右键单击解决方案并 select 管理 NuGet 包。
实施修复
- 进行单元测试很有帮助。使用错误的电子邮件地址创建一个 excel 文件,例如 test@gmail,com。 (注意逗号而不是点)。
- 确保您打开的流和对 SpreadsheetDocument.Open 的调用允许读写。
- 您需要实现一个RelationshipErrorHandlerFactory 并在您打开时在选项中使用它。这是我使用的代码:
public class UriRelationshipErrorHandler : RelationshipErrorHandler
{
public override string Rewrite(Uri partUri, string id, string uri)
{
return "https://broken-link";
}
}
- 那么打开文档的时候就需要这样使用了:
var openSettings = new OpenSettings
{
RelationshipErrorHandlerFactory = package =>
{
return new UriRelationshipErrorHandler();
}
};
using var document = SpreadsheetDocument.Open(stream, true, openSettings);
此解决方案的优点之一是它不需要您创建文件的临时“固定”版本,而且代码少得多。