携程在手 说走就走。今天来记录一下爬取携程旅行的教程。
首先告诉大家,爬携程还是相对简单的,当然也有难得方法,这里就以实现为最终目标讲解最简单得方法。
我要采集得是携程门票一栏得景点信息。还是照惯例用chrome抓包。首先需要获取每个景点详情页的url,我这边找到了直接贴图。
原来以为拿到这个页面问题就解决了,可实践发现事情并没有我想的那么简单,这是一个post请求。我把参数都带上进行访问,压根拿不到数据。返回的json文件是这样的。
研究了一下,发现参数里有一个变动的加密参数‘traceid’,很明显每次请求都会生成一个一次性的加密参数,一次之后就过期无法复用,如果要走这个方法只能去做js逆向解密破解它,然而我内心是拒绝的,这不符合我的人物设定。
也给看到文章的朋友讲讲,如果你不是专注只做爬虫的话,其实没必要去研究js解密,因为首先你得会js。爬虫这个方向说实话入门的下限很低,但是上限非常高。如果你一心只搞爬虫,那可以花大把时间去研究,像我这种只拿爬虫作为工具的就不想花太多时间在这上面了。所以这里我改用selenium去获取数据,简单实用太香了。
selenium功能还是挺强大的,使用这个方法就可以拿到class值为’right-content-list’标签下的所有内容。然后就是正则匹配出每个景点详情页的url以及标题。拿到详情页的标题之后就可以requests请求详情页数据了。详情页的数据可以直接拿到比较简单。
然后在这里解释一下为什么要用while True死循环,因为携程在换页的时候它的url是不会变化的,所以这里只能用selenium的自动点击功能,让它在采完第一页数据之后自动跳转下一页进行采集,设置好你要爬取的页码,也就是我代码里的self.page的值,达到爬取的页数就跳出循环即可。
到了爬取详情页这一步,就看你想采什么样的数据去写规则了。在这里我只提一点,不难但是麻烦,大家在往下看之前也可以先思考一下。看下图。
图中的内容,有景点介绍 开放时间 优待政策几栏,但不是每个景点都是这几个,有的是只有一个,有的还有一个服务设备栏,而且看它的前端代码,属性标签啥的都是一模一样的,遇到这种情况如果是你会怎么去获取,才能保证不会少拿而且又能正确对应上数据?
思考令人进步。。。
下面给出我的答案!
我这里是把这个栏目的标题以及内容分别使用xpath提取形成两个列表,因为不管到底有几个内容,它的标题跟内容都肯定是一一对应上的,而列表是有序的。列表出来之后就是使用dict(zip(desc_title_list, desc_list)),先转json再转字典,标题就是key值,内容就是value值。如果要单独展示就遍历字典就可以了。这一步我代码里没写,大家自己去写吧。
要注意的点大致就这些,下面贴我的完整代码!!!
import pandas
import re
import time
import requests
import urllib3
from lxml import etree
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.options import Options
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class Jy_jd(object):
def __init__(self):
options = Options()
options.add_argument('--headless')
self.chrome = Chrome(options=options)
self.chrome.get(
'https://huodong.ctrip.com/things-to-do/list?pagetype=city&citytype=dt&keyword=%E6%8F%AD%E9%98%B3&pshowcode=Ticket2')
time.sleep(3)
self.page = 1
self.headers = {
'cookie': 'Session=SmartLinkCode=U155952&SmartLinkKeyWord=&SmartLinkQuary=&SmartLinkHost=&SmartLinkLanguage=zh; _RSG=KqK3qETfa143fOqQl4rFXB; _RDG=282f24100640c82731283334fcc3364464; _RGUID=4064a5d3-b40d-4d14-b84f-d44bdad18a43; Union=OUID=index&AllianceID=4897&SID=155952&SourceID=&createtime=1600831032&Expires=1601435831593; MKT_OrderClick=ASID=4897155952&AID=4897&CSID=155952&OUID=index&CT=1600831031597&CURL=https%3A%2F%2Fwww.ctrip.com%2F%3Fsid%3D155952%26allianceid%3D4897%26ouid%3Dindex&VAL={"pc_vid":"1600831028743.427gzc"}; MKT_CKID=1600831031634.5olt5.f6pj; MKT_CKID_LMT=1600831031635; _ga=GA1.2.248639397.1600831032; _gid=GA1.2.1954297618.1600831032; MKT_Pagesource=PC; GUID=09031031210931119554; nfes_isSupportWebP=1; appFloatCnt=1; nfes_isSupportWebP=1; ASP.NET_SessionSvc=MTAuNjAuMzUuMTQ2fDkwOTB8amlucWlhb3xkZWZhdWx0fDE1ODkwMDMyMjQ5NDI; U_TICKET_SELECTED_DISTRICT_CITY=%7B%22value%22%3A%7B%22districtid%22%3A%22835%22%2C%22districtname%22%3A%22%E6%8F%AD%E9%98%B3%22%2C%22isOversea%22%3Anull%7D%2C%22createTime%22%3A1600847243848%2C%22updateDate%22%3A1600847243848%7D; _RF1=113.118.204.141; _gat=1; _pd=%7B%22r%22%3A1%2C%22d%22%3A614%2C%22_d%22%3A613%2C%22p%22%3A634%2C%22_p%22%3A20%2C%22o%22%3A655%2C%22_o%22%3A21%2C%22s%22%3A668%2C%22_s%22%3A13%7D; _bfa=1.1600831028743.427gzc.1.1600843833503.1600847244099.5.49.10650038368; _bfs=1.30; _bfi=p1%3D290510%26p2%3D290510%26v1%3D49%26v2%3D48; _jzqco=%7C%7C%7C%7C1600831031803%7C1.1555887407.1600831031625.1600849509140.1600849530503.1600849509140.1600849530503.0.0.0.19.19; __zpspc=9.4.1600846262.1600849530.14%232%7Cwww.baidu.com%7C%7C%7C%25E6%2590%25BA%25E7%25A8%258B%7C%23',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
}
def get_url(self):
while True:
content = self.chrome.find_element_by_class_name('right-content-list').get_attribute('innerHTML')
cons = re.findall(r'href="(.*?)" title="(.*?)"', content)
for con in cons:
self.detail_url = 'https:' + con[0]
self.title = con[1]
print(self.detail_url, self.title)
self.get_detail()
self.chrome.find_element_by_class_name('u_icon_enArrowforward').click()
time.sleep(1)
self.page += 1
if self.page == 120:
break
def get_detail(self):
detail_con = requests.get(self.detail_url, verify=False, headers=self.headers).text
# time.sleep(2)
'''使用正则获取信息'''
self.rank = ''.join(re.findall(r'rankText">(.*?)<', detail_con, re.DOTALL))
self.address = ''.join(re.findall(r'景点地址</p><p class="baseInfoText">(.*?)<', detail_con, re.DOTALL))
self.mobile = ''.join(re.findall(r'官方电话</p><p class="baseInfoText">(.*?)<', detail_con, re.DOTALL))
print(self.rank, self.address, self.mobile)
'''使用xpath获取信息'''
ret = etree.HTML(detail_con)
desc_cons = ret.xpath('//div[@class="detailModule normalModule"]//div[@class="moduleContent"]')
desc_titles = ret.xpath('//div[@class="detailModule normalModule"]//div[@class="moduleTitle"]')
desc_list = []
desc_title_list = []
for d in desc_cons:
des = ''.join(d.xpath('.//text()'))
desc_list.append(des)
for d in desc_titles:
des = ''.join(d.xpath('.//text()'))
desc_title_list.append(des)
desc_dict = dict(zip(desc_title_list, desc_list))
print(desc_dict)
'''获取图片链接'''
img_list = []
imgs = re.findall(r'background-image:url\((.*?)\)', detail_con, re.DOTALL)
for img in imgs:
'''匹配到的同一张图片会有两种尺寸,我们只要大图,所以把尺寸为521*391的匹配出来即可'''
image = re.search(r'521_391', img)
if image:
img_list.append(img)
print(img_list)
self.get_ticket()
def get_ticket(self):
id = self.detail_url.split('/')[-1]
print(id)
ticket_url = f'https://piao.ctrip.com/ticket/dest/{id}?onlyContent=true&onlyShelf=true'
ticket_res = requests.get(ticket_url, verify=False, headers=self.headers).text
# time.sleep(1)
ticket_ret = etree.HTML(ticket_res)
ticket = ticket_ret.xpath('//table[@class="ticket-table"]//div[@class="ttd-fs-18"]/text()')
price = ticket_ret.xpath(
'//table[@class="ticket-table"]//td[@class="td-price"]//strong[@class="ttd-fs-24"]/text()')
print(ticket)
print(price)
'''拿到的列表里可能存在不确定数量的空值,所以这里用while True把空值全部删除,这样才可以确保门票种类与价格正确对应上'''
while True:
try:
ticket.remove(' ')
except:
break
while True:
try:
price.remove(' ')
except:
break
'''
这里多一个if判断是因为我发现有些详情页即便拿到门票信息并剔除掉空值之后仍然存在无法对应的问题,原因是网页规则有变动,
所以一旦出现这种情况需要使用新的匹配规则,否则会数据会出错(不会报错,但信息对应会错误)
'''
if len(ticket) != len(price):
ticket = ticket_ret.xpath(
'//table[@class="ticket-table"]/tbody[@class="tkt-bg-gray"]//a[@class="ticket-title "]/text()')
price = ticket_ret.xpath('//table[@class="ticket-table"]//strong[@class="ttd-fs-24"]/text()')
while True:
try:
ticket.remove(' ')
except:
break
while True:
try:
price.remove(' ')
except:
break
print(ticket)
print(price)
ticket_dict = dict(zip(ticket, price))
print(ticket_dict)
if __name__ == '__main__':
jy_jd = Jy_jd()
jy_jd.get_url()