Python郵件正文及附件解析

email郵件解析作為比較基礎(chǔ)的模塊昙篙,用來(lái)收取郵件惯殊、發(fā)送郵件骤肛。python的mail模塊調(diào)用幾行代碼就能寫一個(gè)發(fā)送/接受郵件的腳本纳本。但是如果要做到持續(xù)穩(wěn)定,能夠上生產(chǎn)環(huán)境的代碼腋颠,還是需要下一番功夫繁成,解決編碼和內(nèi)容異常的問(wèn)題∈缑担可能遇到的問(wèn)題如下:

  • 郵件編碼問(wèn)題
  • 郵件日期格式解析
  • 多附件的下載
  • 郵件如何增量解析巾腕?

一、連接郵件服務(wù)器

首先絮蒿,將郵件的賬戶密碼配置化:

# config.py
MAIL = {
    "mail_host": "smtp.exmail.qq.com",  # SMTP服務(wù)器
    "mail_user": "xxx@abc.com",  # 用戶名
    "mail_pwd": "fdaxxx",  # 登錄密碼
    "sender": "xxx@abc.com",  # 發(fā)件人郵箱
    "port":465  # SSL默認(rèn)是465
}

創(chuàng)建郵件連接尊搬,獲取郵件列表

from config.py import MAIL

# 連接到騰訊企業(yè)郵箱,其他郵箱調(diào)整括號(hào)里的參數(shù)
conn = imaplib.IMAP4_SSL(MAIL['mail_host'], MAIL['port'])
conn.login(MAIL['mail_user'], MAIL['mail_pwd'])
# 選定一個(gè)郵件文件夾
conn.select("INBOX")  # 獲取收件箱

# 提取了文件夾中所有郵件的編號(hào)
resp, mails = conn.search(None, 'ALL')

# 提取了指定編號(hào),按最新時(shí)間倒序
mails_list = mails[0].split()
mails_list = list(reversed(mails_list))
mail_nums = len(mails_list)

for i in range(mail_nums):
    print("mail: {}/{}".format(i+1, mail_nums))

    resp, data = conn.fetch(mails_list[i], '(RFC822)')   
    emailbody = data[0][1]
    mail = email.message_from_bytes(emailbody)

二.土涝、郵件編碼問(wèn)題

郵件主題中是一般是可以獲取到郵件編碼的佛寿,但也有獲取不準(zhǔn)的時(shí)候,這時(shí)就會(huì)報(bào)錯(cuò)回铛。這需要做編碼兼容性處理狗准。
decode_data()函數(shù)優(yōu)先采用郵件內(nèi)容獲取的編碼克锣,如果解析不成功,就依次用UTF-8腔长,GBK袭祟,GB2312編碼來(lái)解析。

# 獲取郵件自帶的編碼
from email.header import decode_header
mail_encode = decode_header(mail.get("Subject"))[0][1]
mail_title = decode_data(decode_header(mail.get("Subject"))[0][0], mail_encode)

def decode_data(bytes, added_encode=None):
    """
    字節(jié)解碼
    :param bytes:
    :return:
    """
    def _decode(bytes, encoding):
        try:
            return str(bytes, encoding=encoding)
        except Exception as e:
            return None

    encodes = ['UTF-8', 'GBK', 'GB2312']
    if added_encode:
        encodes = [added_encode] + encodes
    for encoding in encodes:
        str_data = _decode(bytes, encoding)
        if str_data is not None:
            return str_data
    return None

三捞附、郵件日期格式解析

郵件日期的格式一般是Mon, 8 Jun 2020 22:02:41 +0800這樣的巾乳,也有8 Jun 2020 22:02:41 +0800,去掉了星期鸟召。
要做到兼容胆绊,我只需要解析中間的年月日時(shí)分秒。

from datetime import datetime

def parse_mail_time(mail_datetime):
    """
    郵件時(shí)間解析
    :param bytes:
    :return:
    """
    print(mail_datetime)
    GMT_FORMAT = "%a, %d %b %Y %H:%M:%S"
    GMT_FORMAT2 = "%d %b %Y %H:%M:%S"
    index = mail_datetime.find(' +0')
    if index > 0:
        mail_datetime = mail_datetime[:index] # 去掉+0800

    formats = [GMT_FORMAT, GMT_FORMAT2]
    for ft in formats:
        try:
            mail_datetime = datetime.strptime(mail_datetime, ft)
            return mail_datetime
        except:
            pass

    raise Exception("郵件時(shí)間格式解析錯(cuò)誤")

四欧募、郵件增量解析

我們定義郵件的表結(jié)構(gòu)如下:

CREATE TABLE `mail_record_history` (
  `receive_time` datetime NOT NULL COMMENT '郵件接收時(shí)間',
  `title` varchar(200) NOT NULL COMMENT '郵件標(biāo)題',
  `mail_from` varchar(100) DEFAULT NULL,
  `content` text COMMENT '郵件內(nèi)容',
  `attachment` varchar(400) DEFAULT NULL COMMENT '郵件附件文件',
  `parse_time` datetime DEFAULT NULL COMMENT '解析時(shí)間',
  `status` int(11) DEFAULT NULL COMMENT '狀態(tài):-1:失敗压状,0:正常; -2: 文件大小為0',
  PRIMARY KEY (`receive_time`,`title`)
)

mail_record_history表的每條記錄對(duì)應(yīng)一份郵件,郵件接受時(shí)間和郵件標(biāo)題作為主鍵跟继。
通過(guò)表字段receive_time的最大值來(lái)作為增量解析郵件的標(biāo)準(zhǔn)是有缺陷的种冬。
python的mail模塊接口沒(méi)找到指定日期后的郵件,每次都是取全量的郵件序號(hào)舔糖,從最新的郵件開始解析娱两,如果程序一切順利(幾乎不可能),那是沒(méi)有問(wèn)題的金吗。
但是十兢,只有出現(xiàn)一次錯(cuò)誤,有可能是網(wǎng)絡(luò)超時(shí)摇庙,有可能是郵件服務(wù)器不響應(yīng)旱物,有可能是解析服務(wù)器故障,就會(huì)出現(xiàn)從最新日期到數(shù)據(jù)庫(kù)郵件最大日期之間丟失郵件跟匆。
而且下次再觸發(fā)郵件解析時(shí)無(wú)法從中斷處連續(xù)异袄。
這里,我們用redis來(lái)存儲(chǔ)最大郵件解析的時(shí)間點(diǎn)玛臂。

REDIS_PARAMS = {
        'host': "192烤蜕、168.1.111",
        'port': 6379,
        'password': 'xxxxx',
        'db': 14,
    }

def get_redis_client():
    r = redis.Redis(host=REDIS_PARAMS['host'], port=REDIS_PARAMS['port'], password=REDIS_PARAMS['password'], db=REDIS_PARAMS['db'])
    return r

redis_client = get_redis_client()
REDIS_KEY = "max_mail_recieve_time" 

每次解析先獲取數(shù)據(jù)庫(kù)中最新的郵件時(shí)間

def get_max_mail_recieve_time():
    """
    獲取數(shù)據(jù)庫(kù)最新郵件時(shí)間
    :return:
    """
    max_receive_time = redis_client.get(REDIS_KEY)
    if max_receive_time is None or max_receive_time == 'None':
        max_receive_time = "2020-01-01 00:00:00"  #
        redis_client.set(REDIS_KEY, max_receive_time)

    if isinstance(max_receive_time, bytes):
        max_receive_time = str(max_receive_time, encoding='utf-8')
    return max_receive_time


從最新郵件開始解析,當(dāng)郵件時(shí)間小于數(shù)據(jù)庫(kù)最新時(shí)間時(shí)迹冤,就終止解析

import arrow

max_recieve_time = get_max_mail_recieve_time()
max_mail_time_str = None
for i in range(mail_nums):
    print("mail: {}/{}".format(i+1, mail_nums))

    resp, data = conn.fetch(mails_list[i], '(RFC822)')
   
    emailbody = data[0][1]
    mail = email.message_from_bytes(emailbody)
    mail_datetime = parse_mail_time(mail.get("date"))

    if arrow.get(mail_datetime) < arrow.get(max_recieve_time):
        return
    if i == 0:
        max_mail_time_str = arrow.get(mail_datetime).format("YYYY-MM-DD HH:mm")

當(dāng)所有郵件都解析成功時(shí)讽营,才更新redis的數(shù)據(jù)庫(kù)最新時(shí)間(REDIS_KEY)。

if max_mail_time_str:
    redis_client.set(REDIS_KEY, max_mail_time_str)

五泡徙、郵件正文解析

mail_body = decode_data(get_body(mail))  

# 解析郵件內(nèi)容
def get_body(msg):
    if msg.is_multipart():
        return get_body(msg.get_payload(0))
    else:
        return msg.get_payload(None,decode=True)

六橱鹏、郵件附件下載

MAIL_DIR = '/tmp'
mail_date_str = '2020-06-09'

# 獲取郵件附件
fileNames = []
for part in mail.walk():        
    fileName = part.get_filename()

    # 如果文件名為純數(shù)字、字母時(shí)不需要解碼,否則需要解碼
    try:
        fileName = decode_header(fileName)[0][0].decode(decode_header(fileName)[0][1])
    except:
        pass

    # 如果獲取到了文件莉兰,則將文件保存在制定的目錄下
    if fileName:
        dirPath = os.path.join(MAIL_DIR, mail_date_str)
        os.system("chmod -R 777 {}".format(dirPath))
        if not os.path.exists(dirPath):
            os.makedirs(dirPath)

        filePath = os.path.join(dirPath, fileName)

        try:
            if not os.path.isfile(filePath):
                fp = open(filePath, 'wb')
                fp.write(part.get_payload(decode=True))
                fp.close()
                print("附件下載成功挑围,文件名為:" + fileName)
            else:
                print("附件已經(jīng)存在,文件名為:" + fileName)
        except Exception as e:
            print(e)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末糖荒,一起剝皮案震驚了整個(gè)濱河市杉辙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌捶朵,老刑警劉巖蜘矢,帶你破解...
    沈念sama閱讀 223,126評(píng)論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異综看,居然都是意外死亡品腹,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門红碑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)舞吭,“玉大人,你說(shuō)我怎么就攤上這事句喷×偷洌” “怎么了?”我有些...
    開封第一講書人閱讀 169,941評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵唾琼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我澎剥,道長(zhǎng)锡溯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,294評(píng)論 1 300
  • 正文 為了忘掉前任哑姚,我火速辦了婚禮祭饭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘叙量。我一直安慰自己倡蝙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評(píng)論 6 398
  • 文/花漫 我一把揭開白布绞佩。 她就那樣靜靜地躺著寺鸥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪品山。 梳的紋絲不亂的頭發(fā)上胆建,一...
    開封第一講書人閱讀 52,874評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音肘交,去河邊找鬼笆载。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凉驻。 我是一名探鬼主播腻要,決...
    沈念sama閱讀 41,285評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼涝登!你這毒婦竟也來(lái)了闯第?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,249評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤缀拭,失蹤者是張志新(化名)和其女友劉穎咳短,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛛淋,經(jīng)...
    沈念sama閱讀 46,760評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡咙好,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了褐荷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勾效。...
    茶點(diǎn)故事閱讀 40,973評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖叛甫,靈堂內(nèi)的尸體忽然破棺而出层宫,到底是詐尸還是另有隱情,我是刑警寧澤其监,帶...
    沈念sama閱讀 36,631評(píng)論 5 351
  • 正文 年R本政府宣布萌腿,位于F島的核電站,受9級(jí)特大地震影響抖苦,放射性物質(zhì)發(fā)生泄漏毁菱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評(píng)論 3 336
  • 文/蒙蒙 一锌历、第九天 我趴在偏房一處隱蔽的房頂上張望贮庞。 院中可真熱鬧,春花似錦究西、人聲如沸窗慎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)遮斥。三九已至,卻和暖如春商膊,著一層夾襖步出監(jiān)牢的瞬間伏伐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評(píng)論 1 275
  • 我被黑心中介騙來(lái)泰國(guó)打工晕拆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留藐翎,地道東北人材蹬。 一個(gè)月前我還...
    沈念sama閱讀 49,431評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吝镣,于是被迫代替她去往敵國(guó)和親堤器。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評(píng)論 2 361