65.9K
CodeProject 正在变化。 阅读更多。
Home

使用原生 .NET 代码将任意 URL 转换为 MHTML 存档

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (57投票s)

2004 年 9 月 12 日

5分钟阅读

viewsIcon

740040

downloadIcon

5752

一个用于保存 URL 的原生 .NET 类:仅文本、HTML 页面、HTML 存档或完整的 HTML。

Sample Image - MhtBuilder.gif

引言

如果您曾经在 Internet Explorer 中使用过 文件 | 另存为... 菜单,您可能会注意到 IE 在 保存类型 下拉框中提供了一些有趣的选项。

Screenshot - Internet Explorer Save As menu

提供的选项有:

  • 网页,全部保存
  • 网页存档,单个文件
  • 网页,仅 HTML
  • 文本文件

其中大多数不言自明,除了 网页存档 (MHTML) 格式。这种格式的优点在于,它将网页及其所有引用打包到一个紧凑的 .MHT 文件中。分发一个自包含的文件比分发一个 HTML 文件以及一个包含该 HTML 文件引用的图像/CSS/Flash/XML 文件的子文件夹要容易得多。在我们的案例中,我们正在生成 HTML 报告,并且需要将这些报告检入一个期望单个文件的文档管理系统。MHTML (*.mht) 格式完美地解决了这个问题!

本项目包含 MhtBuilder 类,这是一个 100% .NET 受管代码解决方案,可以在一行代码中从目标 URL 自动生成 MHT 文件。作为奖励,它还可以生成上面列出的所有其他格式。而且它是完全免费的,不像您可能找到的一些 商业解决方案

背景

我知道人们总是对微软抱有最坏的假设,但 MHTML 格式实际上是基于 RFC 标准 2557 的,符合 MIME 标准的多部分消息 (MHTML Web 存档)。所以它是一个真正的互联网标准!网页存档,也称为 MHTML,是一种非常简单的纯文本格式,它看起来很像(实际上几乎完全相同)一封电子邮件。这是您在页面顶部看到的 MHT 文件的头部。

Screenshot - Mht file header

要生成 MHTML 文件,我们只需合并 HTML 中引用的所有文件。红线标记了第一个内容块;每个文件都会有一个内容块。不过,我们必须遵守一些规则:

  • 对文本格式使用 Quoted-Printable 编码。
  • 对二进制格式使用 Base64 编码。
  • 确保 Content-Location 具有每个引用的正确绝对 URL。

并非所有网站都能容忍被打包成 MHTML 文件。此版本的 Mht.Builder 支持 frames 和 IFrame,但要注意那些包含大量复杂 JavaScript 的页面。对于这类网站,您可能需要使用 .StripScripts 选项。

使用 Mht.Builder

MhtBuilder 带有一个完整的演示应用程序。

Screenshot - Mht demo application

尝试在您喜欢的网站上使用它。文件将默认生成在解决方案的 \bin 文件夹中。只需单击“查看”按钮即可启动它们。请记住,对于“网页存档”和“全部保存”选项卡,目标网页的所有内容都必须下载到 /bin 文件夹,所以可能需要一些时间!虽然我还没有提供任何反馈事件,但我通过 Debug.Write 发出大量进度反馈,所以切换到调试输出选项卡可以实时查看正在发生的情况。

这里有四个选项卡,就像 IE 在“另存为类型”选项中提供的四个选项一样。在 MhtBuilder 中,这些是按选项卡上出现的顺序调用的四个方法。

Public Sub SavePageComplete(ByVal outputFilePath As String, Optional url As String)
Public Sub SavePageArchive(ByVal outputFilePath As String, Optional url As String)
Public Sub SavePage(ByVal outputFilePath As String, Optional url As String)
Public Sub SavePageText(ByVal outputFilePath As String, Optional url As String)

从 Windows XP Service Pack 2 开始,从磁盘打开的 HTML 文件会导致安全阻止。为了避免这种情况,我们需要为文件添加 “Web 标记”,以便 IE 知道它来自哪个 URL,从而分配一个适当的安全区域给 HTML。blnAddMark 参数就是为此而设的;它会在文件顶部添加这一行。

<!-- saved from url=(0027)https://codeproject.org.cn/ -->

保存这些文件时,我们需要做的另一件事是修复 URL。任何相对 URL,例如:

<img src="/images/standard/logo225x72.gif">

必须转换为绝对 URL,如下所示:

<img src="https://codeproject.org.cn/images/standard/logo225x72.gif">

我们使用正则表达式来实现这一点,它会为我们需要修复的所有引用生成一个 NameValueCollection。我们遍历每个引用并对 HTML 字符串执行修复。

Private Function ExternalHtmlFiles() As Specialized.NameValueCollection
  If Not _ExternalFileCollection Is Nothing Then
    Return _ExternalFileCollection
  End If
  
  _ExternalFileCollection = New Specialized.NameValueCollection
  Dim r As Regex
  Dim html As String = Me.ToString
  
  Debug.WriteLine("Resolving all external HTML references from URL:")
  Debug.WriteLine("    " & Me.Url)
  
  '-- src='filename.ext' ; background="filename.ext"
  '-- note that we have to test 3 times to catch all quote styles: '', "", and none
  r = New Regex( _
    "(\ssrc|\sbackground)\s*=\s*((?<Key>'(?<Value>[^']+)')|" & _
    "(?<Key>""(?<Value>[^""]+)"")|(?<Key>(?<Value>[^ \n\r\f]+)))", _
    RegexOptions.IgnoreCase Or RegexOptions.Multiline)
    AddMatchesToCollection(html, r, _ExternalFileCollection)
  
  '-- @import "style.css" or @import url(style.css)
  r = New Regex( _
    "(@import\s|\S+-image:|background:)\s*?(url)*\s*?(?<Key>" & _
    "[""'(]{1,2}(?<Value>[^""')]+)[""')]{1,2})", _
    RegexOptions.IgnoreCase Or RegexOptions.Multiline)
    AddMatchesToCollection(html, r, _ExternalFileCollection)
  
  '-- <link rel=stylesheet href="style.css">
  r = New Regex( _
    "<link[^>]+?href\s*=\s*(?<Key>" & _
    "('|"")*(?<Value>[^'"">]+)('|"")*)", _
    RegexOptions.IgnoreCase Or RegexOptions.Multiline)
    AddMatchesToCollection(html, r, _ExternalFileCollection)
  
  '-- <iframe src="mypage.htm"> or <frame src="mypage.aspx">
  r = New Regex( _
    "<i*frame[^>]+?src\s*=\s*(?<Key>" & _
    "['""]{0,1}(?<Value>[^'""\\>]+)['""]{0,1})", _
    RegexOptions.IgnoreCase Or RegexOptions.Multiline)
    AddMatchesToCollection(html, r, _ExternalFileCollection)
  
  Return _ExternalFileCollection
End Function

我们使用类似的技术来获取需要下载的所有文件的列表,然后通过我的 WebClientEx 类下载。为什么使用它而不是内置的 Net.WebClient?好问题!因为它不支持 HTTP 压缩。而我的类则支持。

Private Function Decompress(ByVal b() As Byte, _
      ByVal CompressionType As HttpContentEncoding) As Byte()

  Dim s As Stream
  Select Case CompressionType
    Case HttpContentEncoding.Deflate
      s = New Zip.Compression.Streams.InflaterInputStream(New MemoryStream(b), _
          New Zip.Compression.Inflater(True))
    Case HttpContentEncoding.Gzip
      s = New GZip.GZipInputStream(New MemoryStream(b))
    Case Else
      Return b
  End Select
  
  Dim ms As New MemoryStream
  Const chunkSize As Integer = 2048
  
  Dim sizeRead As Integer
  Dim unzipBytes(chunkSize) As Byte
  While True
    sizeRead = s.Read(unzipBytes, 0, chunkSize)
    If sizeRead > 0 Then
      ms.Write(unzipBytes, 0, sizeRead)
    Else
      Exit While
    End If
  End While
  s.Close()
  
  Return ms.ToArray
End Function

HTTP 压缩是显而易见的:它通过使用标准的 GZIP 压缩将您的有效带宽提高了 75%——这要归功于 SharpZipLib 库。

结论

创建 MHTML 文件并不难,但在处理 HTML、正则表达式和 HTTP 下载时有很多细微的差别。我已尝试在源代码中记录所有困难的部分。到目前为止,我还在数十个不同的网站上测试了 MhtBuilder,并取得了优异的结果。

文章顶部的源代码中有更多细节和注释,请查看。请不要犹豫提供反馈,无论是好是坏!希望您喜欢这篇文章。如果您喜欢,您可能还会喜欢 我的其他文章

历史

  • 2004 年 9 月 12 日,星期日 - 发布。
  • 2005 年 3 月 28 日,星期一 - 版本 2.0
    • 完全重写!
    • 内容编码自动检测(例如,国际网页),已针对多语言网站进行测试。
    • 现在能正确解压两种类型的 HTTP 压缩。
    • 支持完全内存操作(用于服务器端使用)或磁盘存储(用于客户端使用)。
    • 现在支持处理具有 frames 和 IFrames 的网页,使用递归检索。
    • HTTP 身份验证和 HTTP 代理支持。
    • 允许配置浏览器 ID 字符串以检索特定于浏览器的内容。
    • 基本 cookie 支持(需要增强和测试)。
    • 用于解析 HTTP 的正则表达式已得到显著改进。
    • 广泛使用 VB.NET 2005 风格的 XML 注释。
© . All rights reserved.