如何用Python解析电子邮件

242 阅读6分钟

用Python解析电子邮件

也许你是一名机器学习工程师,正试图建立一个电子邮件垃圾邮件分类器。你可能想获得一些预处理邮件的方法,或者寻找随机邮件之间的相关性。你将以这样或那样的方式,首先对邮件进行解析。我们将看看如何用Python做这件事。Python的email 模块包含了一些方法来帮助我们实现这一目标。

前提条件

要跟上这篇文章,读者应该具备以下条件。

  • 对Python的理解。
  • 安装了Jupyter笔记本或能够访问Google Colab。

我们将在本文中使用Jupyter笔记本。你可以使用普通的Python文件来执行代码。

开始工作

我们将解析来自SpamAssassin网站的电子邮件,查看其结构和内容。该网站包含垃圾邮件和火腿邮件。我们将尝试使用突出显示的指标来观察它们的结构。

我们将在一个新的单元格中导入我们需要的包,然后添加从SpamAssassin网站上获取邮件的代码。

import os
import tarfile
import urllib.request
import email
import email.policy
from collections import Counter

#the root url
EMAILS_URL_ROOT = "http://spamassassin.apache.org/old/publiccorpus/"
#the emails url
HAM_URL = EMAILS_URL_ROOT + "20030228_easy_ham.tar.bz2"
SPAMS_URL = EMAILS_URL_ROOT + "20030228_spam.tar.bz2"
#datasets path
SPAM_PATH = os.path.join("datasets", "spam")

#method for fetching the emails from the url
def fetch_emails(HAM_URL=HAM_URL, SPAMS_URL=SPAMS_URL, spams_path=SPAM_PATH):
  #creating a directory
    if not os.path.isdir(spams_path):
        os.makedirs(spams_path)
        for filename, url in (("ham.tar.bz2", HAM_URL),("spam.tar.bz2", SPAMS_URL)):
            path = os.path.join(spams_path, filename)
            #checking if there is a file
            if not os.path.isfile(path):
                urllib.request.urlretrieve(url, path)
                #extracting
            email_tar_file = tarfile.open(path)
            email_tar_file.extractall(path=spams_path)
            email_tar_file.close()

我们将路径和URL设置为常数。然后,在fetch_emails() 方法中,我们使用os 模块的isdir() 方法检查目录是否存在。如果目录不存在,我们使用makedirs() 方法创建一个新的目录。

我们创建火腿邮件和垃圾邮件所需的两个路径来存储邮件。

完成后,如果文件目录不存在,我们就创建该目录,并使用urlretrieve() 方法进行检索。最后,我们打开tar文件并提取它们。

在下一个单元格中,我们调用fetch_emails() 方法。

fetch_emails()

让我们在一个新单元格中开始解析电子邮件。

#creating drectories for the extracted emails
HAM_DIR = os.path.join(SPAM_PATH, "easy_ham")
SPAMS_DIR = os.path.join(SPAM_PATH, "spam")

# sorted filenames for the emails
ham_filenames = [name for name in sorted(os.listdir(HAM_DIR))]
spam_filenames = [name for name in sorted(os.listdir(SPAMS_DIR))]
#load the emails
def load_emails(is_spam, filename, spams_path=SPAM_PATH):
  #if the argument is true for spam, load from the spam_emails directory and vice versa
    directory = "spam" if is_spam else "easy_ham"
    #open as readable and in binary
    with open(os.path.join(spams_path, directory, filename), "rb") as f:
      #parse using the defaul line break(\n)
        return email.parser.BytesParser(policy=email.policy.default).parse(f)
#load
ham_emails = [load_emails(is_spam=False, filename=name) for name in ham_filenames]
spam_emails = [load_emails(is_spam=True, filename=name) for name in spam_filenames]

我们为垃圾邮件和火腿邮件各创建一个目录,并对它们进行分类。

load_emails() ,我们将相应的目录打开为二进制格式的可读目录。之后,我们使用BytesParser 类对它们进行解析。BytesParser 类在构造函数中包含一个名为policy 的参数。它的策略与default 相同。使用default 让我们使用\n 的换行符来解析电子邮件。

然后我们调用load_emails() 方法来加载电子邮件。

在一个新的单元格中,我们可以看一下火腿邮件的样本。

print(ham_emails[42].get_content().strip())

查看电子邮件的结构

在这一部分,我们将研究如何获得电子邮件的结构,以及垃圾邮件和火腿邮件的常见结构类型。

将此代码粘贴在下一个单元格中。

'''Getting the most common email structures'''
def get_structures(email):
  #if its plain text return text/plain
    if isinstance(email, str):
        return email
    email_payload = email.get_payload()
    #if the payload is a list then its probably a multipart
    #return a multipart thereafter
    if isinstance(email_payload, list):
        return "multipart({})".format(", ".join([
            get_structures(sub_email)
            for sub_email in email_payload
        ]))
    else:
        return email.get_content_type()
#function for counting the types
def type_counter(emails):
    our_count = Counter()
    for email in emails:
        structure = get_structures(email)
        our_count[structure] += 1
    return our_count

在这段代码中,我们有两个方法,get_structures()type_counter()

get_structures() 函数中,我们检查结构。如果它是一封火腿文本邮件,我们返回text/plain, ,但如果它是一个多部分类型的邮件,我们返回multipart 和它包含的所有部分。如果该多部分邮件的子邮件中有许多邮件结构,则使用递归。

多部分电子邮件是指包含多个部分的电子邮件。例如,它可以同时包含一个文本/普通话部分和一个HTML部分。为了检查电子邮件的其他部分,我们必须检查相同的部分,直到对电子邮件进行反复解析。一个很好的方法是通过使用递归来实现。

除了text/plainmultipart这两种邮件结构之外,任何其他的邮件结构都可以通过返回邮件的内容类型(get_content_type())来显示。

type_counter() 方法检查垃圾邮件和火腿邮件中存在多少种邮件结构类型,例如:'text/plain', 2409 。因此,我们首先启动一个计数器,然后对每个类似的结构进行计数。

这段代码显示的是火腿邮件的一个。

print(structures_counter(ham_emails))

我们使用most_common() 方法检查最常见的结构。

print(structures_counter(spam_emails).most_common())

我们应该看到如下所示的输出。

[('text/plain', 2409), ('multipart(text/plain, application/pgp-signature)', 66), ('multipart(text/plain, text/html)', 8), ('multipart(text/plain, text/plain)', 4), ('multipart(text/plain)', 3), ('multipart(text/plain, application/octet-stream)', 2), ('multipart(text/plain, text/enriched)', 1), ('multipart(text/plain, application/ms-tnef, text/plain)', 1), ('multipart(multipart(text/plain, text/plain, text/plain), application/pgp-signature)', 1), ('multipart(text/plain, video/mng)', 1), ('multipart(text/plain, multipart(text/plain))', 1), ('multipart(text/plain, application/x-pkcs7-signature)', 1), ('multipart(text/plain, multipart(text/plain, text/plain), text/rfc822-headers)', 1), ('multipart(text/plain, multipart(text/plain, text/plain), multipart(multipart(text/plain, application/x-pkcs7-signature)))', 1), ('multipart(text/plain, application/x-java-applet)', 1)]

输出显示了类型,然后是它们各自的计数,例如,在火腿邮件中有2409封text/plain邮件。

然后我们用查看标题和它们的值。

for header, value in spam_emails[0].items():
    print(header,":",value)

邮件中的头信息是指诸如主题、日期等部分。输出的样本在这里。

Return-Path : <12a1mailbot1@web.de>
Delivered-To : zzzz@localhost.spamassassin.taint.org
Received : from localhost (localhost [127.0.0.1])   by phobos.labs.spamassassin.taint.org (Postfix) with ESMTP id 136B943C32    for <zzzz@localhost>; Thu, 22 Aug 2002 08:17:21 -0400 (EDT)
Received : from mail.webnote.net [193.120.211.219]  by localhost with POP3 (fetchmail-5.9.0)    for zzzz@localhost (single-drop); Thu, 22 Aug 2002 13:17:21 +0100 (IST)
Received : from dd_it7 ([210.97.77.167])    by webnote.net (8.9.3/8.9.3) with ESMTP id NAA04623 for <zzzz@spamassassin.taint.org>; Thu, 22 Aug 2002 13:09:41 +0100
From : 12a1mailbot1@web.de
Received : from r-smtp.korea.com - 203.122.2.197 by dd_it7  with Microsoft SMTPSVC(5.5.1775.675.6);  Sat, 24 Aug 2002 09:42:10 +0900
To : dcek1a1@netsgo.com
Subject : Life Insurance - Why Pay More?
Date : Wed, 21 Aug 2002 20:31:57 -1600
MIME-Version : 1.0
Message-ID : <0103c1042001882DD_IT7@dd_it7>
Content-Type : text/html; charset="iso-8859-1"
Content-Transfer-Encoding : quoted-printable

结论

我们使用python研究了电子邮件的解析。首先从SpamAssassin网站上获取邮件,并将其存储在我们的工作目录中。接下来,我们研究了如何获得电子邮件的结构,并进一步检查了电子邮件中的常见结构。

参与解析的主要模块是email 模块。