Python | 火锅年度报告:上海人最爱吃火锅?
作者 | 沈仲强
编辑 | Jane
出品 | Python大本营
【导语】明天就要过年了,每逢过年胖三斤,除了明天的年夜饭,还有好多饭局,要说营长的最爱,那必须是火锅啊!和家人一起吃一顿热腾腾的火锅,团团圆圆,享受在一起的时光。自古火锅就广受大家的喜爱,而且种类异彩纷呈,每个地方都有自己的特色。北京有铜锅涮羊肉和羊蝎子火锅、潮汕有牛肉火锅、重庆有麻辣火锅、四川有四川火锅和串串香、浙江有八生火锅、云南有滇味火锅、台湾有沙茶火锅,澳门有豆捞火锅,还有很多新派、创意特色火锅等等。正所谓“没有什么问题是一顿火锅不能解决的,如果不行,就两顿”,“唯一能阻止我减肥的就是火锅”,无论什么饭局,和谁一起约饭,火锅都是营长的首选。
今天我们就和大家一起分析一下全国火锅店的数据,看看大家觉得哪种火锅最受大家欢迎?哪个城市的人最爱吃火锅?结果可能跟你想的不一样哦~反正营长是都猜错了。
前言
忙碌了一整年,又到了过年的时候了,马上就要开启吃吃喝喝,“每逢佳节胖三斤”的节奏了。冬天里,最温暖人心的美食大概就是火锅了,虽然现在很少在家里吃火锅了,但火锅依然是我最喜欢的美食之一。俗话说,过年吃火锅,越吃越红火,和亲朋好友一起围坐在热气腾腾的火锅旁,吃着火锅、聊着天,那味道就是过年的味道。
由此,我这次专门用 Python 爬取了点评上全国 25000 多家火锅店,分析了一下这些火锅店的数据,来看看全国火锅的那些事儿!
关于数据:本文选取了全国 35 个热门城市,爬取了点评上每个城市中火锅店的价格、评论人数、评分等数据,基于这些数据做一下分析。
关于点评的反爬,在之前一篇文章(点评网的反爬再也不是烦恼)中有讲过,这里不再重复。
爬虫思路
点评上选取 35 个热门城市,对于每一个城市的页面,我们选取美食分类标签为“火锅”,像下面这样
然后爬取每个城市的所有火锅店。我们发现火锅店的 URL 都是类似
http://www.dianping.com/<city>/ch10/g110
这样的,所以我们只需要替换 URL 中的 <city> 就可以爬取不同城市的火锅店了。
在给出爬虫代码之前,先给出应对反爬的几个函数,对这部分原理不理解的可参考前面给出的文章
1import time
2import sys
3import pymongo
4import re
5from lxml import etree
6import requests
7headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"}
8
9def fix_url(url):
10 if not re.match(r"http", url):
11 return "http:" + url
12 return url
13
14# 获取css class的前缀名
15
16def get_class_prefix(city):
17 url = "http://www.dianping.com/{}/ch10".format(city)
18 r = requests.get(url, headers=headers)
19 content = r.content.decode("utf-8")
20 root = etree.HTML(content)
21 node = root.xpath('.//div[@id="shop-all-list"]/ul/li[1]//a[@class="review-num"]/b/span')[0]
22
23 prefix = node.attrib["class"][:2]
24 return prefix
25
26# 获取css的URL
27def get_css(city):
28 url = "http://www.dianping.com/{}/ch10".format(city)
29 r = requests.get(url, headers=headers)
30 content = r.content.decode("utf-8")
31 matched = re.search(r'href="([^"]+svgtextcss[^"]+)"', content, re.M)
32
33 if not matched:
34 raise Exception("cannot find svgtextcss file")
35
36 css_url = matched.group(1)
37 css_url = fix_url(css_url)
38 return css_url
39
40# 获取svg里的数字信息
41def get_svg(css_url, prefix):
42 r = requests.get(css_url, headers=headers)
43 content = r.content.decode("utf-8")
44 regex = r'span\[class\^="' + prefix + r'.*?background\-image: url\((.*?)\);'
45
46 matched = re.search(regex, content, re.M)
47
48 if not matched:
49 raise Exception("cannot find svg file")
50
51 svg_url = matched.group(1)
52 svg_url = fix_url(svg_url)
53 r = requests.get(svg_url, headers=headers)
54 content = r.content.decode("utf-8")
55 matched = re.findall(r'text x="[^"]+" y="([^"]+)">(\d+)</text>', content, re.M)
56
57 if not matched:
58 raise Exception("cannot find digits")
59
60 all_digits = []
61 tops = []
62
63 for group in matched:
64 top = group[0]
65 digits = list(group[1])
66 tops.append(top)
67 all_digits.append(digits)
68 return [tops, all_digits]
69
70# 获取每一个css class定义的偏移量
71def get_class_offset(css_url):
72 r = requests.get(css_url, headers=headers)
73 content = r.content.decode("utf-8")
74 matched = re.findall(r'(\.[a-zA-Z0-9-]+)\{background:(\-\d+\.\d+)px (\-\d+\.\d+)px', content)
75
76 result = {}
77 for item in matched:
78 css_class = item[0][1:]
79 left_offset = item[1]
80 top_offset = item[2]
81 result[css_class] = [left_offset, top_offset]
82
83 return result
84
85# 获取css class对应的坐标,坐标将会用于在svg里定位到相应的数字
86def class_to_coord(class_offset, class_name, svg_tops):
87 [left_offset, top_offset] = class_offset[class_name]
88 x = int((float(left_offset) + 7) / -12)
89 y = -1
90
91 for i in range(len(svg_tops)):
92 tmp = 24 - float(top_offset)
93 if tmp == float(svg_tops[i]):
94 y = i
95 break
96 if y < 0:
97 raise Exception("error in class_to_coord")
98 return (x, y)
99
100# 获取css class对应的实际的数值
101def get_number(node, class_offset, tops, all_digits):
102 num = 0
103 if node.text:
104 matched = re.search(r'(\d+)', node.text)
105 if matched and matched.group(1):
106 num = num * 10 + int(matched.group(1))
107
108 for digit_node in node:
109 class_name = digit_node.attrib["class"]
110 (x, y) = class_to_coord(class_offset, class_name, tops)
111 digit = float(all_digits[y][x])
112 num = num * 10 + digit
113
114 if len(node) == 0:
115 return num
116 last_digit = node[-1].tail
117 if last_digit:
118 matched = re.search(r'(\d+)', last_digit)
119 if matched and matched.group(1):
120 num = num * 10 + int(matched.group(1))
121 return num
上面最重要的就是 get_number 这个函数,这个函数会将点评网页上编码过的数值信息解析出来,比如像下面这些评论条数、人均、口味的数值:
我们看到的是数字,但是在爬取的时候看到的是这样的
所以需要通过 get_number 函数将数值解析出来。
有了上面的函数后,我们接下来爬取火锅店的信息。我们爬取火锅店的名字、评论数、人均、口味、环境、服务这些信息,把它们存入 MongoDB。
代码如下
1# 获取一个分页里的所有火锅店
2def get_restaurants_by_url(city, page_url):
3 prefix = get_class_prefix(city)
4 css_url = get_css(city)
5
6 [tops, all_digits] = get_svg(css_url, prefix)
7 class_offset = get_class_offset(css_url)
8 client = pymongo.MongoClient()
9 db = client.dianping
10 r = requests.get(page_url, headers=headers)
11 content = r.content.decode("utf-8")
12 root = etree.HTML(content)
13 shop_nodes = root.xpath('.//div[@id="shop-all-list"]/ul/li')
14
15 for shop_node in shop_nodes:
16 name_node = shop_node.xpath('.//div[@class="tit"]/a')[0]
17 name = name_node.attrib["title"]
18 url = name_node.attrib["href"]
19 print(name, url)
20 review_num = 0
21 review_num_nodes = shop_node.xpath('.//div[@class="comment"]/a[@class="review-num"]/b')
22
23 if len(review_num_nodes) > 0:
24 review_num_node = review_num_nodes[0]
25 review_num = get_number(review_num_node, class_offset, tops, all_digits)
26 price_nodes = shop_node.xpath('.//div[@class="comment"]/a[@class="mean-price"]/b')
27
28 price = None
29 if len(price_nodes) > 0:
30 price_node = price_nodes[0]
31 price = get_number(price_node, class_offset, tops, all_digits)
32
33 taste = 0
34 taste_nodes = shop_node.xpath('.//span[@class="comment-list"]/span[1]/b')
35
36 if len(taste_nodes) > 0:
37 taste_node = taste_nodes[0]
38 taste = get_number(taste_node, class_offset, tops, all_digits) / 10
39
40 env = 0
41 env_nodes = shop_node.xpath('.//span[@class="comment-list"]/span[2]/b')
42
43 if len(env_nodes) > 0:
44 env_node = env_nodes[0]
45 env = get_number(env_node, class_offset, tops, all_digits) / 10
46
47 service = 0
48 service_nodes = shop_node.xpath('.//span[@class="comment-list"]/span[3]/b')
49
50 if len(service_nodes) > 0:
51 service_node = service_nodes[0]
52 service = get_number(service_node, class_offset, tops, all_digits) / 10
53
54 db.restaurants.insert({
55 "city": city,
56 "url": url,
57 "name": name,
58 "review": review_num,
59 "price": price,
60 "taste": taste,
61 "env": env,
62 "service": service,
63 })
64 return len(shop_nodes)
65
66# 获取一个城市的所有火锅店
67def get_restaurants_by_city(city, start):
68 while start <= 50:
69 url = "http://www.dianping.com/{}/ch10/g110p{}".format(city, start)
70 shop_num = get_restaurants_by_url(city, url)
71 print(city, start, shop_num)
72 if shop_num == 0:
73 break
74 start += 1
75
76# 获取所有城市的火锅店
77def get_hotpot_restaurants():
78 cities = [
79 "shanghai",
80 "beijing",
81 "guangzhou",
82 "shenzhen",
83 "hangzhou",
84 "nanjing",
85 "suzhou",
86 "chengdu",
87 "wuhan",
88 "chongqing",
89 "xian",
90 "hongkong",
91 "xiamen",
92 "jinan",
93 "zhengzhou",
94 "qingdao",
95 "tianjin",
96 "taipei",
97 "hefei",
98 "changsha",
99 "xining",
100 "nanchang",
101 "wuxi",
102 "shenyang",
103 "jilin",
104 "haerbin",
105 "huhehaote",
106 "taiyuan",
107 "shijiazhuang",
108 "kunming",
109 "fuzhou",
110 "haikou",
111 "nanning",
112 "guiyang",
113 "lanzhou"
114 ]
115
116 for city in cities:
117 get_restaurants_by_city(city, 1)
上面每个函数的含义参见注释。接下来,我们只需要在main函数里调用get_hotpot_restaurants 就可以爬取火锅店了,如下
1if __name__ == "__main__":
2 get_hotpot_restaurants()
数据分析
爬取了火锅店的信息后,我们从 MongoDB 中提取出这些数据来作分析。我们要分析的信息有以下这些:
哪个城市的火锅最好吃
哪个城市吃一顿火锅最贵
哪个城市的人最爱吃火锅
哪个城市的火锅性价比最高
详细代码如下:
1def query():
2 client = pymongo.MongoClient()
3 db = client["dianping"]
4
5 # 一共爬取的火锅店总数
6 items = db.restaurants.aggregate([
7 {"$group": {"_id": "$url", }},
8 {"$group": {"_id": None, "count": {"$sum": 1}}},
9 ])
10 for item in items:
11 print(item["count"])
12
13 # 按照每个城市火锅店的口味平均得分降序排序
14 items = db.restaurants.aggregate([
15 {"$match": {"$and": [{"price": {"$gt": 0}}, {"review": {"$gt": 0}}, {"taste": {"$gt": 0}}]}},
16 {"$group": {"_id": "$url", "taste": {"$first": "$taste"}, "review": {"$first": "$review"}, "env": {"$first": "$env"}, "service": {"$first": "$service"}, "name": {"$first": "$name"}, "city": {"$first": "$city"}, "price": {"$first": "$price"}}},
17 {"$group": {"_id": "$city", "city": {"$first": "$city"}, "review_sum": {"$sum": "$review"}, "taste_sum": {"$sum": {"$multiply": ["$taste", "$review"]}}}},
18 {"$project": {"taste": {"$divide": ["$taste_sum", "$review_sum"]}}},
19 {"$sort": {"taste": -1}},
20 ])
21 print("================ taste ================")
22 for item in items:
23 print(item)
24
25 # 按照每个城市火锅店的平均价格降序排序
26 items = db.restaurants.aggregate([
27 {"$match": {"$and": [{"price": {"$gt": 0}}, {"review": {"$gt": 0}}, {"taste": {"$gt": 0}}]}},
28 {"$group": {"_id": "$url", "taste": {"$first": "$taste"}, "review": {"$first": "$review"}, "env": {"$first": "$env"}, "service": {"$first": "$service"}, "name": {"$first": "$name"}, "city": {"$first": "$city"}, "price": {"$first": "$price"}}},
29 {"$group": {"_id": "$city", "city": {"$first": "$city"}, "review_sum": {"$sum": "$review"}, "price_sum": {"$sum": {"$multiply": ["$price", "$review"]}}}},
30 {"$project": {"price": {"$divide": ["$price_sum", "$review_sum"]}}},
31 {"$sort": {"price": -1}}
32 ])
33 print("================ price ================")
34 for item in items:
35 print(item)
36
37 # 按照每个城市火锅店的总评论人数降序排序
38 items = db.restaurants.aggregate([
39 {"$match": {"$and": [{"price": {"$gt": 0}}, {"review": {"$gt": 0}}, {"taste": {"$gt": 0}}]}},
40 {"$group": {"_id": "$url", "taste": {"$first": "$taste"}, "review": {"$first": "$review"}, "env": {"$first": "$env"}, "service": {"$first": "$service"}, "name": {"$first": "$name"}, "city": {"$first": "$city"}, "price": {"$first": "$price"}}},
41 {"$group": {"_id": "$city", "city": {"$first": "$city"}, "review_sum": {"$sum": "$review"}}},
42 {"$sort": {"review_sum": -1}}
43 ])
44 print("================ review ================")
45 for item in items:
46 print(item)
47
48 # 按照每个城市火锅店的性价比降序排序
49 items = db.restaurants.aggregate([
50 {"$match": {"$and": [{"price": {"$gt": 0}}, {"review": {"$gt": 0}}, {"taste": {"$gt": 0}}]}},
51 {"$group": {"_id": "$url", "taste": {"$first": "$taste"}, "review": {"$first": "$review"}, "env": {"$first": "$env"}, "service": {"$first": "$service"}, "name": {"$first": "$name"}, "city": {"$first": "$city"}, "price": {"$first": "$price"}}},
52 {"$group": {"_id": "$city", "city": {"$first": "$city"}, "review_sum": {"$sum": "$review"}, "price_sum": {"$sum": {"$multiply": ["$price", "$review"]}}, "taste_sum": {"$sum": {"$multiply": ["$taste", "$review"]}}}},
53 {"$project": {
54 "price": {"$divide": ["$price_sum", "$review_sum"]},
55 "taste": {"$divide": ["$taste_sum", "$review_sum"]}
56 },
57 },
58 {"$project": {"tp": {"$divide": ["$taste", "$price"]}}},
59 {"$sort": {"tp": -1}}
60 ])
61 print("================ taste price ratio ================")
62 for item in items:
63 print(item)
对于我们关心的信息,我们用图表把它展示出来。
哪个城市的火锅最好吃
重庆火锅和成都火锅果然名不虚传,榜上有名。有意思的是,图上排名前四的都是我们的四大直辖市。
哪个城市吃一顿火锅最贵
可以看到香港是最贵的,这也不意外。香港什么都贵,吃火锅也贵,平均吃一顿火锅每的人均高达 285。
哪个城市的人最爱吃火锅
我们用评论的人数来估算火锅店的到访人数,上图中的数值,是每个城市所有火锅店评论数总数。这个绝对数值没什么参考价值,我们重点关心的是不同城市间的比较。
结果有点令人意外,全中国最喜欢吃火锅的是上海人。本以为北方天寒地冻,最爱吃火锅的会是北方人。但仔细想想,上海的潮湿阴冷是一种独特的冷,让你感觉冷到骨髓里的冷,上海人喜欢吃火锅也就可以理解了。
哪个城市的火锅性价比最高
我们用口味/价格来计算性价比指数。上图中我们看到,性价比最高的都是二三线城市,这些都是省会城市,虽然口味上不一定是最棒的,但这些城市吃火锅可以吃的又便宜又好吃。
好了,以上是我们本次的全部分析结果,这些结果在一定程度上与评论数据样本有关,比如城市人口基数不同、火锅店数量较大的差距,二来可能上海、北京人更喜欢写点评,如果有其他分析角度,欢迎在下方与我们多多交流!最后提前祝大家春节快乐,阖家团圆!
(本文为 Python大本营原创文章,转载请微信联系 1092722531。)
推荐阅读: