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)