本文使用python3实现QQ邮箱爬虫和Email解析,会将设计思路和核心代码分享在此处,欢迎大家多多评论交流,感谢?
近一年多来,因个人原因,坐火车往返跑了很多地方,也没有记录什么时间去了哪里,一共跑了多少次,频率怎么样,一共花了多少大洋…
我想知道具体的数据,怎么办?
那就爬下来呗?
怎么爬?
12306网站有查询购票记录的功能,但最多只保存了最近一段时间的,一年多的记录肯定没有。
但每次购票记录,12306都就发送邮件到账号绑定的邮箱。
翻了下我的QQ邮箱,嗯,确实都在。
那现在的问题变成了:QQ邮箱内容爬虫
经过前期探索,第1和第2两个方案,都需要搞清楚QQ邮箱的cookie和一大堆接口参数,预计花费较大时间,弃用。
直接选用第3种方案。
首先要了解邮件格式,详情参考:
邮件格式详解
MIME邮件格式分析及信息提取
SMTP电子邮件格式及源码解析
1、首先获取邮件
2、解析邮件内容
3、设计数据结构
4、解析关键字段:
邮件发送者send_email、
邮件类型支付/改签/退票type_email、
购票时间date_buy_ticket、
订单号码id_buy_ticket、
姓名name_ticket、
发车日期date_drives、
发车时间time_drive、
出发地址start_city、
目的地址aim_city、
车次number_train、
座号number_seat、
座位类型type_seat、
票价price_ticket、
退票费fee_refund_ticket、
应退票款price_refund_ticket
5、封装关键字解析方法
6、循环遍历所有邮件,只解析12306发送的邮件,如果邮件发送者send_email不是12306,直接跳过
7、打印所有结果
8、将结果数据存储到数据库
import poplib
from POP.utils import get_send_email, get_html_content, get_type_email, get_init_content
from POP.xpath import get_content, analyze_info_ticket
# 邮件地址,密码和POP3服务器地址
email = "[email protected]"
password = "asdfjkhdgjqlkajdgkl"
pop3_server = "pop.qq.com"
# 连接到POP3服务器
server = poplib.POP3_SSL(pop3_server)
# 可以打开或关闭调试信息
server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字
print(server.getwelcome().decode('utf-8'))
# 身份认证
server.user(email)
server.pass_(password)
# stat()返回邮件数量和占用空间
print('Messages: %s. Size: %s' % server.stat())
# list()返回邮件的编号
resp, mails, octets = server.list()
# 可以查看返回的列表类似[b'1 82953', b'2 8746', ...]
print(mails)
# 邮件指针编号,索引从1开始
index = len(mails)
# 关闭连接
server.quit()
上述中的password
是QQ邮箱的授权码
如何获取授权码
# 获取初始邮件内容
def get_init_content(num, s):
resp, lines, octets = s.retr(num)
init_content = b'\r\n'.join(lines).decode('utf-8')
msg = Parser().parsestr(init_content)
return msg
# 获取初始的html内容
def get_html_content(msg):
content = ''
if (msg.is_multipart()):
parts = msg.get_payload()
for n, part in enumerate(parts):
msg = part
content_type = msg.get_content_type()
if content_type == 'text/plain' or content_type == 'text/html':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
return content
下面是utils.py的所有关键方法
# coding=utf-8
from email.header import decode_header
from email.parser import Parser
# 从email header中获取信息v
# 比如发件人地址value_from_header: (msg, 'From')
# 如果判断是不是来自与12306的邮件,就可以 value.find('12306'), 如果是返回1,否则返回-1
def value_from_header(msg, v):
value = msg.get(v)
return value
# 获取邮件发送者
def get_send_email(msg):
send_email = value_from_header(msg, 'From')
return send_email
# 获取邮件类型
# 从header中获取邮件主题Subject, 用以获取此邮件类型:支付/退票/改签
def get_type_email(msg):
s = value_from_header(msg, 'Subject')
# 12306 Subject:"网上购票系统--用户支付通知"
# 截取"用户"后面的两个字符:支付/改签/退票
type_email = decode_str(s)[10:12]
return type_email
# 获取初始邮件内容
def get_init_content(num, s):
resp, lines, octets = s.retr(num)
init_content = b'\r\n'.join(lines).decode('utf-8')
msg = Parser().parsestr(init_content)
return msg
# 获取初始的html内容
def get_html_content(msg):
content = ''
if (msg.is_multipart()):
parts = msg.get_payload()
for n, part in enumerate(parts):
msg = part
content_type = msg.get_content_type()
if content_type == 'text/plain' or content_type == 'text/html':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
return content
# 邮件中的subject或者Email中包含的名字都是经过编码后的str,要正常显示,必须decode
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
# 获取邮件中的编码格式charset的值
def guess_charset(msg):
charset = msg.get_charset()
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset
最新的12306邮件截图
上述页面的HTML如下,要从如下HTML中获取到对应的关键字
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>12306通知邮件title>
<meta name="description" content="">
<meta name="keywords" content="">
<link href="" rel="stylesheet">
head>
<body>
<table cellspacing="0" cellpadding="0" width="760px"
style="border-spacing: 0; color: #333333; border: 1px solid #f1f1f1; margin-left: auto; margin-right: auto;">
<tr>
<td width="760">
<img src="http://mobile.12306.cn/weixin/resources/weixin/images/mail/mail_top.jpg" width="760" height="275" alt="">
td>
tr>
<tr>
<td width="720"
style="padding-left: 20px; padding-right: 20px; background: url(http://mobile.12306.cn/weixin/resources/weixin/images/mail/mail_train.jpg); background-position: bottom right; background-repeat: no-repeat;">
<table cellspacing="0" cellpadding="0" width="720px"
style="border-spacing: 0; color: #333333;">
<tr>
<td width="720"
style="font-size: 16px; height: 40px; font-weight: bold;">
尊敬的 <span style="color: #ff764c;">王先生:span>
td>
tr>
<tr>
<td width="720">
<div style="line-height: 20px; font-size: 12px;">您好!div>
<div style="line-height: 20px; font-size: 12px;">您于2020年06月18日在中国铁路客户服务中心网站(<a href="http://www.12306.cn" target="_blank" style="cozzz lor: #7095bd; text-decoration: none;">12306.cna>)
成功购买了2张车票,票款共计666.00元,订单号码
<span style="color: #ff764c; font-size: 14px;">EA66668888span>
。
所购车票信息如下:
div>
td>
tr>
<tr>
<td width="720" style="padding-top: 10px; padding-bottom: 10px;">
<div style="border-top: 1px dashed #e9ecf0; border-bottom: 1px dashed #e9ecf0; color: #000000; font-size: 14px; padding-top: 10px; padding-bottom: 10px;">
<div style="line-height: 20px; color: #000000; padding-top: 5px; padding-bottom: 5px; font-weight: bold;">
1.老王,2029年06月18日11:11开,北京-上海,G1888次列车,8车8D号,一等座,票价888.8元。
div>
<div style="line-height: 20px; color: #000000; padding-top: 5px; padding-bottom: 5px; font-weight: bold;">
2.小王,2019年06月18日11:11开,北京-上海,G1888次列车,8车8F号,一等座,票价888.8元。
div>
<div style="line-height: 20px; color: #000000; padding-top: 5px; padding-bottom: 5px; font-weight: bold;">
为了确保旅客人身安全和列车运行秩序,车站将在开车时间之前提前停止售票、检票,请合理安排出行时间,提前到乘车站办理换票、安检、验证并到指定场所候车,以免耽误乘车。
div>
div>
td>
tr>
<tr>
<td width="720" valign="middle" style="font-size: 16px; color: #ff764c; padding-top: 15px; padding-bottom: 15px;">
<img src="http://mobile.12306.cn/weixin/resources/weixin/images/mail/mail_tips.jpg"
alt="温馨提示" width="18" height="18"
style="vertical-align: -2px; margin-right: 10px;">温馨提示td>
tr>
<tr>
<td width="720" style="font-size: 12px;">
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(1)请牢记
<a href="http://www.12306.cn" target="_blank" style="cozzz lor: #7095bd; text-decoration: none;">12306.cna>
网站提供的订单号码
<span style="color: #7095bd; font-weight: bold;">EA66668888span>,并妥善保管,以确保您的购票信息安全。
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(2)选择车票快递服务的,请准备有效身份证件原件。您可在“已完成订单”-> “订单详情”-> “快递详情”中查看你的快递状态。当车票处于“待制票”状态时,用户可进行以下变更操作:
<br/>
<span style="margin-right: 10px;">
1>取消车票快递服务:您可单独选择取消车票快递服务,系统自动退还快递服务费。车票快递服务一经取消,同一订单无法再次提供车票快递服务。
span>
<br/>
<span style="margin-right: 10px;">
2>改签、变更到站、退票及换取纸质车票:您可自行办理车票的改签、变更到站、退票、换取纸质车票等业务。变更后符合快递服务条件的车票将按照原约定继续提供快递服务;变更后整件不符合快递服务条件的车票将取消快递服务,同时系统自动退还快递服务费。
span>
<br/>
<span style="margin-right: 10px;">
当车票处于“已制票”、 “派件中”状态时,您不能在网站办理取消车票快递服务及办理车票的改签、变更到站、退票、换票等业务,如有特殊情况可联系快递(物流)企业客户代表。
span>
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(3)根据国家车票实名制管理有关规定,换票及进站乘车时,请携带购票时所使用的乘车人有效身份证件原件;否则,不能换票,车站拒绝进站乘车,列车按无票处理。
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(4)<a href="http://www.12306.cn">12306.cna>网站对注册用户和常用联系人(乘车人)进行身份信息核验。如您的身份信息核验状态为“待核验”时,请携带购票时所使用的有效身份证件原件到车站售票窗口或者铁路客票代售点办理身份信息核验;为“未通过”(限居民身份证)、“请报验”时,请携带购票时所使用的有效身份证件原件到车站售票窗口办理。如果您拟委托他人办理时,请同时携带代办人和您的有效身份证件原件。办理时,如果您的居民身份证不能通过自动识读设备自动读取的,不能办理,请到发证机关换证后再办理。核验通过的,可以在车站售票窗口对已购车票办理换票、改签、变更到站、退票。您也可以在<a href="http://www.12306.cn">12306.cna>网站对已购车票正常办理退票。
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(5)使用乘车人本人的居民身份证在<a href="http://www.12306.cn">12306.cna>网站购票,且乘车站和下车站均具备居民身份证自动识读检票条件的,可以通过自动检票机(闸机)自动识读居民身份证的方式办理进、出站检票手续,无需提前换取纸质车票;因此乘车至到站后,需报销凭证时,请不晚于自乘车日期之日起31日,凭购票时所使用的居民身份证原件到车站售票窗口索取,逾期不予办理。
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(6)其他情形请换取纸质车票后进站乘车。自<a href="http://www.12306.cn">12306.cna>网站购票交易成功之时起,您即可到铁路客票代售点、办理客运售票业务的车站售票窗口或自动售(取)机换取纸质车票。如您拟于乘车前到乘车站换票,请合理安排出行时间,以避免因车站提前停止售票、排队人数多、来不及换票而耽误乘车。在车站售票窗口,请提供居民身份证原件;如果使用港澳居民来往内地通行证、台湾居民来往大陆通行证、按规定可使用的护照购票或者所使用的居民身份证不能识读的,请提供该有效身份证件原件和订单号码EA66668888。在自动售(取)票机,请使用购票时所使用的乘车人居民身份证原件。学生票、残疾军人(含伤残人民警察)票,请提供购票时所使用的有效身份证件和附有学生火车票优惠卡的学生证、“中华人民共和国残疾军人证”、“中华人民共和国伤残人民警察证”(均为原件),符合规定条件的,可以换票、进站、乘车。换票时,按规定核收异地售票手续费或铁路客票销售服务费。换票后,请妥善保管纸质车票,保持票面信息清晰、可识读,以便您顺利乘车。请注意妥善保护票面身份信息。
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(7)如果换票后丢失车票时,请不晚于票面乘车站停止检票时间前20分钟到车站售票窗口办理挂失补办。办理时,请提供购票时所使用的乘车人有效身份证件原件、原车票乘车日期和购票地车站名称等,经车站确认无误后,请按原车票车次、席位、票价重新购买一张新车票。持新车票乘车时,请向列车工作人员声明;到站前列车长经确认该席位使用正常的,将开具客运记录;请在到站后24小时内,凭客运记录、新车票和购票时所使用的有效身份证件原件,至到站退票窗口办理新车票退票。办理时,车站按规定核收补票的手续费。如果超过规定时间或者原车票已经退票、挂失补办的,不能办理挂失补办。新车票不能改签或变更到站,新车票不能改签或变更到站,但可以退票;退票时按规定核收补票的手续费。
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(8)如果改变行程或取消旅行,没有换取纸质车票且不晚于开车前30分钟的,可以在<a href="http://www.12306.cn">12306.cna>网站办理改签或者退票;已经换取纸质车票或者开车时间前30分钟之内的,请到车站售票窗口办理。在有运输能力的前提下,开车前48小时(不含)以上,可改签或变更到站为预售期内的其他列车;开车前48小时以内,可改签开车前的其他列车,也可改签开车后至票面日期当日24:00之间的其他列车,不办理票面日期次日及以后的改签;开车之后,旅客仍可改签当日其他列车,但只能在票面发站办理改签。在车站售票窗口办理时,请携带居民身份证原件;如果使用港澳居民来往内地通行证、台湾居民来往大陆通行证、按规定可使用的护照购票或者所使用的居民身份证不能识读的,请携带该有效身份证件原件和订单号码EA66668888。如果请他人代办退票时,还请携带代办人的有效身份证件原件。改签、变更到站均只可以办理一次,并且,改签不能改变原车票的发站和到站,变更到站不能改变原车票的发站。已经办理“变更到站”的车票不再办理改签,对已改签车票、团体票及通票暂不提供“变更到站”服务。改签、变更到站时,按购票时所使用在线支付工具的有关规定,如果新车票票价高于原车票时,请支付新车票全额票款(在铁路售票窗口请使用带有银联标志的银行卡,在<a href="http://www.12306.cn">12306.cna>网站请使用所支持的在线支付工具),原票款在规定时间内退回至购票时所使用的在线支付工具;新车票票价低于原车票的,退还差额,对差额部分核收退票费并执行现行退票费标准,应退票款在规定时间内退回购票时所使用的在线支付工具。新车票票价等于原车票的,无需办理支付。退票时,应退票款同样退回至购票时所使用的在线支付工具。
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(9)在车站办理换票、改签、变更到站、退票、挂失补办、身份信息核验、索取报销凭证、验证、候车、检票时,请注意车站公告以及售票窗口、验证口、候车室(区)、检票口等标识。
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(10)跨境车票退票规则:
<br/>
<span style="margin-right: 10px;">
1>到站为香港西九龙站的车票,办理退票应不晚于票面指定的日期、车次开车前30分钟;发站为香港西九龙站的车票应不晚于60分钟。
span>
<br/>
<span style="margin-right: 10px;">
2>退票费核收标准:在票面开车时间前48小时内办理退票的,按票面票价的50%计算;在票面开车时间前48小时至第14天的,按票面票价的30%计算;在票面开车时间前15天及以上的,按票面票价的5%计算。退票费按元计算,不足一元的部分舍去免收。
span>
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(11)跨境车票改签规则:
<br/>
<span style="margin-right: 10px;">
1>到站为香港西九龙站的车票,办理改签应不晚于票面指定的日期、车次开车前30分钟;发站为香港西九龙站的车票应不晚于60分钟。
span>
<br/>
<span style="margin-right: 10px;">
2>改签后的车票不得退票。
span>
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(12)跨境车票乘车规则:
<br/>
<span style="margin-right: 10px;">
1>旅客必须持有效车票并按票面载明的日期、车次、席别乘车,同时需携带有效的出入境证件及签注。
span>
<br/>
<span style="margin-right: 10px;">
更多跨境车票注意事项详见铁路跨境旅客相关运输组织规则和车站公告。
span>
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
(13)未尽事项,请关注<a href="http://www.12306.cn">12306.cna>网站或车站公告。
div>
<div style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">
感谢您使用中国铁路客户服务中心网站<span style="color: #ff764c;">12306.cnspan>!
本邮件由系统自动发出,请勿回复。
div>
<div
style="line-height: 20px; margin-bottom: 10px; font-size: 12px;">祝您的朋友(们)旅途愉快!
div>
td>
tr>
<tr>
<td width="720">
<table cellspacing="0" cellpadding="0" width="720px"
style="border-spacing: 0; color: #333333;">
<tr>
<td>td>
<td width="200"
style="text-align: center; height: 24px; font-size: 12px;">
<img src="http://mobile.12306.cn/weixin/resources/weixin/images/mail/mail_logo.jpg"
alt="logo" width="20" height="20"
style="vertical-align: bottom; margin-right: 10px;">中国铁路客户服务中心
td>
tr>
<tr>
<td>td>
<td width="200"
style="text-align: center; height: 24px; font-size: 12px;">2019年05月28日td>
tr>
table>
td>
tr>
<tr>
<td width="720" style="padding-top: 10px; padding-bottom: 15px;">
<img src="http://mobile.12306.cn/weixin/resources/weixin/images/mail/mail_line.jpg" alt="">
td>
tr>
table>
td>
tr>
table>
body>
html>
Demo:
支付:
'1.老王,2029年06月18日11:11开,北京-上海,G1888次列车,8车8D号,一等座,票价888.8元。'
'2.小王,2019年06月18日11:11开,北京-上海,G1888次列车,8车8F号,一等座,票价888.8元。'
退票:
'小明,2029年06月18日11:11开,上海-北京,K321次列车,18车88号,硬座,票价24.5元,退票费5.0元,应退票款19.5元。'
# coding=utf-8
from lxml import etree
# 获取邮件中主要信息
def get_content(content, type_email):
# 初始化
html = etree.HTML(content)
# 购票时间
date_buy_ticket = html.xpath("//table//table/tr[2]/td/div")[1].text[2:13]
# 订单号码
id_buy_ticket = html.xpath("//table//table/tr[2]/td/div[2]/span")[0].text
# 车票信息,返回一个lis0: tinfo_buy_ticket,因同一个订单(邮件)可能对应多张车票
# Demo:
# 支付: '1.小明,2029年06月18日18:18开,上海南-南京,K123次列车,8车66号,硬座,票价28.5元。'
# 退票: '小明,2029年06月18日11:11开,上海南-北京,K321次列车,18车88号,硬座,票价24.5元,退票费5.0元,应退票款19.5元。'
info_buy_ticket = []
if type_email == "支付":
# 当购买车票时,一个订单(邮件)可能对应多张车票
info_xpath = html.xpath("//table//table/tr[3]/td/div/div")
for i in range(len(info_xpath) - 1):
info_buy_ticket.append(info_xpath[i].text.strip())
else:
# 当退票/改签时,一个订单只对应一张车票
info_buy_ticket.append(html.xpath("//table//table/tr[3]/td/div/div")[0].text.strip())
return date_buy_ticket, id_buy_ticket, info_buy_ticket
# 解析邮件中车票信息
# info 是 get_content() 返回值中的第三个结果
def analyze_info_ticket(info):
l = info.split('。')[0].split(',')
# 姓名
# 兼容处理:
# 当info第一个元素的第一位是数字时,name从第一个元素的第2位开始取;
# 否则取整个第一个元素
if info[0].isdigit():
name_ticket = l[0][2:]
else:
name_ticket = l[0]
# 发车日期
date_drive = l[1][0:11]
# 发车时间
time_drive = l[1][-6:-1]
# 出发地址
start_city = l[2].split('-')[0]
# 目的地址
aim_city = l[2].split('-')[1]
# 车次
number_train = l[3].split(',')[0][:-3]
# 座号
number_seat = l[3].split(',')[1]
# 座号类型
type_seat = l[4]
# 票价
price_ticket = l[5][2:-1]
# 解析后的车票信息列表
info_list = [name_ticket, date_drive, time_drive, start_city, aim_city, number_train, number_seat, type_seat,
price_ticket]
return info_list
具体的爬虫过程相见如下代码,有问题再做详解
# 2018年1月1号起,12306邮件格式变化,只解析此后的邮件
# 第251封以后的邮件老格式邮件,此处不做解析
while index > 251:
msg = get_init_content(index, server)
temp_list = []
# 只解析来自12306的邮件
if get_send_email(msg).find('12306') == 1:
count_email += 1
# 获取初始的html内容
html_content = get_html_content(msg)
type_email = get_type_email(msg)
date_ticket, id_ticket, info_ticket = get_content(html_content, type_email)
for i in info_ticket:
temp_list = analyze_info_ticket(i)
temp_list.append(date_ticket)
temp_list.append(id_ticket)
temp_list.append(type_email)
info_ticket_final_list.append(temp_list)
index -= 1
print("来自12306的邮件共:%d" % count_email)
print("收集共 %d 条车票信息" % len(info_ticket_final_list))
# 购买车票总费用
price_num = 0
for i in info_ticket_final_list:
print(i)
if i[11] == "支付":
price_num += float(i[8])
print("共花费: %d" % price_num)
至此,一年多的12306购票记录爬取完成,什么时间去了哪里,跑了多少次,花了多少大洋等等,一清二楚?
TODO:将数据存储到数据库
参考连接
POP3收取邮件
email — An email and MIME handling package
poplib — POP3 protocol client