写在前面
两周前完成了Python 12306验证码自动验证、用户登录和查询余票一文,后来总觉得写得有点凌乱,于是想进行重构,让整个项目结构看起来更加清晰明了。
项目结构
写完整个项目后觉得其实也很简单,无非是使用Session
进行多次Get
和Post
请求,难点在于Post请求时使用的Data
从何而来?我们先使用抓包工具(浏览器F12)完成一次12306平台订票之完整过程,对需要进行哪些网络请求心里有个大概印象。使用Session
的主要原因是为了避免每次请求数据时都去考虑Cookies
,如此可能会方便很多。
我们将整个订票过程中使用到的API 放在一个文件里,原因很简单:一旦某个接口地址改变了,我们只需在此文件里进行修改,无法在代码里到处查找修改,省时省力。我自己之前在写iOS
应用时候也是采用这样的方式。
12306 API
class API(object): # 登录链接 login = 'https://kyfw.12306.cn/passport/web/login' # 验证码验证链接 captchaCheck = 'https://kyfw.12306.cn/passport/captcha/captcha-check' # 获取验证码图片 captchaImage = 'https://kyfw.12306.cn/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand' # 车站Code stationCode = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js' # 查余票 queryTicket = 'https://kyfw.12306.cn/otn/leftTicket/query' # 查票价 queryPrice = 'https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice' # 检查用户 checkUser = 'https://kyfw.12306.cn/otn/login/checkUser' # 用户登录 userLogin = 'https://kyfw.12306.cn/otn/login/userLogin' uamtk = 'https://kyfw.12306.cn/passport/web/auth/uamtk' uamauthclient = 'https://kyfw.12306.cn/otn/uamauthclient' initMy12306 = 'https://kyfw.12306.cn/otn/index/initMy12306' # 确定订单信息 submitOrderRequest = 'https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest' # initDc,获取globalRepeatSubmitToken initDc = 'https://kyfw.12306.cn/otn/confirmPassenger/initDc' # 获取曾经用户列表 getPassengerDTOs = 'https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs' # 检查订单信息 checkOrderInfo = 'https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo' # 获取队列查询 getQueueCount = 'https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount' # 确认队列 confirmSingleForQueue = 'https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue'
常量
将项目里使用到的常量都集中在一个文件里,方便管理。特别需要注意的是座位类型不是固定的,我在写整个项目时发现有几个座位类型是变化的,比如硬座在我写本文的时候是1
,但是之前都是A1
,其他座位类型变化情况参见具体代码内容。
from codePlatform import CJYClient# 12306登录用户名userName = '你的12306账号'# 12306密码password = '你的12306密码'# 超级鹰打码平台chaoJiYing = CJYClient('你的超级鹰平台账户', '你的超级鹰平台密码','896970')# 验证码图片路径captchaFilePath = 'captcha.jpg'# 车站电报码路径stationCodesFilePath = 'stationsCode.txt'# 座位类型,订票下单时需要传入noSeat = 'WZ' #无座firstClassSeat = 'M' #一等座secondClassSeat = 'O' #二等座advancedSoftBerth = '6' #高级软卧 A6hardBerth = '3' #硬卧 A3softBerth = '4' #软卧 A4moveBerth = 'F' #动卧hardSeat = '1' #硬座 A1businessSeat = '9' #商务座 A9
Utility 工具类
通常项目中都会有很多共用方法,我们将这些方法抽离出来放在一个工具类文件里,如此可以减少冗余代码。
from datetime import datetimefrom stationCodes import StationCodesfrom color import Coloredimport timeimport requestsclass Utility(object): @classmethod def getSession(self): session = requests.session() # 创建session会话 session.headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36" } # session.verify = False # 跳过SSL验证 return session @classmethod def redColor(self,str): return Colored.red(str) @classmethod def greenColor(self, str): return Colored.green(str) # 反转字典 @classmethod def reversalDict(self, dict): return {v: k for k, v in dict.items()} # 将历时转化为小时和分钟的形式 @classmethod def getDuration(self, timeStr): duration = timeStr.replace(':', '时') + '分' if duration.startswith('00'): return duration[4:] return duration # 获取一个时间是周几 @classmethod def getWeekDay(self, date): weekDayDict = { 0: '周一', 1: '周二', 2: '周三', 3: '周四', 4: '周五', 5: '周六', 6: '周天', } day = datetime.strptime(date, '%Y-%m-%d').weekday() return weekDayDict[day] # 转化日期格式 @classmethod def getDateFormat(self, date): # date格式为2018-08-08 dateList = date.split('-') if dateList[1].startswith('0'): month = dateList[1].replace('0', '') else: month = dateList[1] if dateList[2].startswith('0'): day = dateList[1].replace('0', '') else: day = dateList[2] return '{}月{}日'.format(month, day) # 检查购票日期是否合理 @classmethod def checkDate(self, date): localTime = time.localtime() localDate = '%04d-%02d-%02d' % (localTime.tm_year, localTime.tm_mon, localTime.tm_mday) # 获得当前时间时间戳 currentTimeStamp = int(time.time()) # 预售时长的时间戳 deltaTimeStamp = '2505600' # 截至日期时间戳 deadTimeStamp = currentTimeStamp + int(deltaTimeStamp) # 获取预售票的截止日期时间 deadTime = time.localtime(deadTimeStamp) deadDate = '%04d-%02d-%02d' % (deadTime.tm_year, deadTime.tm_mon, deadTime.tm_mday) # print(Colored.red('请注意合理的乘车日期范围是:{} 至 {}'.format(localDate, deadDate))) # 判断输入的乘车时间是否在合理乘车时间范围内 # 将购票日期转换为时间数组 trainTimeStruct = time.strptime(date, "%Y-%m-%d") # 转换为时间戳: trainTimeStamp = int(time.mktime(trainTimeStruct)) # 将购票时间修改为12306可接受格式 ,如用户输入2018-8-7则格式改为2018-08-07 trainTime = time.localtime(trainTimeStamp) trainDate = '%04d-%02d-%02d' % (trainTime.tm_year, trainTime.tm_mon, trainTime.tm_mday) # 比较购票日期时间戳与当前时间戳和预售截止日期时间戳 if currentTimeStamp <= trainTimeStamp and trainTimeStamp <= deadTimeStamp: return True, trainDate else: print(Colored.red('Error:您输入的乘车日期:{}, 当前系统日期:{}, 预售截止日期:{}'.format(trainDate, localDate, deadDate))) return False, None @classmethod def getDate(self,dateStr): # dateStr格式为20180801 year = time.strptime(dateStr,'%Y%m%d').tm_year month = time.strptime(dateStr,'%Y%m%d').tm_mon day = time.strptime(dateStr,'%Y%m%d').tm_mday return '%04d-%02d-%02d' % (year,month,day) # 根据车站名获取电报码 @classmethod def getStationCode(self, station): codesDict = StationCodes().getCodesDict() if station in codesDict.keys(): return codesDict[station] # 输入出发地和目的地 @classmethod def inputStation(self, str): station = input('{}:\n'.format(str)) if not station in StationCodes().getCodesDict().keys(): print(Colored.red('Error:车站列表里无法查询到{}'.format(station))) station = input('{}:\n'.format(str)) return station # 输入乘车日期 @classmethod def inputTrainDate(self): trainDate = input('请输入购票时间,格式为2018-01-01:\n') try: trainTimeStruct = time.strptime(trainDate, "%Y-%m-%d") except: print('时间格式错误,请重新输入') trainDate = input('请输入购票时间,格式为2018-01-01:\n') timeFlag, trainDate = Utility.checkDate(trainDate) if timeFlag == False: trainDate = input('请输入购票时间,格式为2018-01-01:\n') timeFlag, trainDate = Utility.checkDate(trainDate) return trainDate @classmethod def getTrainDate(self,dateStr): # 返回格式 Wed Aug 22 2018 00: 00:00 GMT + 0800 (China Standard Time) # 转换成时间数组 timeArray = time.strptime(dateStr, "%Y%m%d") # 转换成时间戳 timestamp = time.mktime(timeArray) # 转换成localtime timeLocal = time.localtime(timestamp) # 转换成新的时间格式 GMT_FORMAT = '%a %b %d %Y %H:%M:%S GMT+0800 (China Standard Time)' timeStr = time.strftime(GMT_FORMAT, timeLocal) return timeStr
特别要注意一下getTrainDate
方法里返回时间字符串格式,我使用Firefox浏览器抓包时发现格式是Wed+Aug+22+2018+00:00:00+GMT+0800+(China+Standard+Time)
,但是在项目里使用此格式时会发现无法请求到数据。后来使用Google浏览器抓包发后现时间字符串里没有+
符号。
作者:东东隆东抢
链接:https://www.jianshu.com/p/5954d7c52d22