500元爬取美团外卖 标题党了一下,实际上并没有完整爬取,只是实现了部分爬取。具体情况是蚂蚁老师的学习群中发了一个500元的爬取美团店铺信息的爬虫单子,先是被人接单了,但没过多久,因为难度太大,而退单了。有难度的事情,咱们头铁,试试看能不能搞得定。
任务要求与难度评估目标网站是美团外卖的H5页面https://h5.waimai.meituan.com 先尝试找了一下目标数据,发现数据还是相对容易查到并解析的,整个任务难点不在数据,而在于以下3处:
登录需要手机号、短信验证码,且有请求发送短信,需要通过滑动条验证码的验证 ;登录后需要设置所在地,因为,所在地是影响搜索结果的,而如何设置所在地,有相当复杂的逻辑 (并没有深入研究);使用关键词搜索目标店铺、进入目标店铺、请求获取数据,等等有非常复杂的逻辑、参数极多,解析有大量的工作量 。 方案确定 综合上述难点,坦率而言,500元的标价是不匹配的,且时效要求只有1天,几乎是不可能的任务(大神除外)。于是先找甲方沟通了一下,发现甲方的实际需求极为简单,时效紧是因为短时间内有5家店铺信息需要获取上线。 而后续即使有新增目标,也是少量的,陆陆续续产生的需求。其实只需要解决获取数据,下载图片(每个店铺可能对应100多个菜品,此处是繁琐的工作量),且不出错即可。 对于甲方来说,其需求量少,不可能投入大量的资源解决这么一个小问题,对于接单人来说,又不可能为了这么一点收获,投入大量精力,二者存在偏差。那么如何使二者达成一致呢? 于是,我提出了半自动爬虫的方案 解决问题,也征得了甲方的同意。何谓半自动爬虫 呢?即在需求的店铺数量有限的前提下,在部分获取数据环节上通过人工操作的方式解决,回避前面的3个难点;而爬虫专注于解决数据解析,以及下载图片的繁琐步骤 。 达成一致后,那么就开始代码吧~~~
页面分析 这些步骤都人工操作了,就不需要分析页面了。唯一碰到的问题是,使用PC浏览器登录时,发送验证码的滚动条验证步骤无法通过,需要使用手机浏览器成功发送验证码后,将验证码填入PC端登录 。
巨坑注意:虽然PC端无法通过滚动条验证步骤,但是该动作还是要做的,否则即使填入了手机获取到的验证码,是无法提交登录的。(与甲方共同测试时,在这个环节卡了好久) 该步骤不难,F12打开chrome的开发者模式,进入店铺后,使用关键词搜索一下,就能发现,所有的菜品,都在一个food的response中
手机页面是竖屏,把窗口收窄能够有更好的浏览体验 人工复制数据的步骤 图片的请求则相当简单,只需要设置请求头,再使用GET请求图片网址即可,而图片网址在目标店铺的数据中,每个菜品都有对应的图片网址。
图片网址请求 解决步骤与代码将人工获取的店铺数据存入TXT文件,放在一个目录下,如下图: 人工获取的数据
有了数据之后,遍历、读取、加工数据、下载图片就都是基础的json、pandas与requests的操作了,具体详见代码注释吧。 # -*- coding: utf-8 -*- # @author: Lin Wei # @contact: 580813@qq.com # @time: 2022/6/18 9:24 """ 本程序用于分析爬取美团外卖的店铺商品信息及图片 1、本程序不考虑登录美团、定位、查找店铺等操作,直接使用人工查找获得的信息进行解析,获取店铺信息 2、根据店铺信息中的图片地址,下载对应图片 """ import timeimport requestsimport pandas as pdimport jsonimport pathlib as plfrom random import randintclass 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(1 , 3 ) / 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 True , None except Exception as e: return False , eif __name__ == '__main__' : spider = SpiderObj() res, err = spider.run() if res: input('程序已运行完毕,按回车键退出' ) else : input(f'程序运行出错,错误提示是: {err} ' )
大功告成 总结 不管全自动还是半自动,能解决问题的都是好爬虫。最后实现的爬虫实际上使用到的知识点都不是困难的:
读取文件,使用json、pandas解析数据输出Excel表格; 使用for循环,requests的get请求下载图片 因此需求是要进行多沟通的,说不定沟通后有难度的会变成没有难度的... 至于回避的那三个难点留给某位大老板用Money激发我去攻克吧^_^
今晚来蚂蚁老师抖音直播间,Python带副业全套餐有优惠!!!