我们的一位员工丢失了他的邮箱,但幸运的是他的电子邮件以 mbox 格式转储。我需要以某种方式获取 mbox 文件中的所有消息并将它们喷射到我们的技术支持数据库中(因为它是一个自定义工具,所以没有可用的导入工具)。
我发现SharpMimeTools可以分解消息,但不允许您遍历 mbox 文件中的一堆消息。
有谁知道无需学习 RFC 就可以打开的体面解析器?
我们的一位员工丢失了他的邮箱,但幸运的是他的电子邮件以 mbox 格式转储。我需要以某种方式获取 mbox 文件中的所有消息并将它们喷射到我们的技术支持数据库中(因为它是一个自定义工具,所以没有可用的导入工具)。
我发现SharpMimeTools可以分解消息,但不允许您遍历 mbox 文件中的一堆消息。
有谁知道无需学习 RFC 就可以打开的体面解析器?
我正在使用 C# 开发一个名为MimeKit的 MIME 和 mbox 解析器。
它基于我编写的早期 MIME 和 mbox 解析器(例如GMime),速度非常快(可以在大约 1 秒内解析 1.2GB mbox 文件中的每条消息)。
我还没有测试 MimeKit 的性能,但是我在 C# 中使用了许多与我在 C 中使用的相同的技术。我怀疑它会比我的 C 实现慢,但是因为瓶颈是 I/O 而 MimeKit 是像 GMime 那样写来做最佳(4k)读取,它们应该非常接近。
您发现当前方法很慢(StreamReader.ReadLine(),组合文本,然后将其传递给 SharpMimeTools)的原因是由于以下原因:
StreamReader.ReadLine() 不是从文件中读取数据的最佳方式。虽然我确定 StreamReader() 会进行内部缓冲,但它需要执行以下步骤:
A) 将从文件中读取的字节块转换为 unicode(这需要遍历从磁盘读取的 byte[] 中的字节,以将从流中读取的字节转换为 unicode char[])。
B) 然后它需要遍历其内部的 char[],将每个 char 复制到 StringBuilder 中,直到找到一个 '\n'。
所以就在那里,只需读取行,您就可以通过 mbox 输入流至少 2 次。更不用说正在进行的所有内存分配......
然后你把你读过的所有行合并成一个大字符串。这需要对您的输入进行另一次传递(大概将从 ReadLine() 读取的每个字符串中的每个字符复制到 StringBuilder 中?)。
我们现在对输入文本进行了多达 3 次迭代,甚至还没有进行任何解析。
现在你将你的巨型字符串交给 SharpMimeTools,它使用一个 SharpMimeMessageStream... (/facepalm) 是一个基于 ReadLine() 的解析器,它位于另一个进行字符集转换的 StreamReader 之上。在解析任何内容之前进行 5 次迭代。如果 SharpMimeMessageStream 发现它读得太远,它还有一种方法可以“撤消” ReadLine()。因此可以合理地假设他至少扫描了其中一些行两次。更不用说正在进行的所有字符串分配......呃。
对于每个标题,一旦 SharpMimeTools 拥有其行缓冲区,它就会拆分为字段和值。那是另一个关卡。到目前为止,我们最多可以通过 6 次。
然后 SharpMimeTools 使用 string.Split()(这很好地表明这个 mime 解析器不符合标准)通过拆分来标记地址头,并通过拆分参数化头(例如 Content-Type 和 Content-Disposition)在 ';'。那是另一个关卡。(我们现在最多 7 次传球。)
一旦它拆分它们,它就会对从 string.Split() 返回的每个字符串运行正则表达式匹配,然后每个 rfc2047 编码词令牌传递更多的正则表达式,然后最终对编码词字符集和有效负载组件进行另一次传递。到目前为止,我们正在谈论至少 9 或 10 次通过大部分输入。
我放弃了进一步的考试,因为它已经是 GMime 和 MimeKit 所需的 2 倍多,而且我知道我的解析器可以优化为至少比它们少 1 次通过。
此外,作为旁注,任何解析字符串而不是 byte[](或 sbyte[])的 MIME 解析器都不会很好。电子邮件的问题在于,如此多的邮件客户端/脚本/等会在标题和消息正文中发送未声明的 8 位文本。unicode 字符串解析器如何处理这个问题?提示:不能。
using (var stream = File.OpenRead ("Inbox.mbox")) {
var parser = new MimeParser (stream, MimeFormat.Mbox);
while (!parser.IsEndOfStream) {
var message = parser.ParseMessage ();
// At this point, you can do whatever you want with the message.
// As an example, you could save it to a separate file based on
// the message subject:
message.WriteTo (message.Subject + ".eml");
// You also have the ability to get access to the mbox marker:
var marker = parser.MboxMarker;
// You can also get the exact byte offset in the stream where the
// mbox marker was found:
var offset = parser.MboxMarkerOffset;
}
}
2013-09-18 更新:我已经让 MimeKit 可以用于解析 mbox 文件并成功地解决了问题,但它的速度不如我的 C 库快。这是在 iMac 上测试的,因此 I/O 性能不如我的旧 Linux 机器(这是 GMime 能够在 ~1s 内解析类似大小的 mbox 文件的地方):
[fejj@localhost MimeKit]$ mono ./mbox-parser.exe larger.mbox
Parsed 14896 messages in 6.16 seconds.
[fejj@localhost MimeKit]$ ./gmime-mbox-parser larger.mbox
Parsed 14896 messages in 3.78 seconds.
[fejj@localhost MimeKit]$ ls -l larger.mbox
-rw-r--r-- 1 fejj staff 1032555628 Sep 18 12:43 larger.mbox
正如你所看到的,GMime 仍然相当快,但我对如何提高 MimeKit 解析器的性能有一些想法。事实证明,C# 的fixed
语句非常昂贵,所以我需要重新使用它们。例如,我昨天做的一个简单优化将总时间缩短了大约 2-3 秒(如果我没记错的话)。
优化更新:通过替换将性能提高了 20%:
while (*inptr != (byte) '\n')
inptr++;
和:
do {
mask = *dword++ ^ 0x0A0A0A0A;
mask = ((mask - 0x01010101) & (~mask & 0x80808080));
} while (mask == 0);
inptr = (byte*) (dword - 1);
while (*inptr != (byte) '\n')
inptr++;
优化更新:通过放弃使用 Enum.HasFlag() 并改用直接位掩码,我终于能够使 MimeKit 与 GMime 一样快。
MimeKit 现在可以在 3.78 秒内解析相同的 mbox 流。
作为比较,SharpMimeTools 需要 20 多分钟(为了测试这一点,我不得不将电子邮件分成单独的文件,因为 SharpMimeTools 无法解析 mbox 文件)。
另一个更新:通过整个代码中的各种其他调整,我已经把它降到了 3.00 秒。
我不知道任何解析器,但 mbox 确实是一种非常简单的格式。一封新电子邮件以“发件人”(发件人+空格)开头的行开始,每封邮件的末尾都附有一个空行。如果在电子邮件本身的行首出现任何“From”,则将其引用(通过在前面添加“>”)。
另请参阅Wikipedia 关于该主题的条目。
如果您可以使用 Python,标准库中有一个。遗憾的是,我无法为 .NET 找到任何内容。
要阅读 .mbox 文件,您可以使用第三方库Aspose.Email。该库是一套完整的电子邮件处理 API,用于构建跨平台应用程序,能够在不使用 Microsoft Outlook 的情况下创建、操作、转换和传输电子邮件。
请看一下我在下面提供的示例。
using(FileStream stream = new FileStream("ExampleMbox.mbox", FileMode.Open, FileAccess.Read))
{
using(MboxrdStorageReader reader = new MboxrdStorageReader(stream, false))
{
// Start reading messages
MailMessage message = reader.ReadNextMessage();
// Read all messages in a loop
while (message != null)
{
// Manipulate message - show contents
Console.WriteLine("Subject: " + message.Subject);
// Save this message in EML or MSG format
message.Save(message.Subject + ".eml", SaveOptions.DefaultEml);
message.Save(message.Subject + ".msg", SaveOptions.DefaultMsgUnicode);
// Get the next message
message = reader.ReadNextMessage();
}
}
}
它很容易使用。我希望这种方法能让您和其他搜索者满意。
我在 Aspose 担任开发人员布道师。