用Python做一个简单的机票价格爬取(一) 知识

MagicQ 2019-6-25 1464

0x00. 前言

最近在某程上查机票价格的时候,发现往返机票总是出现刚查看低价信息就被提示机票已售空,再次搜索的时候在也不会出现同一个低价票,顺道就查了查啥情况下才能出现那个低价。

0x01. 哭泣

在经过手动查了几程的结果后发现,太累了,每次查一个就会消耗3分钟左右的时间,这个3分钟不是说我动作慢,而是系统自动检索更多低价票信息,需要等、需要等、需要等。。。

想了想既然是这样,那我用脚本爬取吧,遂对访问请求进行了观察,总结一下

0x02 请求过程

在对访问流分析后得到如下几个有效信息:

1、机票查询信息实际是由请求  https://flights.ctrip.com/international/search/api/search/batchSearch?v=0.8318506636091845  来完成。

请求方式:POST

请求参数:那个v没啥子用,只是为了确保请求资源不被cache用的,值是一个随机数

需要携带的头,注意sign头是某程用来防止爬虫的必须携带:

transactionid  ->  随机字符串的MD5值
sign  ->  md5( transactionid + 要查询的航班信息 ) ; 要查询的航班信息组成方式看源码 e.get("flightSegments").valueSeq().forEach(function(e){var n=e.get("departureCityCode"),r=e.get("arrivalCityCode"),i=e.get("departureDate");t+=n+r+i} ; 简写就是 遍历每段行程依然 用 出发城市代码+到达城市代码+出发日期...
content-type   ->   application/json     ;固定的,只是用来说明POST携带的content是啥玩呀

携带的内容:

{
    "flightWay": "S",   //单程S,返程D,多程M
    "flightWayEnum": "OW",   //与FlightWay值对应,对应关系:S -> OW;  D ->  RT ;  M ->  MT
    "infantCount": 0,   // 婴儿数量
    "childCount": 0,  //儿童数量
    "Count": 1,  //成人数量
    "segmentNo": 1,       //座位数
    "cabin": "Y_S",   //经济舱&超级经济舱,如需查询头等/商务舱,请使用C_F
    "cabinEnum": "Y_S",  //与cabin值保持一致
    "departureCityId": 7, //出发城市代码
    "departProvinceId": 10
    "departCountryName": "中国",
    "arrivalProvinceId": 11088,           //到达省份代码,可通过
    "arrivalCountryName": "日本",     //到达省国家名字
    "arrivalCityId": 219,             //到达城市ID
    "directFlight": false,    //如只查询直飞航班,值为true
    "isMultiplePassengerType": 0,  //如既有成人又有儿童/婴儿 值为1, 结果里会包含多个price (|child|infant)(Price|Tax)
    "flightSegments": [
        {
            //出发信息
            "departureDate": "2019-07-09",      //出发日期
            "departureCityName": "青岛",          //出发城市
            "departureCityCode": "TAO",         //出发城市代码
            "departureCountryName": "中国",       //出发国家
            "departureCityTimeZone": 480,     //出发城市时区,携程的时区是按照分钟算的,作为中国人,就用480吧,哈哈哈
            "departureCityId": 7,             //出发城市ID
            "departureCountryId": 1,          //出发国家ID
            "departureProvinceId": 10,            //出发省份ID
            //到达信息,与出发信息几乎没差
            "arrivalProvinceId": 11088,
            "arrivalCountryName": "日本",
            "arrivalCityName": "大阪",
            "arrivalCityCode": "OSA",
            "arrivalCountryId": 78,
            "arrivalAirportName": "关西国际机场",
            "arrivalCityTimeZone": 540,
            "arrivalCityId": 219,
            "arrivalAirportCode": "KIX",
            "timeZone": 480
        }
    ],
    "extensionAttributes": {},
    "transactionID": "bba14663455245c38889f6cf5fcd2d85",
}

返回结果为JSON数据,再次点赞一下,用Chrome浏览器阅读极具可读性哈哈哈。大概的数据结构如下图


先忽略返回结果的status,msg字段(几乎没见过status为1的情况,所以先不管它了),data才是我们需要的,data的结构是这样子的,包含4个对象,bestChoiceFlightsForceTop这个值的意思如其名,context是这个任务的状态(既然是batch查询呀,那肯定不是一次性能返回的),couponAdditionList优惠信息不管它,flightItineraryList这才是我们要的东西,里面包含了各种航班价格信息。

PS:context没卵用吗?不,还真有作用,前面讲了既然是batch任务那就基本上会有start、process、complete三个状态,至于start,只要batch任务返回结果了那就说明start了且已经在process状态了,而complete就需要看context的finished值。

2、访问第一个链接后系统会返回searchId,这时如果finished为false就可以通过 https://flights.ctrip.com/international/search/api/search/pull/1025659-1561437740213-1028678HH-1?v=0.468365014675614 ,但是这个请求比较坑,竟然是同样需要POST请求且携带的BODY跟第一个请求的body需要一致,返回内容结果与第一个请求的结果一致,这里不赘述。有一点要提醒下,所有可用航班信息是batchsearch+所有pull的总和,而非某一个单独请求,每次pull都会消耗掉当前已经查询好的,所以要循环多pull几次。

请求方式:POST

请求头:与第一个请求的一致

请求体:与第一个请求的一致

PS:不要跟我讲为啥不是 https://flights.ctrip.com/international/search/api/flight/comfort/batchGetComfortTagList?v=0.557773117127768,穷就不看舒适的了,看看大环境下的吧。。。

0x03. 对于请求结果的思考

1、第一次见防爬虫算法是写到js里的,且没有加密的,那个加密算法很好找好吗?只要有兴趣去爬价格的,那样的算法。。。

2、transaction的值竟然是随机。。。而且并没有什么其他限制,我只要造一个ID后续的同一个航班多次爬取价格岂不是毫无影响的进行?爬虫。。。

3、每个价格信息我觉得可以直接塞redis里,后台定期更新或者根据用户的请求来放缓存,可能是我查的路线太过偏僻或者其他原因所以我没看到有走缓存的效果?

4、顺道打个广告,如果有买机票订酒店需求可以私聊我。。。携程上的价格打9折。。。

0x04. 正文

废话了一大堆,该开始搞脚本了,因历史原因我的所有代码都是在python 2.7的版本下完成的,需要的库有request、json两个库就好

环境:
Python 2.7.15 (default, Oct  2 2018, 11:42:04) 
[GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.2)] on darwin

下面是代码示例,可以将爬取间隔即过程放在主程序段,格式化输出结果在formatFlightInfo内完成,flightinfo是数组,可遍历每个object,每个object的数据如下:

{
                                "flightNo": "OZ112",
                                "sequenceNo": 2,
                                "marketAirlineCode": "OZ",
                                "marketAirlineName": "韩亚航空",
                                "marketAlliance": "星空联盟",
                                "departureCountryName": "韩国",
                                "departureProvinceId": 0,
                                "departureCityId": 274,
                                "departureCityCode": "SEL",
                                "departureCityName": "首尔",
                                "departureAirportCode": "ICN",
                                "departureAirportName": "仁川国际机场",
                                "departureTerminal": "T1",
                                "arrivalCountryName": "日本",
                                "arrivalProvinceId": 11088,
                                "arrivalCityId": 219,
                                "arrivalCityCode": "OSA",
                                "arrivalCityName": "大阪",
                                "arrivalAirportCode": "KIX",
                                "arrivalAirportName": "关西国际机场",
                                "arrivalTerminal": "T1",
                                "duration": 105,
                                "transferDuration": 0,
                                "aircraftCode": "359",
                                "aircraftName": "空客350(大)",
                                "departureDateTime": "2019-07-10 07:55:00",
                                "arrivalDateTime": "2019-07-10 09:40:00"
                            }
# -*- coding: utf-8 -*-
import sys,json,requests
import random
import hashlib
import time
fail_retry=3
class logs:
    color={
        'INFO':'\033[92m',
        'ERROR':'\033[91m',
        'DEBUG':'\033[1m',
        'END':'\033[0m'
    }
    def __init__(self,msg,level='info'):
        if level == 1 :
            level = 'ERROR'
        elif level == 2:
            level = 'DEBUG'
        else :
            level='INFO'
        level=level.upper()
        d=time.strftime('%F %H:%M:%S')
        print d,self.color[level],msg,self.color['END']
def md5(a):
    m=hashlib.md5()
    m.update(str(a))
    return m.hexdigest()
#FlightData use origin struct
def search(flightData):
    if not (type(flightData) == object or flightData.get('flightSegments',None) ):
        return False
    #create transactionID
    # md5=hashlib.md5()
    # md5.update(str(random.random()))
    transactionID=md5(random.random()) #md5.hexdigest()
    sign=''
    #create sign
    for flight in flightData['flightSegments']:
        sign+=str(flight['departureCityCode'])
        sign+=str(flight['arrivalCityCode'])
        sign+=str(flight['departureDate'])
    print transactionID+sign
    # md5.update(transactionID+sign)
    # sign=md5.hexdigest()
    api_client=api()
    api_client.headers.setdefault('sign',md5(transactionID + sign))
    api_client.headers.setdefault('transactionid',transactionID)
    api_client.payload=flightData
    searchResult=api_client.request('https://flights.ctrip.com/international/search/api/search/batchSearch')
    tempSearchID=None
    while searchResult:
        tempSearchID=tempSearchID if tempSearchID else searchResult['data']['context']['searchId'] 
        if searchResult['data'].get('flightItineraryList',None):
            formatFlightInfo(searchResult['data']['flightItineraryList'])
        if searchResult['data']['context']['finished']:
            break
        else:
            searchResult=api_client.request('https://flights.ctrip.com/international/search/api/search/pull/'+tempSearchID)
        time.sleep(5)
class api:
    payload=None
    params={
        'v': random.random()
    }
    url=''
    headers = {
        'Content-Type':'application/json',
        'X-Application-For':'Ctrip-interFlight-Price-Crawler-Python',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
        }
    def request(self,url=None):
        retry_count = 0
        self.url=url if url else self.url
        while retry_count < fail_retry :
            try:
                # print self.params
                print self.payload
                r = requests.post(self.url , params = self.params,headers=self.headers,json=self.payload,timeout=60)
                if r.status_code == 200 and r.text:
                    data=r.json()
                    if(data['status']==0):
                        return data
                    else:
                        logs( self.url + ' Error occurred: '+str(data['msg']) + ' ' +str(self.params) + str(self.headers) , 2 )
                logs( self.url + ' Error occurred with status code: '+str(r.status_code) + ' ' +str(self.params) + str(self.headers) , 2 )
                return False
            except Exception as e:
                retry_count +=1
                logs(str(e) + ' Retrying... ',retry_count)
        return False
def formatFlightInfo(d):
    print json.dumps(d)
if __name__ == '__main__':
    print "Start search flights info from Ctrip...\n"
    flightSearch={
    "flightWay": "M",   
    "flightWayEnum": "MT",   
    "infantCount": 0,   
    "childCount": 0,  
    "Count": 1,  
    "segmentNo": 1,       
    "cabin": "Y_S",   
    "cabinEnum": "Y_S",  
    "directFlight": False,    
    "isMultiplePassengerType": 0,  
    "extensionAttributes": {}
    }
    flightSearch['flightSegments']=[
        {
            "departureDate": "2019-07-09",
            "departureCityName": "青岛",
            "departureCityCode": "TAO",         
            "departureCountryName": "中国",       
            "departureCityTimeZone": 480,     
            "departureCityId": 7,             
            "departureCountryId": 1,          
            "departureProvinceId": 10,            
            
            "arrivalProvinceId": 11088,
            "arrivalCountryName": "日本",
            "arrivalCityName": "大阪",
            "arrivalCityCode": "OSA",
            "arrivalCountryId": 78,
            "arrivalAirportName": "关西国际机场",
            "arrivalCityTimeZone": 540,
            "arrivalCityId": 219,
            "arrivalAirportCode": "KIX",
            "timeZone": 480
        },
        {
            
            "departureDate": "2019-07-13",      
            "departureCityName": "青岛",          
            "departureCityCode": "TAO",         
            "departureCountryName": "中国",       
            "departureCityTimeZone": 480,     
            "departureCityId": 7,             
            "departureCountryId": 1,          
            "departureProvinceId": 10,            
            
            "arrivalProvinceId": 11088,
            "arrivalCountryName": "日本",
            "arrivalCityName": "大阪",
            "arrivalCityCode": "OSA",
            "arrivalCountryId": 78,
            "arrivalAirportName": "关西国际机场",
            "arrivalCityTimeZone": 540,
            "arrivalCityId": 219,
            "arrivalAirportCode": "KIX",
            "timeZone": 480
        }
    ]
    search(flightSearch)
最后于 2019-6-28 被luyaops编辑 ,原因:
最新回复 (3)
  • MagicQ 2019-6-25
    0 引用 2
    文中<*a*d*ult*>请自行删掉<>和*。。。
  • adrootrr 2019-6-25
    0 引用 3
    我是爬虫入门小学生
  • DingGuodong 2019-6-26
    0 引用 4
    这个还真不是一般的费劲。
    • 运维开源项目互助社区—致敬开源
      5
        立即登录 立即注册 
返回