前言
这是本人的第一篇博客,感触还是很多的,最近在帮朋友做一个分布式爬虫的论文,遇到很多坑,不过已经一一填平,废话不多说啦。
分类
(1)主从分布式爬虫:
由一台master服务器, 来提供url的分发, 维护待抓取url的list。由多台slave服务器执行网页抓取功能, slave所抽取的新url,一律由master来处理解析,而slave之间不需要做任何通信。
(2)对等分布式爬虫:
由多台相同的服务器集成,每台服务器可单独运作,完成爬虫工作,每台服务器之间的分工有一定的运算逻辑(ex: hash),由运算(配置)的结果,来决定由那台服务器做抓取网页的工作。
具体查看:http://www.dataguru.cn/thread-529666-1-1.html
本文讲解第一种-主从式,只是简单的阐明,所以master端只负责爬取url保存到redis数据库,slave端取出redis里url队列进行爬取网页内容,解析并保存到mongodb数据库。
准备
python3(本人使用的是3.6版本)scrapy
redis
mongodb
安装教程自行百度,使用到的python模块(这里是需要使用pip安装,最好是pip新版本):
scrapy-redis
pymongo
python链接mongodb例子:
import pymongo as pm
host = 'localhost'
port = 27017
# 链接数据库
client = pm.MongoClient(host,port)
# 选择db
db = client.demo
# 选择集合test
# 注意这里的集合可以直接使用,如果没有mongodb会自动创建
db.test.insert({"name":"hello"})
client.close()
实现
我们以58同城为例,爬取二手房,爬取中间有些错误但不影响,由于网站整改302重定向了。1. master端
spider文件
from scrapy.spider import CrawlSpider,Rule
from scrapy.linkextractors import LinkExtractor
from master.items import MasterItem
class myspider(CrawlSpider):
name = 'master'
allowed_domains = ['58.com']
item = MasterItem()
start_urls = ['http://cd.58.com/ershoufang/']
rules = (
Rule(LinkExtractor(allow=('http://cd.58.com/ershoufang/\d{14}x.shtml.*?',)), callback='parse_item',
follow=True),
)
def parse_item(self,response):
item = self.item
item['url'] = response.url
return item
继承CrawlSpider可以遍历整个网站,Rule和LinkExtractor限制遍历那些网页,allow为允许的网址(正则表达式),callback回调函数,注意一定不能写默认的parse函数。
item文件
import scrapy
class MasterItem(scrapy.Item):
# define the fields for your item here like:
url = scrapy.Field()
pass
只保存url
middleware文件
import random
from .useragent import agents
class UserAgentMiddleware(object):
def process_request(self, request, spider):
agent = random.choice(agents)
referer = request.meta.get('referer', None)
request.headers["User-Agent"] = agent
request.headers["Referer"] = referer
这里对默认的middleware文件进行修改,useragent是自建的文件,里面agents=['内容略']数组,字段代码表示为每次请求随机一个User-Agent(浏览器身份),一定程度避免认为是爬虫。
pipeline文件
import redis
class MasterPipeline(object):
def __init__(self):
self.redis_url = 'redis://123456:@localhost:6379/'
self.r = redis.Redis.from_url(self.redis_url,decode_responses=True)
def process_item(self, item, spider):
self.r.lpush('myredis:start_urls', item['url'])
保存url到redis数据库,redis_url中123456为数据库密码,你的redis没有密码就不用写,第二种链接方式:
redis.Redis(host="localhost",port=6379)
默认使用db0,一定要使用db0,否则slave端取不到数据(可能由于本人才疏学浅没有找到链接其他db供slave端使用的方式)
setting文件
ROBOTSTXT_OBEY = False
DOWNLOADER_MIDDLEWARES = {
'master.middlewares.UserAgentMiddleware': 543,
}
ITEM_PIPELINES = {
'master.pipelines.MasterPipeline': 300,
}
注意这里只是部分代码不要覆盖上去,替换相同的地方即可。爬虫一开始会取得网站的robot.txt文件(也叫君子协议文件),得知那些网址可爬,我们设置ROBOTSTXT_OBEY=False不遵循他的协议。
2. slave端
spider文件
import re
from scrapy_redis.spiders import RedisSpider
from scrapy.http import Request
from slave.items import SlaveItem
class myspider(RedisSpider):
name = 'slave'
item = SlaveItem()
redis_key = 'myredis:start_urls'
def parse(self, response):
item = self.item
item['title'] = response.xpath('//div[@class="house-title"]/h1/text()').extract()[0]
item['price'] = response.xpath('//div[@id="generalSituation"]//li[1]/span[2]/text()').extract()[0]
item['type'] = response.xpath("//div[@id='generalSituation']//li[2]/span[2]/text()").extract()[0]
item['area'] = response.xpath("//div[@id='generalSituation']//li[3]/span[2]/text()").extract()[0]
item['direct'] = response.xpath("//div[@id='generalSituation']//li[4]/span[2]/text()").extract()[0]
item['floor'] = response.xpath(
"//div[@id='generalSituation']//ul[@class='general-item-right']/li[1]/span[2]/text()").extract()[0]
item['decorat'] = response.xpath(
"//div[@id='generalSituation']//ul[@class='general-item-right']/li[2]/span[2]/text()").extract()[0]
item['start'] = response.xpath(
"//div[@id='generalSituation']//ul[@class='general-item-right']/li[4]/span[2]/text()").extract()[0]
item['village'] = response.xpath("string(/html/body/div[4]/div[2]/div[2]/ul/li[1]/span[2])").extract()[0]
item['position'] = response.xpath("string(/html/body/div[4]/div[2]/div[2]/ul/li[2]/span[2])").extract()[0]
item['phone'] = response.xpath("//div[@id='houseChatEntry']//p[@class='phone-num']/text()").extract()[0]
txt = response.xpath("/html/head/script[1]/text()").extract()[0]
pattern = re.compile(".*?____json4fe.brokerUrl = '(.*?)';.*?", re.S)
result = re.findall(pattern, txt)
yield Request("http://" + result[0], callback=self.get_user)
def get_user(self, response):
item = self.item
item['user'] = response.xpath("/html/body/div[2]/div[2]/div[1]/div[1]/div/div[1]/text()").extract()[0]
return item
这里不在讲解xpath的用法,告诉你们一个好方法,浏览器F12选中一个元素,右键->复制->xpath。用redis_key代替start_urls,‘myredis:start_urls’为redis数据库db0的键,就是我们master端保存的,直接使用默认回调函数parse。
item文件
import scrapy
class SlaveItem(scrapy.Item):
title = scrapy.Field()
price = scrapy.Field()
type = scrapy.Field()
area = scrapy.Field()
direct = scrapy.Field()
floor = scrapy.Field()
decorat = scrapy.Field()
start = scrapy.Field()
village = scrapy.Field()
position = scrapy.Field()
user = scrapy.Field()
phone = scrapy.Field()
pass
middleware文件(同master端)
pipeline文件
import pymongo as pm
host = 'localhost'
port = 27017
client = pm.MongoClient(host,port)
db = client.demo.tongcheng
class SlavePipeline(object):
def process_item(self, item, spider):
db.insert(dict(item))
保存到mongodb数据库
setting文件
# 启用Redis调度存储请求队列
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
#不清除Redis队列、这样可以暂停/恢复 爬取
# SCHEDULER_PERSIST = True
# 确保所有的爬虫通过Redis去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
#指定用于连接redis的URL(可选)
#如果设置此项,则此项优先级高于设置的REDIS_HOST 和 REDIS_PORT
REDIS_URL = 'redis://123456@127.0.0.1:6379/'
BOT_NAME = 'slave'
SPIDER_MODULES = ['slave.spiders']
NEWSPIDER_MODULE = 'slave.spiders'
ROBOTSTXT_OBEY = False
DOWNLOADER_MIDDLEWARES = {
'slave.middlewares.UserAgentMiddleware': 543,
}
ITEM_PIPELINES = {
'slave.pipelines.SlavePipeline': 300,
}
加入和替换配置
结果
redis数据库
mongodb数据库
最后
本人也是小白,写的粗糙,有不懂或见解的地方一起交流。
ps:本文的github地址:https://github.com/tchtsm/scrapy