使用Python读取一个大的.mbox文件

13

我想阅读来自Gmail备份的一个3GB大小的.mbox文件。下面的命令可以实现:

import mailbox
mbox = mailbox.mbox(r"D:\All mail Including Spam and Trash.mbox")
for i, message in enumerate(mbox):
    print("from   :",message['from'])
    print("subject:",message['subject'])
    if message.is_multipart():
        content = ''.join(part.get_payload(decode=True) for part in message.get_payload())
    else:
        content = message.get_payload(decode=True)
    print("content:",content)
    print("**************************************")

    if i == 10:
        break

除了前10条消息需要超过40秒外,还有更快的方法用Python访问大的.mbox文件吗?


1
我认为 mailbox 库将所有内容读入内存。将一个简单的 mbox 解析器重写为生成器应该不难(简而言之,任何以 From 开头的行都会开始一个新的消息)。 - tripleee
1
不,for message in mailbox.mbox()并不会将所有内容读入内存。它使用生成器高效地逐个迭代消息。但是,在第一次访问时,它确实会预先填充一个小的内部TOC结构,这可能需要一些时间。 - user124114
3个回答

13
这是一个快速、简单的生成器实现,可以逐个消息读取mbox文件。我选择放弃来自From 分隔符的信息;我猜想真正的mailbox库可能会提供更多信息,当然,这只支持读取,不能搜索或写回到输入文件。
#!/usr/bin/env python3

import email
from email.policy import default

class MboxReader:
    def __init__(self, filename):
        self.handle = open(filename, 'rb')
        assert self.handle.readline().startswith(b'From ')

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.handle.close()

    def __iter__(self):
        return iter(self.__next__())

    def __next__(self):
        lines = []
        while True:
            line = self.handle.readline()
            if line == b'' or line.startswith(b'From '):
                yield email.message_from_bytes(b''.join(lines), policy=default)
                if line == b'':
                    break
                lines = []
                continue
            lines.append(line)

使用方法:

with MboxReader(mboxfilename) as mbox:
    for message in mbox:
        print(message.as_string())

policy=default参数(当然,如果你愿意,也可以选择其他任何策略而不是default)选择了Python 3.3中引入并在3.6中成为官方的现代EmailMessage库。如果您需要支持早期的Python版本,从美国失去理智并将邪恶小丑放进白宫之前的更简单时代以前的时间,则应省略该参数;但实际上,新的API在许多方面都更好。


1
如果您只需要裸的消息源,而不需要email对象封装,则可以删除import,并只需使用yield(b''.join(lines)) - tripleee
3
我很惊讶这个功能没有被纳入Python标准库的模块中。大多数Python标准库都适用于迭代器。感谢你提供这个功能! - Jim Pivarski
@tripleee,你应该发起一份请求(PR),将这个代码包含在Python标准库/mailbox模块中!比原来的非常慢的mbox读取器好多了! - Basj
2
不,内置的 mailbox.mbox 并不会将整个存档加载到 RAM 中。但它确实会预加载和缓存一个(小型)TOC结构,该结构将每个消息位置(int)映射到其文件字节偏移量(两个int)。这个 TOC 在第一次访问时可能需要花费一些时间来创建。 - user124114
这个答案能够提取附加文件或它们的大小吗? - jokoon
显示剩余6条评论

1
使用这里提到的 MboxReader 类此链接,您可以使用任何密钥从 mbox 对象获取特定信息。然后可以创建文本文件以进一步分析您的邮箱。
path = "your_gmail.mbox"
mbox = MboxReader(path)
from tqdm import tqdm

with open('Output.txt','w',encoding="utf-8") as file:
    for idx,message in tqdm(enumerate(mbox)):
        # print(message.keys())
        mail_from = f"{str(message['From'])}\n".replace('"','')
        file.write(mail_from)
        print(idx,message['From'])

以下是可以使用的键,供参考

['X-GM-THRID', 'X-Gmail-Labels', 'Delivered-To', 'Received', 'X-Received',
 'ARC-Seal', 'ARC-Message-Signature', 'ARC-Authentication-Results', 
'Return-Path', 'Received', 'Received-SPF', 'Authentication-Results', 
'DKIM-Signature', 'X-Google-DKIM-Signature', 'X-Gm-Message-State', 
'X-Google-Smtp-Source', 'MIME-Version', 'X-Received', 'Date', 'Reply-To',
 'X-Google-Id', 'Precedence', 'List-Unsubscribe', 'Feedback-ID', 'List-Id',
 'X-Notifications', 'X-Notifications-Bounce-Info', 'Message-ID', 'Subject',
 'From', 'To', 'Content-Type']

希望这对你有用 :)


2
顺带一提,这里没有所谓的“上面”;本页面上的答案将根据每个访问者的偏好进行排序(默认情况下按得分排序,在这种情况下,另一个答案目前确实会排在此答案之上)。 - tripleee
谢谢你的回答,但我宁愿不使用TDQM,因为它不是必需的。你可以使用原生的 sys.stdout.writesys.stdout.write - jokoon
抱歉,我是指 sys.stdout.write()sys.stdout.flush() - jokoon
你可以移除tqdm,它只是为了检查剩余的邮件数量。Mbox文件可能会非常大。 - Vinay Verma
提取的标头名称随意散布并不特别信息丰富或有用;其中许多标头是非标准或可选的,而一些可选但标准的标头则缺失。 - tripleee

0
我在selected solution中发现了一个小bug - 根据mbox specs,每封新邮件都必须以"From "开头,如果正文中有"From ",则必须用">"进行转义。 规范中的引用如下:
引用: 程序然后复制消息,对每一行应用>From引用。>From引用确保生成的行不是From_行:程序在任何From_行、>From_行、>>From_行、>>>From_行等之前添加>。
提出的解决方案没有清除转义字符,并且电子邮件中可能会有额外的">"字符。
以下是修复后的解决方案:
import email
from email.policy import default


class MboxReader:
    def __init__(self, filename):
        self.handle = open(filename, 'rb')
        assert self.handle.readline().startswith(b'From ')

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.handle.close()

    def __iter__(self):
        return iter(self.__next__())

    def __next__(self):
        lines = []
        while True:
            line = self.handle.readline()
            if line == b'' or line.startswith(b'From '):
                yield email.message_from_bytes(b''.join(lines), policy=default)
                if line == b'':
                    break
                lines = []
                continue
            if line.startswith(b'>') and line.lstrip(b'>').startswith(b'From '):
                line = line[1:]
            lines.append(line)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接