查看原文
其他

Python | 火锅年度报告:上海人最爱吃火锅?

沈仲强 Python大本营 2019-02-22


作者 | 沈仲强

编辑 | 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。)



推荐阅读:

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存