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('&nbsp;','')
    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('&nbsp;','')
    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:上效果