xChar
·a year ago

携程热门景区评论爬取

前言

由于最近参加了一个比赛,需要爬取云贵川几个省会的所有城市的热门景区的评论以及景点的信息,看了下网上的项目,基本上都试了试,操作太麻烦,有的还需要一个个的去找参数,都不太满足自己的需求,于是还是自己写一个吧,首先先来看一下效果吧。

爬取的数据以excel保存

image

正在爬取中

image

通过一会的爬取,我也是成功的爬取了云贵川三省所有城市的热门景区,加起来有28万的数据不容易啊😭😭😭

下面就分享一下这次爬取的过程🚀🚀🚀

注意:本次分享的所有代码都不是完整代码,完整代码见aglorice/CtripSpider: 携程评论爬虫,使用线程池来爬取热门景区评论,简单易用。一键爬取任意省的所有热门景区。 (github.com)

1. 分析页面

首先先进入到携程攻略.景点页面,将光标移动到国内(含港澳台)就可以得到基本上所有省份的所有城市,这里就是我们城市数据的来源。

image

image

打开控制台,很快就定位到了,如下。

image

有了这个直接上代码,这里我使用的是BeautifulSoup来解析页面。

    def get_areas(self) -> list:
        city_list = []
        try:
            res = self.sees.get(
                url=GET_HOME,
                headers={
                    "User-Agent": get_fake_user_agent("pc")
                },
                proxies=my_get_proxy(),
                timeout=TIME_OUT
            )
        except Exception as e:
            self.console.print(f"[red]获取城市景区信息失败,{e},你可以检查你的网路或者代理。", style="bold red")
            exit()
        res_shop = BeautifulSoup(res.text, "lxml")
        areas = res_shop.find_all("div", attrs={"class": "city-selector-tab-main-city"})

        for area in areas:
            area_title = area.find("div", attrs={"class": "city-selector-tab-main-city-title"}).string
            if area_title is None:
                continue
            area_items = area.find_all("div", attrs={"class": "city-selector-tab-main-city-list"})
            area_items_list = [{"name": item.string, "url": item["href"]} for item in area_items[0].find_all("a")]
            city_list.append({
                "name": area_title,
                "city": area_items_list
            })
        return city_list

通过这样的方式将指定的省份的所有的城市名称和url全部保存为city.json,为什么要先保存起来,主要是为了方便自定义,你可以根据你的需求随意的增加你想要爬取的城市或者删减。爬取的结果如下:

image

紧接着我们打开这些景区的url,如下,我们可以看到首页的就是热门景区或者景点,如下:

image

前置的工作已经完了,我们现在就来爬取对应景区的评论吧。

2.景区评论爬取

image

随便打开一个景区的评论,打开控制台查看请求。如下:

image

首先我们分析一下参数,经过多次尝试,就可以知道那些是动态的。首先是_fxpcqlniredt,检查一下cookie就可以很快找到。

image

其次就是x-traceID,通过对js的逆向,我直接找到了相关的代码。如下:

image-20230728192940229

知道了他是怎么生成的,那就简单了,直接上代码。

    def generate_scene_comments_params(self) -> dict:
        """
        生成请求景区评论的 params参数
        :return:
        """
        random_number = random.randint(100000, 999999)
        return {
            "_fxpcqlniredt": self.sees.cookies.get("GUID"),
            "x-traceID": self.sees.cookies.get("GUID") + "-" + str(int(time.time() * 1000000)) + "-" + str(
                random_number)
        }

其实到这里也差不多了,现在只需要解决poild问题,其实这个在每个页面上都有,在每个景区的页面下的script标签里都有这个参数。但是这样就多了一次请求太浪费时间了,如果能直接请求数据数据就好了,而不需要进入景点页面。那么我们就来转变一下思路。我们进入到携程的h5页面。发现它的获取评论接口和pc端的不一样,如下:

image

在手机端上,就不需要使用poild这个参数了,其实到这里就已经结束了,剩下的就是解决再爬取得过程中各种各样问题,最重要得就是携程得反爬,由于我为了更快使用了线程池,速度很快,为了解决这个问题,我使用了随机ua,以及代理池,在加上各种容错机制,使得爬取能够稳定运行。如下就是接口频繁访问得结果:

image

3.携程反爬的解决办法

解决反爬得第一个就是随机得ua,在之前我使用得是fake-useragent,但是由于后面我使用了h5我的接口,于是ua必须是移动端的,但是这个库不支持,于是就自己手动完成了一个,简单但实用。

# -*- coding = utf-8 -*-
# @Time :2023/7/13 21:32
# @Author :小岳
# @Email  :[email protected]
# @PROJECT_NAME :scenic_spots_comment
# @File :  fake_user_agent.py
from fake_useragent import UserAgent
import random
from config import IS_FAKE_USER_AGENT


def get_fake_user_agent(ua: str, default=True) -> str:
    match ua:
        case "mobile":
            if IS_FAKE_USER_AGENT and default:
                ua = get_mobile_user_agent()
                return ua
            else:
                return "Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36 Edg/114.0.0.0"
        case "pc":
            if IS_FAKE_USER_AGENT and default:
                ua = UserAgent()
                return ua.random
            else:
                return "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Mobile Safari/537.36 Edg/103.0.1264.49"


def get_mobile_user_agent() -> str:
    platforms = [
        'iPhone; CPU iPhone OS 14_6 like Mac OS X',
        'Linux; Android 11.0.0; Pixel 5 Build/RD1A.201105.003',
        'Linux; Android 8.0.0; Pixel 5 Build/RD1A.201105.003',
        'iPad; CPU OS 14_6 like Mac OS X',
        'iPad; CPU OS 15_6 like Mac OS X',
        'Linux; U; Android 9; en-us; SM-G960U Build/PPR1.180610.011',  # Samsung Galaxy S9
        'Linux; U; Android 10; en-us; SM-G975U Build/QP1A.190711.020',  # Samsung Galaxy S10
        'Linux; U; Android 11; en-us; SM-G998U Build/RP1A.200720.012',  # Samsung Galaxy S21 Ultra
        'Linux; U; Android 9; en-us; Mi A3 Build/PKQ1.180904.001',  # Xiaomi Mi A3
        'Linux; U; Android 10; en-us; Mi 10T Pro Build/QKQ1.200419.002',  # Xiaomi Mi 10T Pro
        'Linux; U; Android 11; en-us; LG-MG870 Build/RQ1A.210205.004',  # LG Velvet
        'Linux; U; Android 11; en-us; ASUS_I003D Build/RKQ1.200826.002',  # Asus ROG Phone 3
        'Linux; U; Android 10; en-us; CLT-L29 Build/10.0.1.161',  # Huawei P30 Pro
    ]

    browsers = [
        'Chrome',
        'Firefox',
        'Safari',
        'Opera',
        'Edge',
        'UCBrowser',
        'SamsungBrowser'
    ]

    platform = random.choice(platforms)
    browser = random.choice(browsers)

    match browser:
        case 'Chrome':
            version = random.randint(70, 90)
            return f'Mozilla/5.0 ({platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version}.0.#{random.randint(1000, 9999)}.#{random.randint(10, 99)} Mobile Safari/537.36'

        case 'Firefox':
            version = random.randint(60, 80)
            return f'Mozilla/5.0 ({platform}; rv:{version}.0) Gecko/20100101 Firefox/{version}.0'

        case 'Safari':
            version = random.randint(10, 14)
            return f'Mozilla/5.0 ({platform}) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{version}.0 Safari/605.1.15'

        case 'Opera':
            version = random.randint(60, 80)
            return f'Mozilla/5.0 ({platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version}.0.#{random.randint(1000, 9999)}.#{random.randint(10, 99)} Mobile Safari/537.36 OPR/{version}.0'

        case 'Edge':
            version = random.randint(80, 90)
            return f'Mozilla/5.0 ({platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version}.0.#{random.randint(1000, 9999)}.#{random.randint(10, 99)} Mobile Safari/537.36 Edg/{version}.0'

        case 'UCBrowser':
            version = random.randint(12, 15)
            return f'Mozilla/5.0 ({platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.26 UBrowser/{version}.1.2.49 Mobile Safari/537.36'

        case 'SamsungBrowser':
            version = random.randint(10, 14)
            return f'Mozilla/5.0 ({platform}) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/{version}.0 Chrome/63.0.3239.26 Mobile Safari/537.36'

剩下得就是线程池,这里得使用得是一个开源项目jhao104/proxy_pool: Python爬虫代理IP池(proxy pool) (github.com)

解决到这里,其实已经差不多了。👀👀👀

4. 尾言

通过这次对携程的爬取,其实也可以总结一些经验,在遇到问题的时候的可以多去拓宽思路,不行就去多尝试。

项目地址 aglorice/CtripSpider: 携程评论爬虫,使用线程池来爬取热门景区评论,简单易用。一键爬取任意省的所有热门景区。 (github.com)

Loading comments...