0x01:前言
这就是我大学四年到现在的所有火车票,599张,青春的回忆永存,纸质火车票我是一张都没有丢掉,全部保留了下来,等老了,走不动了,坐在摇椅上,翻开一下,满满的回忆。纸质的回忆可永久保存,但随着时间流逝将会褪色,淡去,不妨可以通过代码的形式将其量化,展现出他的另一面有没有想做一下自己的轨迹地图,对 是自己亲手做的哦,那么福利来了, fork here https://github.com/0x024/etm

0x02:思维导图

0x03:代码解读
step1:
因为我的12306绑定的是QQ邮箱,每次购票记录均会保存在QQ邮箱中,所以本文章均已此邮箱为例,PS:网易邮箱由于安全机制的问题,无法通过IMAP方式读取邮箱内的邮件,有点遗憾
host = 'imap.qq.com' //QQ邮箱imap地址
user = 'sXXXXXXX@163.com' //QQ邮箱地址
passwd = 'ZFXXXXXXXXGH' //需要在邮箱配置界面开启IMAP后,会生成对应的密码,此密码非QQ邮箱密码
raw_conn = imaplib.IMAP4_SSL(host,)
raw_conn.login(user,passwd) //进行QQ邮箱登录
raw_conn.select()
type,data=raw_conn.search(None,'ALL') //获取所有邮件,默认是INBOX,之前有自己创建过分类文件夹,但是都无法获取到,干脆直接获取全量邮件
email_list=data[0].split() //在此就不得不说下邮件的组成格式 见下面
step2:邮件格式分析
1)邮件头包含了发件人、收件人、主题、时间、MIME版本、内容的类型、内容的传输编码方式等重要信息。每条信息称为一个域,由域名后加冒号(“ : ”)和信息内容构成,可以是一行,也可以占用多行。域的首行必须顶头写(即左边不能有空白字符(空格和制表符));续行则必须以空白字符打头,且第一个空白字符不是信息本身固有的,解码时要过滤掉。另外,邮件头中不允许出现空行。
例如:
Date: Wed, 15 Aug 2017 10:09:00 +0800
From: simba@www.simba.com
To: "simba" <simba@www.simba.com>
Cc: "simba" <simba@www.simba.com>
BCC: "simba" <simba@www.simba.com>
Subject: test
Message-ID: <20170815100900.0E67523E1438@www.test3.com>
X-mailer: Foxmail 6, 15, 201, 21 [cn]
X-Spam: yes
X-Rmilter-Greylist: Sender IP ::1 is whitelisted by configuration
Mime-Version: 1.0
2)邮件内容有各种各样的(既纯文本,超文本,内嵌资源(比如内嵌在超文本中的图片),附件的组合),服务器通过第一个content-type判断邮件内容,如果包含了其他内容,邮件体被分为多个段,段中可包含段,每个段又包含段头和段体两部分
+----------------------------------------multipart/mixed----------------------------------------+
| |
| |
| +---------------------------multipart/related---------------------------+ |
| | | |
| | | |
| | +---------mutipart/alternative--------------+ +-------------+ | +-------+ |
| | | | | 内嵌资源 | | | 附件 | |
| | | | +-------------+ | +-------+ |
| | | +---------------+ +---------------+ | | |
| | | | 纯文本正文 | | 超文本正文 | | | |
| | | +---------------+ +---------------+ | +-------------+ | +-------+ |
| | | | | 内嵌资源 | | | 附件 | |
| | | | +-------------+ | +-------+ |
| | +-------------------------------------------+ | |
| | | |
| | | |
| +-----------------------------------------------------------------------+ |
| |
| |
+-----------------------------------------------------------------------------------------------+
step3:
信息获取邮件内容都拿到了,那就将各个字段信息分别拿下来,后续使用
def get_subject(num): //获取邮件主题----好像用不到哈
type,data=raw_conn.fetch(num,'(RFC822)')
try:
msg=BytesParser().parsebytes(data[0][1])
sub=decode_str(msg.get('subject'))
print(sub)
return sub
except TypeError:
print ('empty-email')
except UnicodeDecodeError:
print ('hahah')
def get_from(num): //获取发件人,这个100%用到,后续就是根据这个来判定是否为12306的邮件
type,data=raw_conn.fetch(num,'(RFC822)')
try :
msg=BytesParser().parsebytes(data[0][1])
sub=decode_str(msg.get('From'))
#print (sub)
return sub
except TypeError:
print ('empty-email')
#print(sub)
except UnicodeDecodeError:
print ('hahah')
def get_date(num): //获取邮件时间,你会发现12306的研发真的够狠,不按常理出牌
type,data=raw_conn.fetch(num,'(RFC822)')
try :
msg=BytesParser().parsebytes(data[0][1])
sub=msg.get('Date')
#print(sub)
return sub
#print(num, decode_str(sub))
except TypeError:
print ('empty-email')
except UnicodeDecodeError:
print ('hahah')
def get_content(num,id): //获取邮件体,重中之重
type,data=raw_conn.fetch(num,'(RFC822)')
email_date=get_date(email_list[int44(count)])
try :
msg=BytesParser().parsebytes(data[0][1])
for part in msg.walk():
if not part.is_multipart():
charset = part.get_charset()
contenttype = part.get_content_type()
content=part.get_payload(decode=True)
content=content.decode('GBK')
temp=time_formate(email_date)
if temp=='1':
print('该邮件使用的老的内容,现使用V1版本进行解析')
TransferV2.get_transfer_v1(content,id)
elif temp=='2':
print('该邮件使用的新的内容,现使用V2版本进行解析')
TransferV2.get_transfer_v2(content,id)
except TypeError:
print ('empty-email')
except UnicodeDecodeError:
print ('hahah')
0x04:
拆分邮件体你会惊奇的发现,邮件体格式分两类,在Fri 10 Nov 2017 00:00:00之前 邮件内容是txt形式,在该时间之后变成了html格式,同时还发现邮件体中有用到中文括号,英文括号,中文句号,英文句号所有我不得不先做个替换。把这些字段全部屏蔽掉,但是越到后面发现,这明显是个无底洞。
a1=content.replace(' ','')
a2=a1.replace('<a <a ','<a ')
a3=a2.replace('(<a href="http://www.12306.cn">www.12306.cn</a>)','')
a4=a3.replace('(<a href="http://www.12306.cn">www.12306.cn</a>)','')
a5=a4.replace('(<a href="http://www.12306.cn">12306.cn</a>)','')
a6=a5.replace('(<a href="http://www.12306.cn">12306.cn</a>)','')
1):我大概就把邮件分为V1 V2版本,以Fri 10 Nov 2017为基准,来分别做解析,谈到时间 ,不等不先把所有的时间进行统一处理
def order_time_formate(o_time): //订单时间
str(o_time)
n_time=datetime.datetime.strptime(o_time,"%Y%m%d")
return n_time
def train_time_formate(o_time): //车票时间
n_time=datetime.datetime.strptime(o_time,"%Y%m%d%H:%M")
return n_time
2):以V2来为例,票据分为 购票,改签,退票购票:一个订单下可以有多个乘客 改签:一个订单只能有一个乘客改签 退票:一个订单可以有多个乘客退票 (不要问我咋知道,一个一个试的)同时根据票务内容,将数据库结构进行相关设计

3):就以购票来说,细细分析行
def get_transfer_v2(content,id):
a1=content.replace(' ','')
a2=a1.replace('<a <a ','<a ')
a3=a2.replace('(<a href="http://www.12306.cn">www.12306.cn</a>)','')
a4=a3.replace('(<a href="http://www.12306.cn">www.12306.cn</a>)','')
a5=a4.replace('(<a href="http://www.12306.cn">12306.cn</a>)','')
a6=a5.replace('(<a href="http://www.12306.cn">12306.cn</a>)','')
tree=etree.HTML(a6)
contents=tree.xpath('//text()') //使用xpath 获取html的所有文本信息
line_chick_part1=contents[30].replace("\n",'').replace("\t",'').replace("\t",'')
line_chick_part2=contents[32].replace("\n",'').replace("\t",'').replace("\t",'')
line_chick_part3=contents[33].replace("\n",'').replace("\t",'').replace("\t",'')
order_type=line_chick_part2
if '购买' in order_type :
print("~~~~~~~~~~~~~~~~~part_2_start~~~~~~~~~~~~~~~~~~~~~~~")
#order_count=int(line_chick_part2.split(",")[0][-4])
order_count=int(re.findall("\d+",line_chick_part2.split(",")[0])[0]) // print ('此订单包含'+str(order_count)+'张车票')
order_no=line_chick_part3 // print("订单号:"+order_no)
order_purchaser='张文' //print("操作人员:"+order_purchaser)
order_date_o=line_chick_part1[2:13].replace("年",'').replace("月",'').replace("日",'')
order_date=Timeformate.order_time_formate(order_date_o) //print("订单日期:"+order_date_o)
order_price=line_chick_part2.split(",")[1][4:] //print("订单金额:"+order_price)
order_type="购买" //print("订单状态:"+order_type)
count =1
while count < order_count+1: //通过这里来判断该订单中有多少个乘客
list_detail=contents[(count*2)+39].replace("\n",'').replace("\r",'').replace("\t",'').split(",")
print (list_detail) //print ('第'+str(count)+'张车票信息如下')
train_passenger=list_detail[0].split('.')[1] //print ("火车乘客:"+train_passenger)
train_date_o=list_detail[1][:-1].replace("年",'').replace("月",'').replace("日",'')
train_date=Timeformate.train_time_formate(train_date_o) //print ("发车日期:"+train_date_o)
train_no=list_detail[3].split(",")[0].replace("次列车",'') //print ("火车车次:"+train_no)
train_price=list_detail[5][2:].replace("。",'') //print ("火车票价:"+train_price)
train_type=list_detail[3][0]
if train_type.isdigit():
train_type='绿皮' //print ('火车类型:绿皮火车')
else:
print ('火车类型:'+train_type)
start_station=list_detail[2].split('-')[0] //print ("出发站:"+start_station)
start_lng=Location.get_local(start_station)[0] //print ("经度:"+str(start_lng))
start_lat=Location.get_local(start_station)[1] //print ("维度:"+str(start_lat))
stop_station=list_detail[2].split('-')[1] //print ("终点站:"+stop_station)
stop_lng=Location.get_local(stop_station)[0] //print ("经度:"+str(stop_lng))
stop_lat=Location.get_local(stop_station)[1] // print ("维度:"+str(stop_lat))
sit_type=list_detail[4] //print ("座位类型:"+sit_type)
sit_row=list_detail[3].split(",")[1].split("车")[0] // print ("车厢号:"+sit_row)
sit_no=list_detail[3].split(",")[1].split("车")[1].split("号")[0] //print ("座位号:"+sit_no)
try:
raw_sit=list_detail[3].split(",")[1].split("车")[1].split("号")[1]
except IndexError:
print ("无座")
if sit_type=="硬卧": //承认后来赋予了,都是做的卧铺
if"上" in raw_sit:
sit_flow='上'
print ("卧铺位置:"+sit_flow)
elif"中" in raw_sit:
sit_flow='中'
print ("卧铺位置:"+sit_flow)
elif "下" in raw_sit:
sit_flow='下'
print ("卧铺位置:"+sit_flow)
if '郑州东' in start_station:
try:
ticket_entrance=list_detail[6][3:].split("。")[0].replace(":",'')
print ("检票口:"+ticket_entrance)
except IndexError:
print ("虽是郑州东站,但确实没标记进站口")
else:
print("非郑州东站,不检测检票口")
count=count+1
print("~~~~~~~~~~~~~~~~~part_2_end~~~~~~~~~~~~~~~~~~~~~~~")
0x05:
建表写数据之前有想过将相关数据写在cvs或者excel中,但是那样显得有点low ,写在数据库中高大上呀。先创建数据表结构,再把需要写库的地方都搞个INSERT,写入到数据库中做归类后。数据就变得那么清爽
def build_etmdb():
conn =sqlite3.connect(db_pwd)
c = conn.cursor()
try:
c.execute('''
CREATE TABLE "Refund" (
"order_id" varchar(32),
"order_no" varchar(32),
"order_purchaser" varchar(32),
"order_date" DATE(32),
"order_price" varchar(100),
"order_type" varchar(32),
"train_passenger" varchar(32),
"train_date" DATE(32),
"train_no" varchar(32),
"train_price" varchar(32),
"transfer_fee" varchar(32),
"drawback_fee" varchar(32),
"train_type" varchar(32),
"start_station" varchar(32),
"start_lng" varchar(32),
"start_lat" varchar(32),
"stop_station" varchar(32),
"stop_lng" varchar(32),
"stop_lat" varchar(32),
"sit_type" varchar(32),
"sit_row" varchar(32),
"sit_no" varchar(32),
"sit_flow" varchar(32)
);''')
c.execute("INSERT INTO purchase (order_id,order_no,order_purchaser,order_date,order_count,order_price,order_type,train_passenger,train_date,train_no,train_price,train_type,start_station,start_lat,start_lng,stop_station,stop_lat,stop_lng,sit_type,sit_row,sit_no) VALUES('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')"%(id,order_no,order_purchaser,order_date,order_count,order_price,order_type,train_passenger,train_date,train_no,train_price,train_type,start_station,start_lat,start_lng,stop_station,stop_lat,stop_lng,sit_type,sit_row,sit_no))
conn.commit()

1):很可能要问,坐标数据哪来的,高德呀,这么好的免费的API不用白不用,
def get_local(station):
if "站" not in station:
station=station+"站" //
c.execute("select * from location where station='%s'"%(station)) //将需要的站点名,现在locatin数据库中找下,如果存在正好,不用继续调用高德接口,如果站点名不在的话,再运行API
temp_results = c.fetchall()
conn.commit()
if len(temp_results)==0: //通过使用高德的API,将站名传入,然后将返回的信息进行写库处理
result_raw=Popen('curl -X GET "http://restapi.amap.com/v3/geocode/geo?address=%s&output=JSON&key=%s"'%(station,key),shell=True,stdout=PIPE)
result=(result_raw.stdout.read()).decode('utf-8')
result=json.loads(result)
formatted_address=result["geocodes"][0]["formatted_address"]
country=result["geocodes"][0]["country"]
province=result["geocodes"][0]["province"]
city=result["geocodes"][0]["city"]
try:
citycode=result["geocodes"][0]["citycode"]
except KeyError:
citycode="0000"
district=result["geocodes"][0]["district"]
street=result["geocodes"][0]["street"]
streetnumber=result["geocodes"][0]["number"]
adcode=result["geocodes"][0]["adcode"]
lnglat=str(result["geocodes"][0]["location"]) //经纬度
lng=result["geocodes"][0]["location"].split(",")[0] //经度
lat=result["geocodes"][0]["location"].split(",")[1] //维度
c.execute("PRAGMA busy_timeout = 10000") //这里确实写得有问题,在极少数情况下会造成database lock。只能通过增加时间来缓解,
conn.commit()
c.execute("INSERT INTO location (station,formatted_address,country,province,city,citycode,district,street,streetnumber,adcode,lng,lat) \
VALUES('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')"\
%(station,formatted_address,country,province,city,citycode,district,street,streetnumber,adcode,lng,lat))
conn.commit()
c.execute("PRAGMA busy_timeout = 10000")
conn.commit()
elif len(temp_results)==1:
lng=temp_results[0][10]
lat=temp_results[0][11]
return (lng,lat) //将经纬度信息返回

0x06:
生成js数据好吧 实在不会写js前端,只能拿人家高德的demo直接用了,
pwd=os.getcwd() //这个很重要,因为是在根目录下运行的脚本,为了避免路径出错,都用绝对路径来处理文件
print (pwd)
js_pwd=pwd+"/www/html/city_line.js"
db_pwd=pwd+"/database/etm.db"
conn =sqlite3.connect(db_pwd)
cursor=conn.cursor()
cursor.execute("select * from purchase where train_passenger='张文';") //将张文乘客的所有轨迹信息拿出来
values=cursor.fetchall()
city_lines=[]
for i in range(len(values)): //将读取的数据,写成高德需要的格式
start_station=str(values[i][14])
start_lng=values[i][15]
start_lat=values[i][16]
start_lnglat=start_lng+","+start_lat
stop_staton=str(values[i][17])
stop_lng=values[i][18]
stop_lat=values[i][19]
stop_lnglat=stop_lng+","+stop_lat
name=str(start_station+"-"+stop_staton)
line=str("\""+start_lnglat+"\",\""+stop_lnglat+"\"")
city_line="{\"name\":\""+name+"\","+"\"line\":["+line+"]}"
city_lines.append(city_line)
f = open(js_pwd, "a")
f.write("var city_line ="+str(city_lines).replace("'",""))
f.close()
cursor.close()
conn.close()
0x07:上效果

