社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

Python副业500元,爬取美团外卖

蚂蚁学Python • 1 年前 • 429 次点击  

500元爬取美团外卖

  标题党了一下,实际上并没有完整爬取,只是实现了部分爬取。具体情况是蚂蚁老师的学习群中发了一个500元的爬取美团店铺信息的爬虫单子,先是被人接单了,但没过多久,因为难度太大,而退单了。有难度的事情,咱们头铁,试试看能不能搞得定。

任务要求与难度评估

  • 任务要求:
  1. 目标网站是美团外卖的H5页面https://h5.waimai.meituan.com
  2. 具体任务是爬取目标店铺的菜品、价格、图片等
  3. 目标店铺可以自定义
  • 难度评估:

  先尝试找了一下目标数据,发现数据还是相对容易查到并解析的,整个任务难点不在数据,而在于以下3处:

  1. 登录需要手机号、短信验证码,且有请求发送短信,需要通过滑动条验证码的验证
  2. 登录后需要设置所在地,因为,所在地是影响搜索结果的,而如何设置所在地,有相当复杂的逻辑(并没有深入研究);
  3. 使用关键词搜索目标店铺、进入目标店铺、请求获取数据,等等有非常复杂的逻辑、参数极多,解析有大量的工作量

方案确定

  综合上述难点,坦率而言,500元的标价是不匹配的,且时效要求只有1天,几乎是不可能的任务(大神除外)。于是先找甲方沟通了一下,发现甲方的实际需求极为简单,时效紧是因为短时间内有5家店铺信息需要获取上线。
  而后续即使有新增目标,也是少量的,陆陆续续产生的需求。其实只需要解决获取数据,下载图片(每个店铺可能对应100多个菜品,此处是繁琐的工作量),且不出错即可。
  对于甲方来说,其需求量少,不可能投入大量的资源解决这么一个小问题,对于接单人来说,又不可能为了这么一点收获,投入大量精力,二者存在偏差。那么如何使二者达成一致呢?
  于是,我提出了半自动爬虫的方案解决问题,也征得了甲方的同意。何谓半自动爬虫呢?即在需求的店铺数量有限的前提下,在部分获取数据环节上通过人工操作的方式解决,回避前面的3个难点;而爬虫专注于解决数据解析,以及下载图片的繁琐步骤
  达成一致后,那么就开始代码吧~~~

页面分析

  1. 登录、设置地址、查找目标店铺

  这些步骤都人工操作了,就不需要分析页面了。唯一碰到的问题是,使用PC浏览器登录时,发送验证码的滚动条验证步骤无法通过,需要使用手机浏览器成功发送验证码后,将验证码填入PC端登录

  • 巨坑注意:虽然PC端无法通过滚动条验证步骤,但是该动作还是要做的,否则即使填入了手机获取到的验证码,是无法提交登录的。(与甲方共同测试时,在这个环节卡了好久)
  1. 目标店铺数据分析

  该步骤不难,F12打开chrome的开发者模式,进入店铺后,使用关键词搜索一下,就能发现,所有的菜品,都在一个food的response中

手机页面是竖屏,把窗口收窄能够有更好的浏览体验
人工复制数据的步骤
  1. 图片目标分析

  图片的请求则相当简单,只需要设置请求头,再使用GET请求图片网址即可,而图片网址在目标店铺的数据中,每个菜品都有对应的图片网址。

图片网址请求

解决步骤与代码

  1. 将人工获取的店铺数据存入TXT文件,放在一个目录下,如下图:
人工获取的数据
  1. 有了数据之后,遍历、读取、加工数据、下载图片就都是基础的json、pandas与requests的操作了,具体详见代码注释吧。
# -*- coding: utf-8 -*-
# @author: Lin Wei
# @contact: 580813@qq.com
# @time: 2022/6/18 9:24

"""
本程序用于分析爬取美团外卖的店铺商品信息及图片
1、本程序不考虑登录美团、定位、查找店铺等操作,直接使用人工查找获得的信息进行解析,获取店铺信息
2、根据店铺信息中的图片地址,下载对应图片
"""

import time
import requests
import pandas as pd
import json
import pathlib as pl
from random import randint


class SpiderObj:
    """
    爬虫对象
    """


    def __init__(self):
        """
        初始化对象
        """

        self.file_path = pl.Path(input('请已保存的店铺数据文件路径:\n'))

    def create_dir(self, shop_name: str) -> tuple:
        """
        检查并创建文件夹
        :param shop_name: 店铺名
        :return:
        """

        shop_dir = self.file_path / shop_name
        pic_dir = shop_dir / '图片'
        if not shop_dir.is_dir():  # 如果店铺文件夹不存在,则创建
            shop_dir.mkdir()
        if not pic_dir.is_dir():  # 如果图片文件夹不存在,则创建
            pic_dir.mkdir()
        return shop_dir, pic_dir

    @classmethod
    def get_origin_price(cls, ser: pd.Series) -> float:
        """
        解析skus中的原价origin_price
        :param ser: 数据行
        :return:
        """

        skus = ser['skus'][0]
        origin_price = skus['origin_price']
        return origin_price

    @classmethod
    def parse_data(cls, filename: pl.Path) -> tuple:
        """
        解析获取到的美团店铺数据
        :param filename: 存储数据的文件路径
        :return:
        """

        with open(filename, 'r', encoding='utf-8'as fin:
            data = fin.read()
        data = json.loads(data)
        # 解析数据步骤
        shop_name = data['data']['poi_info']['name']
        data = data['data']['food_spu_tags']
        df = pd.DataFrame()
        for tag in data:
            dfx = pd.DataFrame(tag['spus'])
            dfx['分类'] = tag['name']
            df = pd.concat([df, dfx])
        df = df.loc[df['分类'].map(lambda x: x not in ['折扣''热销''推荐'])]
        df['原价'] = df.apply(cls.get_origin_price, axis=1)
        df.reset_index(inplace=True, drop=True)
        return shop_name, df

    @classmethod
    def download_picture(cls, url: str, filename: pl.Path):
        """
        下载图片的方法
        :param url: 图片的地址
        :param filename: 输出图片的路径(含文件名)
        :return:
        """

        # 初始化请求头
        headers = {
            "accept""image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
            "accept-encoding""gzip, deflate, br",
            "accept-language""zh-CN,zh;q=0.9",
            "referer""https://h5.waimai.meituan.com/",
            "sec-ch-ua""\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"97\", \"Chromium\";v=\"97\"",
            "sec-ch-ua-mobile""?0",
            "sec-ch-ua-platform""\"Windows\"",
            "sec-fetch-dest""image",
            "sec-fetch-mode""no-cors",
            "sec-fetch-site""cross-site",
            "user-agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36"
        }
        # 下载文件
        file = requests.get(url, headers, stream=True)
        with open(filename, "wb"as code:
            for chunk in file.iter_content(chunk_size=1024):  # 边下载边存硬盘
                if chunk:
                    code.write(chunk)

    @classmethod
    def get_pictures(cls, shop_name: str, data: pd.DataFrame, pic_dir: pl.Path):
        """
        批量获取图片数据的方法
        :param shop_name: 店铺名
        :param data: 数据
        :param pic_dir: 图片存放的目录
        :return:
        """

        print(f'开始下载店铺:{shop_name} 的图片')
        # 下载前按照菜品名称与图片地址进行去重处理,减少请求数量
        download_data = data.copy()
        download_data = download_data.drop_duplicates(['name''picture'], keep='last')
        # 筛选去除图片地址为空的
        download_data = download_data.loc[
            (download_data['picture'].map(lambda x: pd.notnull(x))) |
            (download_data['picture'] != '')
            ]
        max_len = len(download_data)
        for idx, food in enumerate(download_data.to_dict(orient='records')):  # 遍历数据
            pic_url = food['picture']
            # 拆分获取图片扩展名
            suffix = pl.Path(pic_url.split('/')[-1]).suffix
            # 加工出图片的路径(包含名称)
            name = food['name'].replace('\\''_').replace('/''_')
            filename = pic_dir / f"{name}{suffix}"
            # 使用下载方法下载
            try:
                cls.download_picture(pic_url, filename)
                print(f'({idx+1}/{max_len})菜品:{food["name"]} 图片下载完成')
            except Exception as e:
                print(f'!!!({idx+1}/{max_len})菜品:{food["name"]} 图片下载失败,错误提示是: {e}')
            # 随机暂停
            time.sleep(randint(13) / 10)

    @classmethod
    def write_data(cls, shop_name, data, shop_dir):
        """
        将数据输出至excel文件
        :param shop_name: 店铺名
        :param data: 数据
        :param shop_dir: 店铺存放的文件夹
        :return:
        """

        data.to_excel(shop_dir / f'{shop_name}.xlsx', index=False)

    def run(self):
        """
        运行程序
        :return:
        """

        try:
            for filename in self.file_path.iterdir():
                # 先解析人工取得的数据
                shop_name, data = self.parse_data(filename)
                # 再创建文件夹
                shop_dir, pic_dir = self.create_dir(shop_name)
                # 写入Excel文件
                self.write_data(shop_name, data, shop_dir)
                # 获取图片
                self.get_pictures(shop_name, data, pic_dir)
                print(f'店铺:{shop_name}的数据已解析下载完毕,数据存储在:“{shop_dir.absolute()}”路径下')
            return TrueNone
        except Exception as e:
            return False, e


if __name__ == '__main__':
    spider = SpiderObj()
    res, err = spider.run()
    if res:
        input('程序已运行完毕,按回车键退出')
    else:
        input(f'程序运行出错,错误提示是: {err}')

  1. 程序运行
大功告成

总结

  不管全自动还是半自动,能解决问题的都是好爬虫。最后实现的爬虫实际上使用到的知识点都不是困难的:

  1. 读取文件,使用json、pandas解析数据输出Excel表格;
  2. 使用for循环,requests的get请求下载图片
  3. 创建文件夹,去重、try-except等

因此需求是要进行多沟通的,说不定沟通后有难度的会变成没有难度的...
至于回避的那三个难点留给某位大老板用Money激发我去攻克吧^_^



今晚来蚂蚁老师抖音直播间,Python带副业全套餐有优惠!!!



Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/136094
 
429 次点击