基于Scrapy的Python3分布式淘宝爬虫

感觉网上的爬虫教程大多数还停留在Python2阶段,所以写了个Python3的爬虫来总结一下开发过程和自己踩过的坑。
所用工具基本都是目前最新的版本,多线程多进程,随机UA,分布式爬虫,爬取动态页面等功能基本都加上了。



开发环境:

  • 电脑系统:win10 64位
  • Python第三方库:lxml、Twisted、pywin32、scrapy、pymongo、scrapy-redis、redis
  • Python版本:Python 3.6.2rc2
  • Pycharm版本: PyCharm 2017.1.4
  • 数据库: MongoDB 3.4.6、redis 3.2.1

  • 完整代码已经托管到我的github仓库
    https://github.com/Leungtamir/Taobao_Spider


最终目的: 输入关键字和搜索页数,获取在淘宝上搜索结果中所有商品的标题、链接、原价、现价、商家地址以及评论数量,并将数据存入MongoDB数据库中


Step1 创建和配置Scrapy项目


cmd中输入

1
scrapy startproject taobao_spider

1. setting.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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# -*- coding: utf-8 -*-
# Scrapy settings for taobao_spider project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
# http://doc.scrapy.org/en/latest/topics/settings.html
# http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
# http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
BOT_NAME = 'taobao_spider'
SPIDER_MODULES = ['taobao_spider.spiders']
NEWSPIDER_MODULE = 'taobao_spider.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:54.0) Gecko/20100101 Firefox/54.0' #设置用户代理值
# Obey robots.txt rules
ROBOTSTXT_OBEY = False #不遵循 robots.txt协议
# Configure maximum concurrent requests performed by Scrapy (default: 16)
CONCURRENT_REQUESTS = 100
# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
#DOWNLOAD_DELAY = 3
# The download delay setting will honor only one of:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
#CONCURRENT_REQUESTS_PER_IP = 16
# Disable cookies (enabled by default)
COOKIES_ENABLED = False #取消Cookies
# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False
# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
#}
# Enable or disable spider middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
# 'taobao_spider.middlewares.TaobaoSpiderSpiderMiddleware': 543,
#}
# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
# 'taobao_spider.middlewares.MyCustomDownloaderMiddleware': 543,
#}
# Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
#EXTENSIONS = {
# 'scrapy.extensions.telnet.TelnetConsole': None,
#}
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'taobao_spider.pipelines.TaobaoSpiderPipeline': 300,
}
# Enable and configure the AutoThrottle extension (disabled by default)
# See http://doc.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
#AUTOTHROTTLE_DEBUG = False
# Enable and configure HTTP caching (disabled by default)
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

可根据自己情况选择设置IP,一般都不用设置

2. 在items.py中添加存储容器对象

1
2
3
4
5
6
7
8
9
10
class TaobaoSpiderItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
link = scrapy.Field()
price = scrapy.Field()
comment = scrapy.Field()
now_price = scrapy.Field()
address = scrapy.Field()
pass

Step2 爬虫程序的编写


1.创建程序

cmd中输入

1
2
cd taobao_spider
scrapy genspider -t basic taobao taobao.com

爬取思路:先爬取搜索结果页上的有用信息,再进入每一间店铺中获取剩余信息,然后对下一页重复操作

2. 分析搜索结果页

在淘宝中输入关键词“虾饺”,发现它的结果总共有60页。而前4页的网址如下:

显然,红色部分为“虾饺”的编码,蓝色部分则为页面的页码信息,为44*(页数-1),减去中间无用部分后,得到最简网址结构为:

1
https://s.taobao.com/search?q=虾饺&s=44*(i-1)

那么我们的回调函数parse()就可以这样写了

1
2
3
4
5
6
7
8
9
10
def parse(self, response):
key = input("请输入你要爬取的关键词\t")
pages = input("请输入你要爬取的页数\t")
print("\n")
print("当前爬取的关键词是",key)
print("\n")
for i in range(0,int(pages)):
url = "https://s.taobao.com/search?q=" + str(key) + "&s=" + str(44*i)
yield Request(url=url, callback=self.page)
pass

3. 抓取搜索结果页上的有用信息

右键查看网页源代码,Ctrl+F输入第一个商品的价格,找到第一个商品的有用信息

在这里我们就可以得到我们所需要的商品现价,商家地址。

进了前三家店铺的网页(前两家为淘宝店铺,第三家为天猫店铺),得到了他们的网址

显然店铺的网址是靠id来标识的,减去无用的部分后,得到最简网址结构为

1
https://item.taobao.com/item.htm?id=店铺id

并且从这里我们发现了淘宝和天猫的店铺的子域名是不同的,但无论是淘宝还是天猫,都可以通过这个构造方式来获取

到了这里,我们就知道我们在搜索结果页上所需要的信息:商品现价商家地址店铺id,通过正则表达式

1
2
3
pat_id = '"nid":"(.*?)"
pat_now_price = '"view_price":"(.*?)"'
pat_address = '"item_loc":"(.*?)"

就可以得到这三个信息

因此我们的page()函数就可以这样写了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def page(self,response):
body = response.body.decode('utf-8', 'ignore')
pat_id = '"nid":"(.*?)"' #匹配id
pat_now_price = '"view_price":"(.*?)"' #匹配价格
pat_address = '"item_loc":"(.*?)"' #匹配地址
all_id = re.compile(pat_id).findall(body)
all_now_price = re.compile(pat_now_price).findall(body)
all_address = re.compile(pat_address).findall(body)
for i in range(0, len(all_id)):
this_id = all_id[i]
now_price = all_now_price[i]
address = all_address[i]
url = "https://item.taobao.com/item.htm?id=" + str(this_id)
yield Request(url=url, callback=self.next, meta={ 'now_price': now_price, 'address': address})
pass
pass

4. 在店铺页面中爬取信息

我们还需要的信息有商品标题链接原价,和评论数目,其中商品链接可以直接通过page()函数中构造的url链接得到,商品的标题和原价则可以通过Xpath或者正则表达式来获取,由于上一步中我用了正则表达式,这里就用Xpath来提取吧

1
2
3
4
5
6
7
8
9
10
11
12
13
if web[0] != 'item.taobao': #天猫或天猫超市
title = response.xpath("//div[@class='tb-detail-hd']/h1/text()").extract() #获取商品名称
price = response.xpath("//span[@class = 'tm-price']/text()").extract() #获取商品原价格
pat_id = 'id=(.*?)&'
this_id = re.compile(pat_id).findall(url)[0]
pass
else: #淘宝
title = response.xpath("//h3[@class='tb-main-title']/@data-title").extract() #获取商品名称
price = response.xpath("//em[@class = 'tb-rmb-num']/text()").extract() #获取商品原价格
pat_id = 'id=(.*?)$'
this_id = re.compile(pat_id).findall(url)[0]
pass

但剩下的最后一个信息评论数目并不能通过这种方法得到,因为评论数目是淘宝通过 ajax动态请求、异步刷新生成的json数据,我们所爬取的静态网页源代码上是没有的,这就要通过抓包获取,使用抓包工具或者F12->Network来进行抓包,具体做法这里就不赘述了。(听说有个叫selenium的Python库也可以很方便地实现?以后有时间会试一试)

通过抓包之后,我发现两家卖虾饺的淘宝和天猫的店铺评论数所在包指向的网址分别是

1
2
3
4
5
淘宝:
https://rate.taobao.com/detailCount.do?_ksTS=1500743617865_99&callback=jsonp100&itemId=21096336641
天猫:
https://dsr-rate.tmall.com/list_dsr_info.htm?itemId=542003280330&spuId=0&sellerId=1574568070&_ksTS=1500743569328_193&callback=jsonp194

简化后为:

1
2
3
4
5
淘宝:
https://rate.taobao.com/detailCount.do?itemId=店铺ID
天猫:
https://dsr-rate.tmall.com/list_dsr_info.htm?itemId=店铺ID

后来在网上看到有人发现其实不管是淘宝还是天猫,都可以使用天猫的构造方式来得到含有评论数的网址,这样就方便多了

到了这里我们就可以构造出爬取店铺页面的信息的next()函数了

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
def next(self, response):
item = TaobaoSpiderItem()
url = response.url
pat_url = "https://(.*?).com"
web = re.compile(pat_url).findall(url)
#淘宝和天猫的某些信息采用不同方式的Ajax加载,
if web[0] != 'item.taobao': #天猫或天猫超市
title = response.xpath("//div[@class='tb-detail-hd']/h1/text()").extract() #获取商品名称
price = response.xpath("//span[@class = 'tm-price']/text()").extract() #获取商品原价格
pat_id = 'id=(.*?)&'
this_id = re.compile(pat_id).findall(url)[0]
pass
else: #淘宝
title = response.xpath("//h3[@class='tb-main-title']/@data-title").extract() #获取商品名称
price = response.xpath("//em[@class = 'tb-rmb-num']/text()").extract() #获取商品原价格
pat_id = 'id=(.*?)$'
this_id = re.compile(pat_id).findall(url)[0]
pass
#抓取评论总数
comment_url = 'https://dsr-rate.tmall.com/list_dsr_info.htm?itemId=' + str(this_id)
comment_data = urllib.request.urlopen(comment_url).read().decode('utf-8', 'ignore')
each_comment = '"rateTotal":(.*?),"'
comment = re.compile(each_comment).findall(comment_data)
item['title'] = title
item['link'] = url
item['price'] = price
item['now_price'] = response.meta['now_price']
item['comment'] = comment
item['address'] = response.meta['address']
yield item


Step3 对pipline.py中的数据进行处理


1. 将数据打印到终端

在此之前确保你的settings.py中的ITEM_PIPELINES项的注释已经去掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def process_item(self, item, spider):
title = item['title'][0]
link = item['link']
price = item['price'][0]
now_price = item['now_price']
comment = item['comment'][0]
address = item['address']
print('商品标题\t', title)
print('商品链接\t', link)
print('商品原价\t', price)
print('商品现价\t', now_price)
print('商家地址\t', address)
print('评论数量\t', comment)
print('------------------------------\n')
postItem = dict(商品标题=title,商品链接=link,商品原价=price,商品现价=now_price,商家地址=address,评论数量=comment)
self.coll.insert(postItem)
return item

运行scrapy : 在项目路径下cmd输入

1
F:\taobao_spide > scrapy crawl taobao --nolog

2. 将数据保存到MongoDB数据库中

MongoDB是当前最流行的开源Nosql数据库之一, 它的特点是高性能、易部署、易使用,存储数据非常方便,尤其是第三方支持特别丰富。通过PyMongo库我们可以很方便地在Python中操作MongoDB数据库

MongoDB官方下载地址

下载并配置好MongoDB

1
pip install pymongo

就可以在Python中愉快地操作MongoDB啦~

附上:PyMongo官方教程

其实我一直习惯用的是MySQL数据库,但是当时调试的时候一直连接不上,无奈之下只好换了MongoDB,结果打开了新世界大门,不需要提前建表,一句 coll.insert(postItem) 就能自动生成,最适合我这种懒人了。

pipline.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
class TaobaoSpiderPipeline(object):
def __init__(self):
# 链接数据库
self.client = pymongo.MongoClient(host=settings['MONGO_HOST'], port=settings['MONGO_PORT'])
# self.client.admin.authenticate(settings['MINGO_USER'], settings['MONGO_PSW']) #如果有账户密码
self.db = self.client[settings['MONGO_DB']] # 获得数据库的句柄
self.coll = self.db[settings['MONGO_COLL']] # 获得collection的句柄
def process_item(self, item, spider):
try:
title = item['title'][0]
link = item['link']
price = item['price'][0]
now_price = item['now_price']
comment = item['comment'][0]
address = item['address']
print('商品标题\t', title)
print('商品链接\t', link)
print('商品原价\t', price)
print('商品现价\t', now_price)
print('商家地址\t', address)
print('评论数量\t', comment)
print('------------------------------\n')
postItem = dict(商品标题=title,商品链接=link,商品原价=price,商品现价=now_price,商家地址=address,评论数量=comment)
self.coll.insert(postItem)
return item
except Exception as err:
pass

然后打开MongoDB就可以使用了

这里注意一下,不同于MySQL,MongoDB默认是没有用户和密码的

下面是关键词虾饺的搜索结果,这里我使用的可视化工具是Robmongo

Step4 设置随机User-Agent

增加随机请求头是最简单的反反爬虫方式,每次请求时通过更换不同的user-agent,可以更好地伪装浏览器。

  1. setting.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
    USER_AGENT_LIST = ['zspider/0.9-dev http://feedback.redkolibri.com/',
    'Xaldon_WebSpider/2.0.b1',
    'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) Speedy Spider (http://www.entireweb.com/about/search_tech/speedy_spider/)',
    'Mozilla/5.0 (compatible; Speedy Spider; http://www.entireweb.com/about/search_tech/speedy_spider/)',
    'Speedy Spider (Entireweb; Beta/1.3; http://www.entireweb.com/about/search_tech/speedyspider/)',
    'Speedy Spider (Entireweb; Beta/1.2; http://www.entireweb.com/about/search_tech/speedyspider/)',
    'Speedy Spider (Entireweb; Beta/1.1; http://www.entireweb.com/about/search_tech/speedyspider/)',
    'Speedy Spider (Entireweb; Beta/1.0; http://www.entireweb.com/about/search_tech/speedyspider/)',
    'Speedy Spider (Beta/1.0; www.entireweb.com)',
    'Speedy Spider (http://www.entireweb.com/about/search_tech/speedy_spider/)',
    'Speedy Spider (http://www.entireweb.com/about/search_tech/speedyspider/)',
    'Speedy Spider (http://www.entireweb.com)',
    'Sosospider+(+http://help.soso.com/webspider.htm)',
    'sogou spider',
    'Nusearch Spider (www.nusearch.com)',
    'nuSearch Spider (compatible; MSIE 4.01; Windows NT)',
    'lmspider (lmspider@scansoft.com)',
    'lmspider lmspider@scansoft.com',
    'ldspider (http://code.google.com/p/ldspider/wiki/Robots)',
    'iaskspider/2.0(+http://iask.com/help/help_index.html)',
    'iaskspider',
    'hl_ftien_spider_v1.1',
    'hl_ftien_spider',
    'FyberSpider (+http://www.fybersearch.com/fyberspider.php)',
    'FyberSpider',
    'everyfeed-spider/2.0 (http://www.everyfeed.com)',
    'envolk[ITS]spider/1.6 (+http://www.envolk.com/envolkspider.html)',
    'envolk[ITS]spider/1.6 ( http://www.envolk.com/envolkspider.html)',
    'Baiduspider+(+http://www.baidu.com/search/spider_jp.html)',
    'Baiduspider+(+http://www.baidu.com/search/spider.htm)',
    'BaiDuSpider',
    'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0) AddSugarSpiderBot www.idealobserver.com',
    ]
    DOWNLOADER_MIDDLEWARES = {
    'taobao_spider.MidWare.HeaderMidWare.ProcessHeaderMidware': 543,
    }
  2. setting.py同级目录下,新建一个文件夹MidWare,在MidWare文件夹中新建Python文件HeaderMidWare.py,在里面增加以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # encoding: utf-8
    from scrapy.utils.project import get_project_settings
    import random
    settings = get_project_settings()
    class ProcessHeaderMidware():
    """process request add request info"""
    def process_request(self, request, spider):
    """
    随机从列表中获得header, 并传给user_agent进行使用
    """
    ua = random.choice(settings.get('USER_AGENT_LIST'))
    spider.logger.info(msg='now entring download midware')
    if ua:
    request.headers['User-Agent'] = ua
    # Add desired logging message here.
    spider.logger.info(u'User-Agent is : {} {}'.format(request.headers.get('User-Agent'), request))
    pass

Step5 使用scrapy-redis实现分布式爬虫

到这里为止,我们的爬虫已经可以自给自足地运行,并有了一定的防反爬虫能力了,如果爬取的信息不多,这个程序已经够我们拿来玩了。

但是这样的爬虫其效率相当低下,原因是阻塞等待发往网站的请求和网站返回。由于Scrapy自带多线程(默认线程数为10),如果想进一步提高效率和防反爬虫能力,就要用到多进程和分布式爬虫了。

  • Redis 是一个开源的内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。

  • Scrapy-Redis是一个基于redis的Scrapy分布式组件,它将起始的网址从start_urls里分离出来,改为从Redis读取,多个客户端可以同时读取同一个redis,从而实现了分布式的爬虫。

下面我将通过Scrapy-redis来改进我们的爬虫

1. scrapy_redis环境搭建

1.安装并启动redis

Redis安装并配置教程

2.通过pip安装Python第三方库

1
2
pip install scrapy-redis
pip install redis

2.修改scrapy实现分布式功能

1.在setting.py中增添以下内容

1
2
3
4
5
6
7
SCHEDULER = "scrapy_redis.scheduler.Scheduler" #启用Redis调度存储请求队列
SCHEDULER_PERSIST = True #不清除Redis队列、这样可以暂停/恢复 爬取
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" #确保所有的爬虫通过Redis去重
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderPriorityQueue'
REDIS_HOST = '127.0.0.1' # 也可以根据情况改成 localhost
REDIS_PORT = 6379
REDIS_URL = None

用多台主机部署分布式爬虫时,在REDIS_URL中填入连接redis的主机URL,我这里只在一台主机上使用,写None或去掉这句话都可以。

官方文档:https://github.com/rmax/scrapy-redis

2.在items.py中增添以下内容

1
2
3
4
5
6
7
8
9
from scrapy.item import Item, Field
from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose, TakeFirst, Join
class TaobaoSpiderLoader(ItemLoader):
default_item_class = TaobaoSpiderItem
default_input_processor = MapCompose(lambda s: s.strip())
default_output_processor = TakeFirst()
description_out = Join()

3.修改爬虫程序taobao.py

首先import一下

1
from scrapy_redis.spiders import RedisSpider

然后修改TaobaoSpider类,把它的父类从scrapy.spider改为RedisSpider,这样爬虫就是直接从redis中读取url,原本的allowed_domainsstart_urls这两项就没用了,我们用redis_key来从redis中获取爬取网站的初始网址

1
2
3
4
5
class TaobaoSpider(RedisSpider):
name = 'taobao'
#allowed_domains = ['taobao.com']
# start_urls = ['http://taobao.com/']
redis_key = 'TaobaoSpider:start_urls'

3. 运行分布式爬虫

1.在项目路径下打开cmd,分别输入:

1
scrapy crawl taobao --nolog

这时爬虫处于等待输入状态,因为我们还没有设置start_url

2.在redis目录下打开cmd,输入:

1
2
3
D:\Redis>redis-cli
127.0.0.1:6379> LPUSH TaobaoSpider:start_urls http://taobao.com/

结果返回 (integer) 1 则表示成功

3.这时候你应该能看到你的多个终端都在同时运行爬虫程序了,体验飞一般的速度吧

四个程序同时跑:

4.如果这时候我们按下 ctrl+c 停止程序,再输入 scrapy crawl taobao –nolog 运行的话,会发现程序并不是重新启动,而是从刚刚结束的地方开始爬取,这是因为我们在setting.py中设置了 SCHEDULER_PERSIST = True ,使它有了断点续传的功能,如果想取消这个功能,只要把True改为False就可以了

5.程序运行结束后,要清除redis中的缓存,否则下次爬取同样内容时爬虫是不会动的

redis终端中:

1
127.0.0.1:6379> flushdb

Step6 爬虫实际速度测试实验

实验条件

  • 当前网速:1.92MB/s (由360宽带测速器测得)
  • 爬取关键词 小吃
  • 爬取页数:100

实验结果

  • 单终端爬取时间:21分30秒
  • 四终端爬取时间: 5分05秒
  • 八终端爬取时间: 2分16秒

实验结论

实验结果比较理想,由此也可知虽然Scrapy很强大,但在大规模的分布式应用时捉襟见肘,利用redis实现分布式爬虫,效率得到了极大的提高。

小结

通过Python3.6scrapy构建了一个淘宝商城的爬虫,然后通过scrapy-redis实现了分布式爬虫,最后通过MongoDB来存储数据。主要难度在于淘宝订单数据的抓包。

爬虫和反爬虫之间的斗争是永无休止的。 我这里只是使用了很简单的反反爬虫技巧,要想实现一个更高效稳定的爬虫,还需要IP代理Cookies登陆限速访问验证码识别等技巧,有时候还需要根据不同的服务器和反爬虫机制,定制出该网站专用的爬虫,有兴趣的朋友可以深入研究。

就到这里,谢谢大家