写在前面
两周前完成了Python 12306验证码自动验证、用户登录和查询余票一文,后来总觉得写得有点凌乱,于是想进行重构,让整个项目结构看起来更加清晰明了。
项目结构
写完整个项目后觉得其实也很简单,无非是使用Session进行多次Get和Post请求,难点在于Post请求时使用的Data从何而来?我们先使用抓包工具(浏览器F12)完成一次12306平台订票之完整过程,对需要进行哪些网络请求心里有个大概印象。使用Session的主要原因是为了避免每次请求数据时都去考虑Cookies,如此可能会方便很多。
我们将整个订票过程中使用到的API 放在一个文件里,原因很简单:一旦某个接口地址改变了,我们只需在此文件里进行修改,无法在代码里到处查找修改,省时省力。我自己之前在写iOS 应用时候也是采用这样的方式。
12306 APIclass 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