Python实现12306车票查询

今天准备给师傅一块去应急响应,票都定好了,发现忘带身份证了,MDZZ…所以我就这样错失了一次宝贵的机会…更™可恶的是我去退票,竟然给我说没有身份证退不了。。。我特么的要是有身份证,我还退你大爷啊。。。

所以想搞它,一想,算了,凭我这本事搞12306还是别装逼,所以就有了这个脚本。。。

源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/python
# -*- coding: UTF-8 -*-
'''
@Author:w2n1ck
@博客:http://byd.dropsec.xyz/
'''
import urllib2
import json
import smtplib
import time
import codecs
from email.mime.text import MIMEText
import ssl
# 记录日志
def log(content):
t = time.strftime('%Y-%m-%d %H:%M:%S')
f = codecs.open('watcher.log', 'a', 'utf-8')
f.write('[%s]%s\n' % (t, content))
f.close()
# 发送邮件
def send_mail(content):
to_list=['xxx@qq.com']
mail_host = 'smtp.163.com'
mail_user = 'xxx'
mail_pass = 'xxx'
mail_postfix = '163.com'
me = "TicketsWatcher"+"<"+mail_user+"@"+mail_postfix+">"
msg = MIMEText(content,_subtype='plain',_charset='gb2312')
msg['Subject'] = 'There are some tickets you need.'
msg['From'] = me
msg['To'] = ";".join(to_list)
server = smtplib.SMTP()
server.connect(mail_host)
server.ehlo()
server.starttls()
server.login(mail_user,mail_pass)
server.sendmail(me, to_list, msg.as_string())
server.close()
log('sent mail successfully')
try:
# 请求地址根据实际要抓取的页面修改,参数包括日期、出发站、到达站
ssl._create_default_https_context = ssl._create_unverified_context
resp = urllib2.urlopen("https://kyfw.12306.cn/otn/lcxxcx/query?purpose_codes=ADULT&queryDate=2016-10-11&from_station=NJH&to_station=SHH")
#print resp
result = resp.read()
#print result
data = json.loads(result)
datas = data['data']['datas']
print datas
for d in datas:
if d['station_train_code'] == 'T135':
content = 'tickes for hard seat of %s: %s' % (d['station_train_code'], d['yz_num'])
log(content)
if unicode(d['yz_num']) != u"无":
send_mail(content)
break
except Exception, e:
content = 'somethings wrong with the program:\n' + str(e)
log(content)
send_mail(content)

测试结果:


说明:

脚本分为三部分:

  • 记录日志
  • 发送邮件
  • 车票信息捕捉

记录日志会在脚本目录下生成一个watch.log文件,这个主要是得结合实时捕捉数据,你可以定时也可以使用crontab,这里我就没在加了(还有十个网站没测呢,政府网站真是尼玛啊,谁有比较好的经验望大牛们不吝分享,不说了都是泪…)

发送邮件部分主要用了smtplib和email库,具体代码为:

1
2
3
4
5
to_list=['xxx@qq.com'] #接收通知的邮箱
mail_host = 'smtp.163.com' #设置服务器
mail_user = 'xxx' #替换为发件邮箱用户名,不带@后面的
mail_pass = 'xxx' #替换为发件邮箱口令
mail_postfix = '163.com' #发件箱的后缀

1
msg = MIMEText(content,_subtype='plain',_charset='gb2312')

第一个参数就是邮件正文,第二个参数是MIME的subtype,传入’plain’,最终的MIME就是’text/plain’,最后设置编码为gb2312,不过为了兼容性,你可以使用utf-8,具体的过程函数我就不解释了。

信息抓取部分,很简单就是把数据变为数组,从数组中匹配信息。这里我遇到一个问题:urllib2.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590)>
这个错误信息主要是因为:SSL: CERTIFICATE_VERIFY_FAILEDPython 升级到 2.7.9 之后引入了一个新特性,当使用urllib.urlopen打开一个 https 链接时,会验证一次 SSL 证书。而当目标网站使用的是自签名的证书时就会抛出一个 urllib2.URLError: 的错误消息.

解决方案:

1
2
3
4
import ssl
import urllib2
ssl._create_default_https_context = ssl._create_unverified_context
print urllib2.urlopen("https://www.111cn.net/").read()

附:

smtp协议的基本命令包括:

1
2
3
4
5
6
7
8
9
10
11
12
HELO 向服务器标识用户身份
MAIL 初始化邮件传输 mail from:
RCPT 标识单个的邮件接收人;常在MAIL命令后面,可有多个rcpt to:
DATA 在单个或多个RCPT命令后,表示所有的邮件接收人已标识,并初始化数据传输,以.结束
VRFY 用于验证指定的用户/邮箱是否存在;由于安全方面的原因,服务器常禁止此命令
EXPN 验证给定的邮箱列表是否存在,扩充邮箱列表,也常被禁用
HELP 查询服务器支持什么命令
NOOP 无操作,服务器应响应OK
QUIT 结束会话
RSET 重置会话,当前传输被取消
MAIL FROM 指定发送者地址
RCPT TO 指明的接收者地址

SMTP会话的流程:

  1. ehlo
  2. auth login
  3. mail from
  4. rcpt to
  5. data
  6. quit

上面说的是最普通的情况,但是现在好多企业邮件都是安全邮件的,就是通过SSL发送的邮件,这个怎么发呢?SMTP对SSL安全邮件的支持有两种方案,一种老的是专门开启一个465端口来接收ssl邮件,另一种更新的做法是在标准的25端口的smtp上增加一个starttls的命令来支持。

这个很简单,smtplib里就有这个方法,叫smtplib.starttls()。当然,不是所有的邮件系统都支持安全邮件的,这个需要从ehlo的返回值里来确认,如果里面有starttls,才表示支持。

注意:
以上的代码为了方便我都没有判断返回值,严格说来,是应该判断一下返回的代码的,在smtp协议中,只有返回代码是2xx或者3xx才能继续下一步,返回4xx或5xx的,都是出错了。

大爷,赏个铜板呗!