python爬虫爬取“美团美食”汕头地区的所有店铺信息

一、目的

获取美团美食每个店铺所有的评论信息,并保存到数据库和本地。要获取的店铺的评论专栏如下:
评论数据
最终将463609条评论数据保存到本地:
评论数据
评论数据

二、实现步骤

获取所有店铺的poiId

首先观察详情页的url,后面是跟着一串数字的,而这一串数字代表着每个店铺特有的id号,我们称之为poiId。所以,要想爬取所有店铺的评论数据,就必须爬取所有店铺的id号。

因此,退回到上一级页面,打开控制台,逐个点击请求的preview选项,找出携带有poiId数据的请求。最终我们找到了红色下划线的那条url。
在这里插入图片描述
而我们要做的,就是找出这个请求的规律,模拟客服端发送此请求,这样子我们就可以获得所有店铺的poiId了。

接下来就是找规律了,我们观察发送的请求。很长对不对,不要紧,我们慢慢分析。
在这里插入图片描述

  • https://st.meituan.com/meishi/api/poi/getPoiList? (固定的一段字符串)
  • cityName=%E6%B1%95%E5%A4%B4& (城市中文名字经过urlencode编码)
  • cateId=0&areaId=0&sort=&dinnerCountAttrId=& (固定的一段字符串,具体意义未知)
  • page=1 (页码)
  • userId=&uuid=9efd650a0d204774ba7a.1577010898.1.0.0 (一段cookie,每隔一段时间会更新,用于验证用户的身份,由后端传递到前端。F12打开控制台,点击Application的Cookies查看得知)
  • platform=1&partner=126& (一段固定的字符串)
  • originUrl=https%3A%2F%2Fst.meituan.com%2Fmeishi%2F(对该网页的url进行urlencode)
  • riskLevel=1&optimusCode=10 (一段固定的字符串)
  • _token=eJx1j1tvozAQhf%2BLX4OCDZiYvEEuu5ASQqGQpOoDIdxrmmAn0FT972u0uw%2F7sNJIc%2BbM0aeZL9DZZzBHEBoQSuCedWAO0BROdSABzsQGz2YQQQNjREQg%2Fccjmq5L4NRFSzB%2FJYohEUzeRuNZzK8IC6QgwzfptzZ0IRVN1BiyRQaUnF%2FYXJYZn9Ks4reknaYfVBaalZUsbvhPAAgCDUcCgaqkYTQazWiInvzp%2FO%2FsiqcEi1VFK1Tm9O91irhZr%2Fxyfy%2B1zVatrYPb7AezdSz%2FVL0Xvem5rDvuNfVHs3ZsDzaLhq%2FCa2XGrTFpL3Iw%2BAuTDMWS1rDcHnaIDC95PZucLrKMO9s77lhAbvoLjjM3juLwqt4CPSx6Kyw3k1SlqbM9J9q9R8vIoQ6nnoIvm%2FXjnq%2BLY%2FdQ%2FDJ%2F3pVtmlKVLvIocH5Gp9uTlusHjz662La4e93lONGGSjnbbbcOlivn8MjqzxwmW3WTqYteiXXGFKsk%2FgTZMFfB9y82H5QP (token令牌,每隔一段时间会更新)

而在这些参数中,有几个参数是必须的(皆可通过正则表达式获取):

  1. uuid(可以从cookie中获得,按理来说应该每隔一段时间就应该重新获取一次,但是我获取了一次之后就可以一直用,个人认为是后端没有验证该字段);
  2. city(获取店铺所在的城市名);
  3. page(页码。获取店铺数量,然后除以每页最大显示条目可得;该字段在“meishi/“文件里ctrl+F搜索totalCount可得)。

下面就是获取这几个参数的config.py文件源代码:

#获得城市名,uuid和商铺数目以及页数
import requests
import re  #用于正则表达式
import math

#获得城市名,uuid和商铺数目以及页数
def getInfo():
    """获取uuid"""
    url = 'https://st.meituan.com/meishi/'  #汕头美食
    headers = {
        'Host': 'st.meituan.com',
        'Referer': 'https://st.meituan.com/',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
    }
    res = requests.get(url, headers=headers).text
    # findall(pattern, string, flags=0),返回string中所有与pattern相匹配的全部字串,r表示原生字符例:\n不表示换行。re.S表示作用域拓展到整个字符串,即包括换行符
    if res:
        uuid = re.findall(r'"uuid":"(.*?)"', res, re.S)[0]
        city = re.findall(r'"chineseFullName":"(.*?)"',res,re.S)[0]
        shopsNum = re.findall(r'"totalCounts":(\d+)',res,re.S)[0]
        with open('./output_file/uuid_city_shopsNum.log', 'w',encoding="utf-8") as f:
            print('chrome_uuid:'+uuid+'\n'+'city:'+city+'\n'+'shopsNum:'+str(shopsNum))
            f.write('chrome_uuid:'+uuid+'\n'+'city:'+city+'\n'+'shopsNum:'+str(shopsNum))
    ans = {
        'uuid':uuid,
        'city':city,
        'shopsNum':int(shopsNum),
        'pages':math.ceil(int(shopsNum)/15),
    }
    return ans
ans = getInfo()

破解token参数
_token参数的构造:
解密: 由现成_token参数结尾的’='猜测进行了base64加密,于是进行base64解密,得到bytes类型字符串,进行zlib解压后得出_token的加密生成字典,其中有两个比较 重要的变化参数为ts和cts,其中ts为13位时间戳,cts则为ts+100*1000。还有一个sign参数,形式与_token参数一致,再对sign参数进行一次同样的解密,得到一个字符串,其中的uuid在首页源码中可以正则匹配出来。

加密: 由上可知_token参数的构造过程,进行了两次zlib压缩和base64编码加密。第一次加密对象位sign参数。第二次加密就是生成_token的字典,构造好字典后再进行一次上述加密即为_token。

另外,需要特别说明的是,_token参数破解了之后,仍会有一些参数是常量,一些是变量,但模拟的过程仍与前面模拟的过程是相似的,所以在这一不一一赘述,详见代码。

get_shops.py源代码:

'''
    用于保存所有页面的ajax_url
'''
import base64, zlib
import time
import random
import pandas as pd
import os
import urllib.parse
import json
import re  #用于正则表达式
from config import ans

print('ans:',ans)

get_shops_url = [] #用于存储所有生成的ajax_url

for page in range(1,ans['pages']+1):
    DATA = {
        "cityName": '汕头',
        "cateId": '0',
        "areaId": "0",
        "sort": "",
        "dinnerCountAttrId": "",
        "page": page,
        "userId": "",
        "uuid": ans['uuid'],
        "platform": "1",
        "partner": "126",
        "originUrl": "https://{}.meituan.com/meishi".format('st'),
        "riskLevel": "1",
        "optimusCode": "1"
    }
    SIGN_PARAM = "areaId={}&cateId={}&cityName={}&dinnerCountAttrId={}&optimusCode={}&originUrl={}/pn{}/&page={}&partner={}&platform={}&riskLevel={}&sort={}&userId={}&uuid={}".format(
        DATA["areaId"],
        DATA["cateId"],
        re.findall(r"b'(.+?)'",str(DATA["cityName"].encode(encoding='UTF-8',errors='strict')))[0],
        DATA["dinnerCountAttrId"],
        DATA["optimusCode"],
        DATA["originUrl"],
        DATA["page"],
        DATA["page"],
        DATA["partner"],
        DATA["platform"],
        DATA["riskLevel"],
        DATA["sort"],
        DATA["userId"],
        DATA["uuid"]
    )

    def encrypt(data):
        """压缩编码"""
        binary_data = zlib.compress(data.encode())      #二进制压缩
        base64_data = base64.b64encode(binary_data)     #base64编码
        return base64_data.decode()                     #返回utf-8编码的字符串

    def token():
        """生成token参数"""
        ts = int(time.time()*1000)  #获取当前的时间,单位ms
        #brVD和brR为设备的宽高,浏览器的宽高等参数,可以使用事先准备的数据自行模拟
        json_path = os.path.dirname(os.path.realpath(__file__))+'\\utils\\br.json'
        df = pd.read_json(json_path)
        brVD,brR_one,brR_two = df.iloc[random.randint(0,len(df)-1)]#iloc基于索引位来选取数据集
        TOKEN_PARAM ={
                "rId": 100900,
                "ver": "1.0.6",
                "ts": ts,  # 变量
                "cts": ts + random.randint(100, 120),  # 经测,cts - ts 的差值大致在 90-130 之间
                "brVD": eval(brVD),  # 变量
                "brR": [eval(brR_one), eval(brR_two), 24, 24],
                "bI": ["https://st.meituan.com/meishi/", ""],  # 从哪一页跳转到哪一页
                "mT": [],
                "kT": [],
                "aT": [],
                "tT": [],
                "aM": "",
                "sign": encrypt(SIGN_PARAM)
        }
        # 二进制压缩
        binary_data = zlib.compress(json.dumps(TOKEN_PARAM).encode())
        # print('binary_data:',json.dumps(TOKEN_PARAM).encode())
        # base64编码
        base64_data = base64.b64encode(binary_data)
        # print('这里是token的使用了ascii编码之前的:', base64_data)
        # print('这里是token的使用了ascii编码之后的:',urllib.parse.quote(base64_data.decode(),'utf-8'))
        return urllib.parse.quote(base64_data.decode(),'utf-8')

    AJAXDATA = {
        'basicUrl':'https://st.meituan.com/meishi/api/poi/getPoiList?',
        'cityName': '%E6%B1%95%E5%A4%B4',
        'cateId': 0,
        'areaId': 0,
        'sort': '',
        'dinnerCountAttrId': '',
        'page': page,
        'userId': '',
        'uuid': ans['uuid'],
        'platform': 1,
        'partner': 126,
        'originUrl': 'https%3A%2F%2Fst.meituan.com%2Fmeishi%2F',
        'riskLevel': 1,
        'optimusCode': 10,
        '_token': token()
    }

    urlParam = 'https://st.meituan.com/meishi/api/poi/getPoiList?cityName={}&cataId={}&areaId={}&sort={}&dinnerCountAttrId={}' \
               '&page={}&userId={}&uuid={}&platform={}&partner={}&originUrl={}&riskLevel={}&optimusCode={}&_token={}'.format(
        AJAXDATA['cityName'],
        AJAXDATA['cateId'],
        AJAXDATA['areaId'],
        AJAXDATA['sort'],
        AJAXDATA['dinnerCountAttrId'],
        AJAXDATA['page'],
        AJAXDATA['userId'],
        AJAXDATA['uuid'],
        AJAXDATA['platform'],
        AJAXDATA['partner'],
        AJAXDATA['originUrl'],
        AJAXDATA['riskLevel'],
        AJAXDATA['optimusCode'],
        AJAXDATA['_token'],
    )

然后,将ajax请求到的店铺数据保存到txt/csv/mongoDB数据库。因为在其他地方也可能调用到相应的方法(增删改查),因此,单独将他们写在另外一个.py文件里,然后封装成类。其中,save_data.py文件源代码如下:

'''
    定义类用于保存数据到数据库,txt或者csv
'''
import pandas as pd           # 将数据保存到csv中
import pymongo

class MongoDB():
    def __init__(self,formName,collection='',result=''):
        self.host = 'localhost'
        self.port = 27017
        self.databaseName = 'meituan'
        self.formName = formName
        self.result = result
        self.collection = collection

    # 连接数据库
    def collect_database(self):
        client = pymongo.MongoClient(host=self.host, port=self.port)  # 连接MongoDB
        db = client[self.databaseName]  # 选择数据库
        collection = db[self.formName]  # 指定要操作的集合,表
        print('数据库已经连接')
        return collection

    # 保存数据
    def save_to_Mongo(self):
        # collection = self.collect_database()
        try:
            if self.collection.insert_many(self.result):
                # print('存储到MongoDB成功', self.result)
                print('存储到MongoDB成功')
        except Exception:
            print('存储到MongoDb失败', self.result)

    # 查询数据
    def selectMongoDB(self):
        # collection = self.collect_database()
        print('评论数据的总长度为:',self.collection.count_documents({}))
        # print('正在查询数据库')
        # for x in self.collection.find():
        #     print(x)

    # 删除数据
    def delete_database(self):
        self.collection.delete_many({})  # 删除数据库内容
        print('已清空数据库')

class SaveDataInFiles():
    def __init__(self,csv_url='',txt_url='',results=''):
        # 需要保存的数据
        self.results = results
        self.csv_url = csv_url
        self.txt_url = txt_url

    # 出口文件
    def saveResults(self):
        self.saveInCsv()
        self.saveInTxt()

    # 将结果ip保存到D:\python\meituan\output_file\proxyIp_kuai.txt中
    def saveInTxt(self):
        txt = open(self.txt_url, 'w')
        txt.truncate()  # 保存内容前先清空内容
        for item in self.results:
            itemStr = str(item)
            txt.write(itemStr)
            txt.write('\n')
        txt.close()

    # 将结果保存到D:\python\meituan\output_file\proxyIp_kuai.csv中
    def saveInCsv(self):
        # print('csv:',self.results,self.csv_url)
        csvUrl = self.csv_url
        pd.DataFrame(self.results).to_csv(csvUrl,mode='a',encoding="utf-8-sig")  # 避免保存的中文乱码
        print('保存到csv文件中成功了')

然后,调用相应的方法将ajax获得的数据保存起来。最重要的是保存到mongoDB数据库(一般是先连接数据库,然后再执行增删改查的操作),保存到csv文件仅仅是为了直观的观察数据。save_shops_info.py文件源代码如下:

'''
    保存每个列表页所有商铺的基本信息
'''
import requests
import json
from get_shops import get_shops_url
from save_data import MongoDB
from save_data import SaveDataInFiles

output = [] #初始化数组,用于保存最终的结果
index = 1
# 定义类获取评论数据
def get_shops_info(ajax_url):
    url = ajax_url  # getshops传递过来的ajax_url
    headers = {
        'Host': 'st.meituan.com',
        'Referer': 'https://st.meituan.com/meishi/',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
        'X-Requested-With': 'XMLHttpRequest',
    }
    try:
        response = requests.get(url, headers=headers)
        # print('response:',response)
        # print('response_text:',response.text)
        # print('type(eval(response.text)):',type(eval(response.text)))
        if response.status_code == 200:
            return response.json()
    except requests.ConnectionError as e:
        print('Error', e.args)

# 从返回的json字符串中获取想要的字段
def save_shops_info(ajax_url,index):
    items = get_shops_info(ajax_url).get('data').get('poiInfos')
    output.extend(items)
    print('正在追加内容到output数组中')

if __name__ == '__main__':
    # for ajaxUrl in get_shops_url:
    #     save_shops_info(ajaxUrl,index)
    #保存数据到数据库中
    collection = MongoDB('shops_info','','').collect_database()    #连接数据库
    # MongoDB('shops_info', collection, '').delete_database()  # 先清空数据库内容
    # MongoDB('shops_info', collection, output).save_to_Mongo()
    # 保存数据到csv中
    # SaveDataInFiles('D:\python\meituan\output_file\shops_info.csv', '', output).saveInCsv()
    #将数据保存到json文件夹中
    # with open('D:\python\meituan\output_file\shops_info.json', 'w') as f:
    #     json.dump(output, f)
    # 查询数据库数据
    MongoDB('shops_info',collection,'').selectMongoDB()

到这一步,每一个店铺(汕头地区一共有1006家店)的信息都已经获取了,其中poiId最为重要,保存的数据如下:
在这里插入图片描述

获取每个店铺的评论信息

打开控制台,会发现,获取评论数据的ajax请求和前面获取店铺基本信息的请求相似,如下:

Request URL:
https://www.meituan.com/meishi/api/poi/getMerchantComment?uuid=9efd650a0d204774ba7a.1577010898.1.0.0&platform=1&partner=126&originUrl=https%3A%2F%2Fwww.meituan.com%2Fmeishi%2F152376939%2F&riskLevel=1&optimusCode=10&id=152376939&userId=&offset=0&pageSize=10&sortType=1

而其中的id=152376939就是我们前面保存的poiId。所以到了这一步,参照前面的方法,我们就可以获取后端传递过来的店铺评论数据了。
在这里插入图片描述
最后,将获取到的店铺评论数据保存起来。detailPage_getComments.py源代码如下:

# 根据数据库中汕头市外卖商铺信息,爬取所有商铺的评论信息

# 爬取美团外卖评论 https://www.meituan.com/meishi/41007600/
import requests  # 模拟浏览器向服务器发出请求
import math
import urllib.parse  # 定义了url的标准接口,实现url的各种抽取
from selenium import webdriver
from save_data import MongoDB
from save_data import SaveDataInFiles
from config import ans
from requests.adapters import HTTPAdapter


#######################################################################################################################
# 定义类获取商铺评论标签和所有评论
class GetShopComments():
    def __init__(self, shopBasicInfo, uuid, shop_num=''):
        self.comments_ajax_url = "https://www.meituan.com/meishi/api/poi/getMerchantComment?"
        self.ajax_headers = {
            'Host': 'www.meituan.com',
            'Referer': 'https://www.meituan.com/meishi/41007600/',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
            'X-Requested-With': 'XMLHttpRequest',
        }
        # 前面GetShopInformation的类中传递过来的最大页数
        self.maxPage = math.ceil(shopBasicInfo['allCommentNum'] / 10)
        self.shopName = shopBasicInfo['title']
        self.poiId = shopBasicInfo['poiId']
        self.uuid = uuid['uuid']
        # self.uuid = uuid
        self.shop_num = shop_num


    # 获取每个店铺页面上的所有数据(json格式),标签+评论
    def get_comments_in_page(self, items):
        parms = {
            'basicUrl':'https://www.meituan.com/meishi/api/poi/getMerchantComment?',
            'uuid': self.uuid,
            'platform': '1',
            'partner': '126',
            'originUrl': 'https%3A%2F%2Fwww.meituan.com%2Fmeishi%2F' + str(self.poiId) + '%2F',
            'riskLevel': '1',
            'optimusCode': '10',
            'id': self.poiId,
            'userId': '',
            'offset': items,
            'pageSize': '10',
            'sortType': '1',
        }
        url = self.comments_ajax_url + urllib.parse.urlencode(parms)
        # 连接超时,重新连接
        request = requests.Session()
        request.mount('http://', HTTPAdapter(max_retries=3))
        request.mount('https://', HTTPAdapter(max_retries=3))
        try:
            response = request.get(url, headers=self.ajax_headers,timeout=10)
            if response.status_code == 200:
                return response.json()
        # except requests.ConnectionError as e:
        except requests.exceptions.Timeout as e:
            print('Error', e.args)

    # 解析json数据,并获取评论数据
    def parse_comments_in_page(self, originJson, page):
        if originJson:
            items = originJson.get('data').get('comments')
            if items:
                for item in items:
                    comments = {
                        'shopName': self.shopName,
                        'page': page,
                        'username': item.get('userName'),
                        'user-icon': item.get('userUrl'),
                        'stars': item.get('star'),
                        'user-comment': item.get('comment'),
                        'user-comment-time': item.get('commentTime'),
                        'user-comment-zan': item.get('zanCnt')}
                    yield comments

    # 解析json数据,并获取标签评论数据
    def parse_comments_tags(self):
        if self.maxPage > 0:
            original_data = self.get_comments_in_page(1)
            if original_data:
                tags = original_data.get('data').get('tags')
                if tags:
                    for item in tags:
                        item['poiId'] = self.poiId
                        item['shopName'] = self.shopName
                    return tags
    # 评论数据的入口和出口
    def get_comments(self):
        commentsData = []  # 用于存储最终的结果,然后将结果保存到数据库中
        if self.maxPage > 0:
            for page in range(1, self.maxPage + 1):
                print('我现在已经爬取到第' + str(shop_num) + '家店铺的第' + str(page) + '页啦~')
                original_data = self.get_comments_in_page(page)
                results = self.parse_comments_in_page(original_data, page)
                for result in results:
                    commentsData.append(result)
            return commentsData

    # 评论标签数据


#######################################################################################################################

if __name__ == '__main__':
    shop_num = 0  # 用于统计爬到哪一家店铺
    # 开启新数据库用于保存评论数据
    tags_collection = MongoDB('shops_tags', '', '').collect_database()  # 连接数据库
    comments_collection = MongoDB('shops_comments', '', '').collect_database()  # 连接数据库
    # 查看数据库内容
    # MongoDB('shops_comments',comments_collection).selectMongoDB()
    # 清空数据库
    # MongoDB('shops_tags', tags_collection).delete_database()
    # MongoDB('shops_comments', comments_collection).delete_database()
    # 获取前面数据库中保存的商家数据
    collection = MongoDB('shops_info', '', '').collect_database()  # 连接数据库
    shops = collection.find({}, {"poiId": 1, "title": 1, "allCommentNum": 1})  # 只输出id和title字段,第一个参数为查询条件,空代表查询所有
    shops = list(shops)  # 将游标转换成数组
    for items in shops[0:]:
        shop_num = shop_num + 1  # 用于统计爬到哪一家店铺
        commentsRes = GetShopComments(items, ans, shop_num).get_comments()  # 获取店铺的所有评论
        tagsRes = GetShopComments(items, ans).parse_comments_tags()  # 获取评论标签
        MongoDB('shops_tags', tags_collection, tagsRes).save_to_Mongo()  # 保存评论标签数据
        MongoDB('shops_comments', comments_collection, commentsRes).save_to_Mongo()  # 保存评论数据
        SaveDataInFiles('D:\python\meituan\output_file\shop_comments.csv', '', commentsRes).saveInCsv()  # 保存评论数据到csv文件中
        SaveDataInFiles('D:\python\meituan\output_file\shop_tags.csv', '', tagsRes).saveInCsv()  # 保存评论数据到csv文件中

最终的效果就是文章最前面显示的那样。github项目地址

发布了28 篇原创文章 · 获赞 22 · 访问量 8056
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览