构建爬取大众点评美食数据的多进程爬虫(一)

一、要抓取哪些数据

以东莞美食为例大众点评-美食-东莞,可以看到,在美食页面,我们想要爬取的信息有:

  1. 商户标题
  2. 星级
  3. 评论数
  4. 人均价格
  5. 各项评分
  6. 菜系
  7. 地址

二、分析页面

(一)多观察页面

通过点击小吃快餐,我们看到小吃快餐下一共有50个页面。再点击面包甜点,发现也只有50个页面。接下来我们看看面包甜点-东城区,发现最多也只有50个页面。如此类推,我们大概可以确定,某菜系-某区域最多只有50个页面,由此基本可以判断,这应该是大众点评做大反爬虫措施。对此,我们在稍后的爬虫设计中,不能简单地从某一菜系下爬取所有页面或者从某一区域下爬取所有页面,因为单独选定菜系或者区域,服务器最多只返回50个页面给你。 因此我们需要将菜系-区域组合起来爬取,尽可能多的爬取商户信息。
通过观察,我们发现在一个网页中,如:http://www.dianping.com/search/category/219/10/g117r434
g117代表菜系,r434代表区域,因此我们可以获取所有的菜系链接,接着在菜系链接的基础上获取菜系-区域链接,这就相当于在用浏览器浏览时,先选定了某一菜系,再选定某一区域。

(二)分析页面结构

这里就是要通过定位来选定我们需要的元素。如一级菜系,它所处的位置是id="classfydiv标签下的a标签中。
商户标题,它所处的位置是class="tit"div标签下的第一个a标签中。通过一个个地查看,我们可以得出我们要爬取的7个商户信息的位置,方便我们后续设计爬虫时进行定位。

三、设计爬虫

(一)爬虫思路

爬虫流程图
爬虫流程图
  1. start_url开始,爬取所有一级菜系链接,得到tag1_url,存入数据库。
  2. 从数据库中读取tag1_url,爬取所有二级菜系链接,得到tag2_url,存入数据库。
  3. 从数据库中读取tag2_url,爬取所有一级区域链接,得到addr1_url,存入数据库。
  4. 从数据库中读取addr1_url,爬取所有二级区域链接,得到addr2_url,存入数据库。
  5. 从数据库中读取addr2_url,爬取所有商户信息,得到dpshop_msg,存入数据库。
    1-4目的都是一样,为了获取最终要爬取的页面链接,5就是为了实际爬取上述7个商户信息,所以我们把1-4写到cate_parsing.py文件中,把5写到shop_parsing.py文件中。另外,为了应对反爬虫,我们将用于伪装的User-Agent代理IP等一些爬虫参数写到config.py文件中。

    (二)构建代码

    config.py代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #coding=utf-8
    #伪装浏览器
    USER_AGENT = [
    'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)',
    'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36',
    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11',
    'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16',
    'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0',
    'Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10'
    ]
    # 代理IP
    PROXY = [
    '111.13.111.184:80',
    '49.119.164.175:80',
    '61.136.163.245:3128',
    '116.199.2.210:80',
    '116.199.2.209:80',
    '116.199.115.79:80',
    '116.199.2.196:80',
    '121.40.199.105:80',
    '125.77.25.118:80',
    '122.228.253.55:808'
    ]
    TIMEOUT = 5
    LINKTIME = 3
    PAGE_NUM_MAX = 50

cate_parsing.py代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#coding=utf-8
import requests
import random
import pymongo
import time
from lxml import etree
from config import USER_AGENT, PROXY, TIMEOUT, LINKTIME
client = pymongo.MongoClient('localhost', 27017)
dp = client['dp']
# 爬取一级菜系、二级菜系、二级菜系下的一级地址
class GetTagAddr(object):
headers = random.choice(USER_AGENT)
proxies = {'http':random.choice(PROXY)}
s = requests.Session()
s.headers.update({'User-Agent':headers})
linktime = LINKTIME
timeout = TIMEOUT
tag1_url_db = dp['tag1_url_db'] # 存储从start_url中成功爬取到的tag1_url
tag2_url_db = dp['tag2_url_db'] # 存储从tag1_url中成功爬取到的tag2_url
crawly_tag1_ok = dp['crawly_tag1_ok'] # 存储爬取成功的tag1_url
addr1_url_db = dp['addr1_url_db'] # 存储从tag2_url中成功爬取到的addr1_url
crawly_tag2_ok = dp['crawly_tag2_ok'] # 存储爬取成功的tag2_url
addr2_url_db = dp['addr2_url_db'] # 存储从addr1_url中成功爬取到的addr2_url
crawly_addr1_ok = dp['crawly_addr1_ok'] # 存储爬取成功的addr1_url
def get_tag1_from(self, start_url):
# 不公开爬虫细节
def get_tag2_from(self, tag1_url):
# 不公开爬虫细节
def get_addr1_from(self, tag2_url):
# 不公开爬虫细节
def get_addr2_from(self, addr1_url):
# 不公开爬虫细节
if __name__ == '__main__':
url = 'http://www.dianping.com/search/category/219/10/g0r0'

get_tag1_from, get_tag2_from, get_addr1_fromget_addr2_from都采用了同样的逻辑:

  1. 先请求url,获得response。
  2. 使用etree.HTML解析网页。
  3. 定位所要爬取元素的位置。
  4. 存储。

shop_parsing.py代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#coding=utf-8
import requests
import random
import pymongo
import time
from lxml import etree
from config import USER_AGENT, PROXY, TIMEOUT, LINKTIME, PAGE_NUM_MAX
client = pymongo.MongoClient('localhost', 27017)
dp = client['dp']
addr2_url_db = dp['addr2_url_db'] # 存储从addr1_url中成功爬取到的addr2_url
dpshop = dp['dpshop'] # 存储从addr2中成功爬取到的dpshop_msg
crawly_addr2_ok = dp['crawly_addr2_ok'] # 存储爬取成功的addr2_url
def get_msg_from(response, addr2_url):
# 不公开爬虫细节
def get_all_msg_from(addr2_url):
# 不公开爬虫细节
def requests_url(result_url, linktime=LINKTIME):
# 不公开爬虫细节
if __name__ == '__main__':
addr2_url = 'http://www.dianping.com/search/category/219/10/g217r27028p3'

  1. requests_url函数用于请求网页,并返回状态码和response。
  2. get_msg_from函数用于解析源代码,定位元素,爬取并存储所有我们需要的商户信息。
  3. get_all_msg_from函数实现了多页码爬取,当遇到网页状态码为404时,代表此时没有相关商户,最后一页已经被爬取,自动跳出for循环。结束某个addr2_url的爬取。

run.py代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# coding=utf-8
from parsing.cate_parsing import GetTagAddr
from parsing.shop_parsing import get_all_msg_from, crawly_addr2_ok
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
if __name__ == '__main__':
start_url = 'http://www.dianping.com/search/category/219/10/g0r0'
tag_addr_task = GetTagAddr()
# 爬取tag1_url
tag_addr_task.get_tag1_from(start_url)
print('tag1_url爬取完成')
# 爬取tag2_url
tag1_wait = set(i['url'] for i in tag_addr_task.tag1_url_db.find())
tag1_ok = set(i['url'] for i in tag_addr_task.crawly_tag1_ok.find())
tag1_task = tag1_wait - tag1_ok
for tag1_url in tag1_task:
tag_addr_task.get_tag2_from(tag1_url)
print('tag2_url爬取完成')
# 爬取addr1_url
tag2_wait = set(i['url'] for i in tag_addr_task.tag2_url_db.find())
tag2_ok = set(i['url'] for i in tag_addr_task.crawly_tag2_ok.find())
tag2_task = tag2_wait - tag2_ok
for tag2_url in tag2_task:
tag_addr_task.get_addr1_from(tag2_url)
print('addr1_url爬取完成')
# 爬取addr2_url
addr1_wait = set(i['url'] for i in tag_addr_task.addr1_url_db.find())
addr1_ok = set(i['url'] for i in tag_addr_task.crawly_addr1_ok.find())
addr1_task = addr1_wait - addr1_ok
with ProcessPoolExecutor(max_workers=4) as executor:
executor.map(tag_addr_task.get_addr2_from, addr1_task)
print('addr2_url爬取完成')
# 根据addr2_url爬取商户信息
addr2_wait = set(i['url'] for i in tag_addr_task.addr2_url_db.find())
addr2_ok = set(i['url'] for i in crawly_addr2_ok.find())
addr2_task = addr2_wait - addr2_ok
with ThreadPoolExecutor(max_workers=8) as executor:
for url in addr2_task:
v = executor.submit(get_all_msg_from, url)
executor.shutdown(wait=True)
# executor.map(get_all_msg_from, addr2_task, chunksize=50)
print('addr2_url_reslut_url爬取完成')

  1. 爬取addr2_url采用多进程爬取,多进程通过concurrent.futures.ProcessPoolExecutor实现。
  2. 爬取dpshop_msg采用了多线程爬取,多线程通过congurrent.futures.ThreadPoolExecutor实现。