0x01 前言
在某些直播中计划进行抽奖活动,参加活动就得先报名,统计报名人员的信息是一个非常繁琐的工作。对于“懒到出汁”的我来说,手动统计是不可能的,这辈子都不可能的。
最近一次抽奖活动将在11号的直播中进行,而报名工作在我写这篇文章前就已经结束了,详细规则和其他内容请留意以下文章:
为了实现统计报名人数和“懒”的基本需求,我特意写了一个渣渣的python脚本。
0x02 思路与准备
为了方便使用脚本统一处理报名邮件,我针对我的实际情况制定了报名规则。需要编写标题格式如下的邮件并发送到指定的邮箱:
然后在邮件系统中新建一个文件夹用于存放报名邮件,还需要利用电子邮箱的自动规则功能识别邮件标题,并自动将报名邮件移动至该文件夹中。
因为我用的是office 365,所以这一切只需要在outlook中配置即可:
为了能通过脚本获取邮件,还需要开通邮箱系统的imap协议,以便通过imap协议登入邮件系统。
准备好之后即可开始编写脚本。
0x03 脚本
0x03.1 配置文件
首先需要准备配置文件,文件内容如下:
{
"email": {
"host": "outlook.office365.com",
"port": "993",
"username": "[email protected]",
"passwd": ""
},
"subj_prefix": "NGX Proj第二期抽奖活动",
"subj_count": 7
}
配置文件中包含电子邮箱的基本信息,因为我用的是office 365服务,所以信息如上。
还需要指定邮件标题的前缀,方便脚本中调用以便核查是否符合相关报名要求。最后是奖品类型的数量,这个数量的详情可以查看这次抽奖规则的文章。
0x03.2 定义基本信息
#!/usr/bin/python3 # -*- coding=utf-8 -*- import email import email.header import imaplib import os import sys from datetime import datetime # 定义目录 local_path = sys.path[0] # 邮箱账户信息配置文件 config_path = local_path + '/config.json' # 读取配置文件 r_conf = open(config_path, 'r') config_content = r_conf.read() r_conf.close() # 格式化配置文件 config_json = eval(config_content) # 邮件配置 imap_host = config_json['email']['host'] imap_port = config_json['email']['port'] imap_username = config_json['email']['username'] imap_passwd = config_json['email']['passwd'] subj_prefix = config_json['subj_prefix'] subj_count = config_json['subj_count'] # 通过IMAP登入邮箱服务器 comm = imaplib.IMAP4_SSL(imap_host, imap_port) comm.login(imap_username, imap_passwd) # 获取邮箱文件夹列表 # print(comm.list()) # 定义邮箱文件夹,list格式 mail_folder = ['INBOX/NGXProj&eyxOjGcfYr1ZVm07Uqg-']
脚本一开始是读取配置文件并将相关信息赋值给变量,方便后面的函数调用。
这个脚本需要用到email和imaplib这两个模块,imaplib主要实现与邮件服务器的imap通讯;而email则负责处理邮件内容。
首先调用imaplib模块登入邮件服务器,并获取邮箱文件夹列表,赋值给变量:mail_folder。如果邮箱文件夹的名称包含英文以外的文字,则会被编码,赋值的时候不需要解码,它输出什么就填写什么,如上所示。
0x03.3 获取邮件
电子邮件在文件夹中是有独一无二的编号的,而且这编号是自动增长的,根据收到邮件的先后顺序自行分配,就算其中的邮件被删除也不会变更其他在此文件夹内的邮件的编号。
所以获取邮件的第一步需要向邮件服务器索取这个邮件id列表,相关函数如下:
# 获取邮件uid
def get_uid_list(folder_name):
comm.select(folder_name)
response, uid_list = comm.uid('search', None, 'ALL')
uid_list = uid_list[0].decode().split(' ')
return uid_list
# 邮件服务器返还的内容如下
[b'1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 32 33 34 35 37']
# 该函数输出的内容如下
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '32', '33', '34', '35', '37']
有了邮件id,就可以通过id和文件夹名称向服务器拉去邮件主体:
# 获取邮件内容与标题(subject)
def get_mail_data(folder_name, mail_uid):
comm.select(folder_name)
response, mail_data = comm.uid('fetch', mail_uid, '(RFC822)')
mail_data = email.message_from_string(mail_data[0][1].decode())
msg_subj = email.header.decode_header(mail_data['Subject'])
msg_subj = msg_subj[0][0].decode(msg_subj[0][1])
msg_sender = email.header.decode_header(mail_data['From'])
if len(msg_sender) == 1:
msg_sender = msg_sender[0][0].strip("'").split('<')[1][:-1]
elif len(msg_sender) == 2:
msg_sender = msg_sender[1][0].decode()[2:-1]
else:
msg_sender = msg_sender[2][0].decode()[2:-1]
return mail_data, msg_subj, msg_sender
一封邮件中包含的内容非常多,但我只需要邮件的内容、标题和发件人等信息。通过email.message_from_string函数可以很轻易地获取邮件不同部件的内容,然后加以处理即可得到我想需要的内容。
拉取邮件的时候需要注意的是,我的需求是拉取邮件的全部内容,在本地自行解析,这时候需要使用:
response, mail_data = comm.uid('fetch', mail_uid, '(RFC822)')
上面代码的意义为通过邮件的uid执行“fetch”命令,拉取该uid邮件的全部内容。
还有一种拉取邮件部分内容的方式:
IMAP4.fetch(message_set, message_parts)
通过这种方式可以减少数据的传输,比如需要拉取邮件的主题时,只需要拉取“Subject”即可,但在需要拉取多部份内容的情况下会导致请求次数过多,很久可能超过服务器的限制而导致脚本执行失败。
剩下的内容则是一些字符上的处理,没什么特别的。
0x03.4 文件存储与文件夹的建立
获取到邮件id列表后需要将最后的邮件id记录到一个临时文件中,当下次运行脚本时需要与之进行比对,以确认是否有新的邮件。
这个脚本本来是支持多个邮件文件夹的,为适配抽奖,我将大部分功能都删减了。但某些老旧的代码依然保留下来,下面的临时文件写入函数就是其中一个:
# 写入日志(N年前的代码片段,能用,但无心修改)
def write_log_to_file(folder, mail_uid):
if os.path.exists(log_file_path):
if os.path.getsize(log_file_path):
f = open(log_file_path, 'r')
f_dict = f.readline()
f_dict = eval(f_dict)
f_dict[folder] = mail_uid
f = open(log_file_path, 'w')
f.write(str(f_dict))
f.close()
else:
f_dict = dict()
f = open(log_file_path, 'w')
f_dict[folder] = mail_uid
f.write(str(f_dict))
f.close()
else:
f_dict = dict()
f = open(log_file_path, 'w')
f_dict[folder] = mail_uid
f.write(str(f_dict))
f.close()
它会将传入的文件夹名称与邮件uid组合成字典的形式并写入临时文件中,内容格式如下:
{'INBOX/NGXProj&eyxOjGcfYr1ZVm07Uqg-': '37'}
本来我是计划将邮件保存功能一并删除的,但最后还是保留下来,以便日后查验邮件内容,以下是邮件内容的函数:
# 邮件保存函数
def write_content_to_file(folder, mail_id, file_content):
mail_file_path = data_store_dir + folder + '/' + str(mail_id) + '.eml'
w_file = open(mail_file_path, 'w')
w_file.write(str(file_content))
w_file.close()
对数据保持一个简洁明了的文件夹结构是非常重要的,所以还针对数据存储路径做了简单规划:
# 获取日期
today_date = datetime.today().date()
today_date = str(today_date).replace('-', '_')
# 定义目录,用于存储邮件数据
data_store_dir = local_path + '/datastore/' + today_date + '/'
# 建立数据存储目录
for i_folder_name in mail_folder:
mail_content_dir = data_store_dir + i_folder_name
if os.path.exists(mail_content_dir):
if not os.path.exists(mail_content_dir):
os.mkdir(mail_content_dir)
else:
os.makedirs(mail_content_dir)
以上代码会生成以下文件与目录结构:
[root@web tmp]# tree -L 5 . ├── config.json ├── datastore │ └── 2018_11_10 │ └── INBOX │ └── NGXProj&eyxOjGcfYr1ZVm07Uqg- │ ├── 10.eml │ ├── 11.eml │ ├── 12.eml │ ├── 13.eml │ ├── 14.eml │ ├── 15.eml │ ├── 16.eml │ ├── 17.eml │ ├── 18.eml │ ├── 1.eml │ ├── 2.eml │ ├── 32.eml │ ├── 33.eml │ ├── 34.eml │ ├── 35.eml │ ├── 37.eml │ ├── 3.eml │ ├── 4.eml │ ├── 5.eml │ ├── 8.eml │ └── 9.eml ├── main.py ├── output.csv └── temp.log 4 directories, 25 files
0x03.5 uid的判断
拉取邮件前判断uid是否有效是非常重要的,可以避免重复性工作。以下是相关函数:
# 检查邮件uid是否已经存在
def check_last_mail_uid(folder, mail_uid):
temp_file_path = local_path + '/' + '.temp.log'
try:
temp_content = open(temp_file_path, 'r')
except FileNotFoundError:
return None, 0
else:
temp_content = temp_content.read()
temp_dict = eval(str(temp_content))
if folder in temp_dict:
mail_old_uid = temp_dict[folder]
if int(mail_uid) > int(mail_old_uid):
return True, mail_old_uid
else:
return False, 0
else:
return None, 0
这里分3中情况:
- 临时文件不存在:这说明从未进行过有效的邮件拉取动作,所以没有生成临时文件
- 有临时文件但文件夹名称不在字典内:这说明之前进行过有效的拉取动作,但未拉取过当前的目标目录
- 有临时文件且字典内有文件夹名称,且有与之对应的邮件id:这说明以前成功拉取过当前目标目录的邮件。
上面的函数会分别给三种不同的情况返还不同的内容,以便进行后续的工作。
0x03.6 邮件标题的格式化
在邮件源码内获取到邮件标题并不能直接使用,还需要对字符串进行处理:
# 格式化邮件标题,输出QQ号码与奖品列表
def format_subj(mail_subj):
mail_subj_list = mail_subj.split('/')
if mail_subj_list[0] == subj_prefix and len(mail_subj_list) == 3:
applicant_id = mail_subj_list[1]
subj_id_list = list(mail_subj_list[2])
return applicant_id, subj_id_list
else:
return False
我要求的邮件主题格式如下:
NGX Proj第二期抽奖活动/21999888/2367
首先以“/”为元素对字符串进行分割,形成列表,然后判断列表的第一个元素是否与配置文件中“subj_prefix”的值相匹配,且列表共有3个元素。如果是则进一步处理,否则返还False,告知程序跳过这个邮件的处理流程。
当邮件主题符合要求后,会将第三个元素列表化,与第二个元素一起返还。
0x03.7 csv
逗号分隔文档–csv的格式是最简单的,直接写入到文本文档即可,以下函数就是干这工作的:
# 输出csv文件
def csv_output(mail_sender, applicant_id, subj_id_list):
csv_output_front_part = mail_sender + ',' + applicant_id + ','
count_start = 1
while subj_count + 1 > count_start:
if str(count_start) in subj_id_list:
csv_output_front_part += 'True,'
else:
csv_output_front_part += 'False,'
count_start += 1
csv_file_path = local_path + '/output.csv'
if os.path.exists(csv_file_path):
csv_output_content = csv_output_front_part[:-1] + '\n'
else:
first_row_front_part = 'mail_sender' + ',' + 'applicant_id' + ','
count_start = 1
while subj_count + 1 > count_start:
first_row_front_part += str(count_start) + ','
count_start += 1
csv_output_content = first_row_front_part[:-1] + '\n' + csv_output_front_part[:-1] + '\n'
w_csv = open(csv_file_path, 'a')
w_csv.write(csv_output_content)
w_csv.close()
在写入之前需要组合每行的内容,通过遍历传入的奖品项目,然后将其转换为“True”和“False”即可。
接下来需要判断csv文件是否存在,如果不存在还需要组合首行内容,再和主要内容一起写入文件;如果文件存在则将主要内容写入即可。
写入后的内容如下图:
使用csv的一大好处是可以通过excel打开,打开后稍作修饰即可保存为excel的格式:
0x03.8 RUN
最后是run函数:
def run():
for i in mail_folder:
mail_uid_list = get_uid_list(i)
mail_uid_new = mail_uid_list[-1]
check_mail = check_last_mail_uid(i, mail_uid_new)
check_mail_uid = check_mail[0]
check_mail_value = check_mail[1]
# 该uid较temp文件中的大,说明有新邮件
if check_mail_uid:
mail_uid_old = int(check_mail_value)
while mail_uid_old < int(mail_uid_new):
mail_content = get_mail_data(i, str(mail_uid_old))
format_subj_tube = format_subj(mail_content[1])
if mail_content[0] is None or format_subj_tube is False:
write_log_to_file(i, mail_uid_old)
else:
write_content_to_file(i, mail_uid_old, mail_content[0])
csv_output(mail_content[2], format_subj_tube[0], format_subj_tube[1])
write_log_to_file(i, mail_uid_old)
mail_uid_old += 1
# 临时文件不存在或临时文件中没有相关邮件文件夹的key,说明从未下载过邮件
elif check_mail_uid is None:
for k in mail_uid_list:
mail_content = get_mail_data(i, k)
format_subj_tube = format_subj(mail_content[1])
if mail_content[0] is None or format_subj_tube is False:
write_log_to_file(i, k)
else:
write_content_to_file(i, k, mail_content[0])
csv_output(mail_content[2], format_subj_tube[0], format_subj_tube[1])
write_log_to_file(i, k)
# uid与临时文件中的值一致,说明无新邮件
else:
pass
逻辑比较清晰,但是功能实现可能不太理想,主要是调用上面所说的各类函数。
0x04 结语
经过半天的调试,终于完成基本功能,估计以后的抽奖活动也会采用这种报名方式。在后续可能会增加实时展示功能,通过该功能可以将报名情况写入数据库,在前端显示。
生命在于折腾,其实我发现wordpress是由相关功能的插件的,但我就是不用!
通过我私有的gitlab可以找到最新的代码,还可以查看更新记录噢:


























