首先12306下单肯定需要用户登录
第一步12306用户登录:12306可以用账号密码登录或者二维码,通过试验账号密码登录过程会出现验证码,比较麻烦,所有选择二维码的方式登录
通过页面实际操作发现二维码登录先通过接口获取二维码图片和uuid,根据uuid通过轮询二维码登录结果
获取二维码接口:https://kyfw.12306.cn/passport/web/create-qr64
查询二维码状态接口:https://kyfw.12306.cn/passport/web/checkqr
登录完后,发现cookie下新增了一下参数,我们跟进去看查询二维码状态接口后做了什么操作
这里我们发现当登录成功后,返回status为2,最终调用popup_loginCallBack()方法
最终通过调用https://kyfw.12306.cn/passport/web/auth/uamtk获取票据,然后根据票据换取全局cookie的tk
下面的具体的代码
import requests
from responseData import getSuccessResultData,getErrorResultData
import logging
from cache.gobalCache import setCache,getCache
from Task.checkLogin import addcheckLoginJob
logger = logging.getLogger(__name__)
def getBaseHearders():
headers = {
"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Referer":"https://kyfw.12306.cn/otn"
}
return headers
def getLoginQrCode(requestId :str):
if(len(requestId)==0):
return getErrorResultData("无效请求")
headers=getBaseHearders()
headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
res = requests.post("https://kyfw.12306.cn/passport/web/create-qr64",data="appid=otn",headers=headers)
if(res.status_code==200 and 'uuid' in res.text):
logger.info("获取二维码成功:%s",res.text)
setCache(requestId,res.json())
return getSuccessResultData(res.json(),"成功")
return getErrorResultData("获取12306登录二维码失败")
def checkqrAndGetTK(body):
uuid = body['uuid']
appid = body['appid']
requestId = body['requestId']
cache = getCache(requestId)
if(len(requestId)==0 or cache is None):
return getErrorResultData("无效请求requestId")
if(cache and 'tk' in cache and 'cookies' in cache):
return getSuccessResultData(cache,"cookie缓存")
checkqrUrl = 'https://kyfw.12306.cn/passport/web/checkqr'
headers = getBaseHearders()
headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
data = "uuid="+uuid+"&appid="+appid
res = requests.post(url=checkqrUrl,data=data,headers=headers)
result = dict()
msg = '查询二维码登录状态失败'
isError = True
if(res.status_code==200 and 'result_code' in res.text):
isError = False
logger.info("查询二维码登录状态成功:%s",res.text)
result_code = res.json()['result_code']
result['status'] = result_code
if(result_code == "2"):
logger.info("二维码登录成功")
uamtk = res.json()['uamtk']
headers['Cookie'] = 'uamtk='+uamtk
res = requests.post("https://kyfw.12306.cn/passport/web/auth/uamtk",data="appid="+appid,headers=headers)
msg = '二维码登录成功获取tk失败'
if(res.status_code==200 and 'newapptk' in res.text and res.json()['result_code']== 0):
newapptk = res.json()['newapptk']
res = requests.post("https://kyfw.12306.cn/otn/uamauthclient",data="tk="+newapptk,headers=headers)
if(res.status_code==200 and 'username' in res.text and res.json()['result_code'] == 0):
cookies = requests.utils.dict_from_cookiejar(res.cookies)
if('tk' in cookies):
result['tk'] = cookies['tk']
msg = '二维码登录成功获取tk成功'
cookies_str = '; '.join([f'{k}={v}' for k, v in cookies.items()])
result['cookies'] = cookies
result['cookiesStr'] = cookies_str
else:
msg = res.json()['result_message']
setCache(requestId,result)
if('tk' in result):
addcheckLoginJob(requestId)
if(isError):
return getErrorResultData(msg)
else:
return getSuccessResultData(result,msg)
登录成功缓存一下用户的cookie到我们内存,同时需要定时校验这个cookie是否有效的
from apscheduler.schedulers.background import BackgroundScheduler
import time
import requests
from cache.gobalCache import getCache,delCache,setCache
import logging
logger = logging.getLogger(__name__)
scheduler = BackgroundScheduler()
scheduler.start()
def checkLoginJob(requestId :str):
resCache = getCache(requestId)
if(resCache is None or 'tk' not in resCache):
logger.info("缓存中已经没有tk,移除任务,%s",requestId)
scheduler.remove_job(requestId)
return
url = 'https://kyfw.12306.cn/otn/index/initMy12306Api'
headers = {
"Cookie": resCache['cookiesStr'],
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
}
res = requests.post(url,headers=headers)
logger.info("定时刷新登录cookie结果:%s,%s",res.text,requestId)
if(res.status_code == 200 and 'data' in res.text):
cookiesDict = requests.utils.dict_from_cookiejar(res.cookies)
cacheCookies :dict = resCache['cookies']
cacheCookies.update(cookiesDict)
cookies_str = '; '.join([f'{k}={v}' for k, v in cacheCookies.items()])
resCache ['cookiesStr'] = cookies_str
resCache ['cookies'] = cacheCookies
setCache(requestId,resCache)
logger.info("定时刷新登录cookie成功")
else:
delCache(requestId)
logger.info("cookie已失效,%s",requestId)
scheduler.remove_job(requestId)
def addcheckLoginJob(requestId :str):
checkLoginJob(requestId)
scheduler.add_job(checkLoginJob, 'interval', seconds=30, id=requestId, args=[requestId])
本次文章的重点,下单购票,可改造定时抢票
废话不多说,先人工走一遍流程,打开查询高铁的页面发现查询接口
https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2024-05-16&leftTicketDTO.from_station=GZQ&leftTicketDTO.to_station=SHH&purpose_codes=ADULT
明显可以见参数就是时间和出发城市code和到达城市code,这个接口实际不用登录就可以查询,就是要获取一个cookie里面的参数
见代码:
def getCoookie(train_date :str,from_station :str,to_station :str,from_station_code :str,to_station_code :str):
url = 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc&fs={},{}&ts={},{}&date={}&flag=N,N,Y'
headers = {
'User-Agent':"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
}
res = requests.get(url= url.format(from_station,from_station_code,to_station,to_station_code,train_date),headers=headers)
cookies = requests.utils.dict_from_cookiejar(res.cookies)
JSESSIONID = None
CLeftTicketUrl = None
if('JSESSIONID' in cookies):
JSESSIONID = cookies['JSESSIONID']
soup = BeautifulSoup(res.text, 'html.parser')
scripts = soup.find_all('script')
for script in scripts:
if 'CLeftTicketUrl' in script.text:
pattern = re.compile(r'CLeftTicketUrl = \'(.*?)\'')
globalCLeftTicketUrl = re.search(pattern, script.text)
if globalCLeftTicketUrl:
CLeftTicketUrl = globalCLeftTicketUrl.group(1)
return JSESSIONID,CLeftTicketUrl
def getNoTransferTrain(train_date :str,from_station :str,from_station_code :str,to_station :str,to_station_code :str,trainList :list):
url = 'https://kyfw.12306.cn/otn/{}?leftTicketDTO.train_date={}&leftTicketDTO.from_station={}&leftTicketDTO.to_station={}&purpose_codes=ADULT'
baseUrl = 'leftTicket/query'
JSESSIONID,CLeftTicketUrl = getCoookie(train_date,from_station,to_station,from_station_code,to_station_code)
if(JSESSIONID is None):
logger.error("火车查询失败,获取cookie异常")
return
headers = {
'Cookie':"JSESSIONID="+JSESSIONID,
'User-Agent':'PostmanRuntime/7.37.0',
'Accept-Language': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
}
if(CLeftTicketUrl):
url=url.format(CLeftTicketUrl,train_date,from_station_code,to_station_code)
else:
url=url.format(baseUrl,train_date,from_station_code,to_station_code)
#url=url.format(from_station,from_station_code,to_station,to_station_code,train_date)
#res :ResultData = page.listenDateByUrl(url,['/leftTicket/query'],['result'])
res = requests.get(url=url,headers=headers)
if(len(res.json()['data'])<=0):
logger.info("火车查询失败")
else:
data = res.json()
for infoStr in data['data']['result']:
if('暂停发售' not in infoStr):
trainTicketDetailInfo = parseTrain(infoStr,train_date)
hours, minutes = calculate_time_difference_in_hours_minutes(trainTicketDetailInfo['departure_time'], trainTicketDetailInfo['arrival_time'])
trainTicketMainInfo = parseTrainNode([trainTicketDetailInfo],"{}小时{}分钟.".format(hours,minutes),False)
trainTicketInfo = TrainTicketInfo(trainTicketMainInfo,trainTicketDetailInfo)
trainList.append(trainTicketInfo.to_dict())
parseTrainNode方法是解析这个接口返回的数据对应到页面显示高铁上的数据
通过上面的高铁数据我们进行下单
1:先获取购票人的信息,通过接口:https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs,需要登录就是上面cookie
def getPassengersInfo(requestId :str):
headers = getCookieHeaders(requestId)
if(headers is None):
return None
url = 'https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs'
res = requests.post(url=url,headers=headers)
if('validateMessagesShowId' in res.text and res.json()['httpstatus']==200):
logging.info("获取乘车人信息成功")
return res.json()['data']['normal_passengers']
logging.info("获取乘车人信息失败")
return None
2:进入购票页面获取到提交参数的SubmitToken,同时提交订单时候有一个加密参数,通过分析页面的js代码,发现在页面加载时候就触发的方法挂载了全局的window.json_ua.toString(),我们使用浏览迷模拟这个js执行结果获取加密参数encryptedData,
然后下单的流程就是,
验证购票人信息是否正确:https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo,
验证票是否足够:https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount
最后提交订单:https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue
最后可以提交订单,然后去app去支付就行了
encryptedData的获取方式,页面里面有个js的加密文件,getSubmitToken这个方法里面有写