从零开始完成一个分布式爬虫项目

写在最前面:

学习scrapy也有一段时间了,准备写一个项目巩固巩固;也快要毕业了,毕业设计题目还没想好;索性先拿这个项目练练手。

ps:源码在文章末尾,有兴趣的自行下载


废话不多说,直接上任务

  1. 爬取网站 :房天下
  2. 爬取内容:各个省市所有的新房、二手房的信息
  3. 爬取策略:分布式爬取(会先从单机开始,之后再改成分布式)
  4. 存储位置:存储在 MongoDB上(有时间,会考虑使用集群)
  5. 数据分析:对爬取下的数据进行分析,如哪个省、市平均房价等等(有时间可以做做)
  6. 数据可视化:使用pyecharts或者自带的matplotlib(有时间做做)
  7. 待定…

ps: 这一套下来,我觉得做一个毕设应该没问题。


一.分析网站

  • 想想要获取所有的省市的房子信息,前提是要知道所有的省市具体名称。幸运的是,还真的有这个页面。


在更多城市那, 点击进去,是一个完整的省市信息。如下图:

ok,没毛病了。爬取这个页面所有的城市的链接,第一级链接就搞到手了。

还没完,接着分析:

以福建省福州市为例子,进入第一级链接。观察URL发现,新房的URL为:http://fz.newhouse.fang.com/house/s/,二手房的URL为http://fz.esf.fang.com/;发现,貌似有点意思。再换另一个城市,如厦门的新房和二手房URL分别是:

http://xm.newhouse.fang.com/house/s/http://xm.esf.fang.com/,这下,规律就出来了吧。不过,有些坑需要注意一下,北京,它的新房和二手房URL并不是 http://bj.esf.fang.com/,这样的,URL竟然是这样的:http://newhouse.fang.com/house/s/https://esf.fang.com/,毕竟它是首都嘛,特殊一点没毛病。

ok,页面分析差不多了,现在看看,需要获得到哪些字段。

根据自己需求来获取,使用xpath进行解析。

给个参考:

class NewHouseItem(scrapy.Item):
    # 省份
    province = scrapy.Field()
    # 城市
    city = scrapy.Field()
    # 小区名字
    name = scrapy.Field()
    # 价格
    price = scrapy.Field()
    # 几居
    rooms = scrapy.Field()
    # 地址
    address = scrapy.Field()
    # 行政区
    district = scrapy.Field()
    # 是否在售
    is_sale = scrapy.Field()
    # 详情页面 url
    orgin_url = scrapy.Field()
class OldHouseItem(scrapy.Item):
    # 省份
    province = scrapy.Field()
    # 城市
    city = scrapy.Field()
    # 小区名字
    name = scrapy.Field()
    # 几室几厅
    rooms = scrapy.Field()
    # 几层
    floor = scrapy.Field()
    # 朝向
    toward = scrapy.Field()
    # 年代
    year = scrapy.Field()
    # 地址
    address = scrapy.Field()
    # 建筑面积
    area = scrapy.Field()
    # 单价
    unit_price = scrapy.Field()
    # 总价
    total_price = scrapy.Field()

难得最近勤快些,我就重新走一遍流程吧。

一. 创建项目

  • 一步步来吧,先创建单机版本的

    scrapy startproject FangTianXinSingle
    
  • Pycharm打开,看到以下目录结构

是不是感觉有点不对劲?别慌,多出来的utils和log是我手动创建的 😃,utils主要是工具类,之后会使用到IP代理池,一些接口会放在那,log也就是日志目录。

别急着创建爬虫,先改改settings.py中的配置文件吧。

  • ROBOTSTXT_OBEY = False (不解释)
  • DOWNLOAD_DELAY = 5(防止IP被封了)
  • COOKIES_ENABLED = False (推荐不使用cookie)
  • 至于USER-AGENT可设可不设,因为下面会写一个UA中间件
  • 其它的先不改

二. 创建爬虫

先进入spiders目录中,cd spiders
控制台执行 scrapy genspider soufang fang.com

如图:

  • 首先,把 start_urls 给改了,换成之前分析的那个url http://www.fang.com/SoufunFamily.htm

  • 其次,别忘记我们的需求,要获得所有的 城市的链接。分析页面,使用xpath,解析出所有的链接。

    通过检查发现,其所有的城市是放在一个 id=c02 的div容器中,这下好办了直接上代码。懒癌犯了,就不一一分析xpath语法了。

# -*- coding: utf-8 -*-
import scrapy

class SoufangSpider(scrapy.Spider):
    name = 'soufang'
    allowed_domains = ['fang.com']
    start_urls = ['https://www.fang.com/SoufunFamily.htm']

    def parse(self, response):
        trs = response.xpath("//div[@id='c02']//tr")
        province = None
        for tr in trs:
            tds = tr.xpath(".//td[not(@class)]")
            province_td = tds[0].xpath(".//text()").get().strip()
            if province_td:
                province = province_td
            city_links = tds[1].xpath(".//a")
            # 不需要爬取国外的
            if province == '其它':
                continue
            for city_link in city_links:
                city_name = city_link.xpath(".//text()").get()
                city_url = city_link.xpath(".//@href").get()
                scheme, domain = city_url.split("//")
                # 北京的新房和二手房链接需要特别处理
                if 'bj.' in domain:
                    newHouseLink = 'http://newhouse.fang.com/house/s'
                    oldHouseLink = 'http://esf.fang.com/'
                else:
                    newHouseLink = scheme + "//" + "newhouse." + domain + "house/s"
                    oldHouseLink = scheme + "//" + "esf." + domain
                print(newHouseLink, oldHouseLink)

执行该爬虫,看看成效:

scrapy crawl soufang

如果你能打印出以下内容,恭喜你
在这里插入图片描述

三. 对这些链接再次访问之前,要做一些反爬虫策略了。

由于刚刚只对该网站的一个页面进行爬取,做不做反爬虫无所谓。但是接下去需要对网站这么网址进行爬取,不做点什么好像对不起这个网站

策略1:UA中间件

在目录下创建UserAgentMiddleware.py文件,我一般不习不在scrapy提供的middlewares.py文件中写


# author:dayin
# Date:2019/12/18 0018

from fake_useragent import UserAgent


class UserAgentMiddleware(object):
    ua = UserAgent()

    def process_request(self, request, spider):
        request.headers['User-Agent'] = self.ua.random

别忘记了!要去settings文件中 downloadmiddleware中加入你创建的中间件!

DOWNLOADER_MIDDLEWARES = {
   'FangTianXiaSingle.UserAgentMiddleware.UserAgentMiddleware': 543,
}

策略2:使用IP代理池

关于IP代理池可以说的有很多。。我简略的说一下

  • 考虑到 经济基础 (我是个穷学生),我还是老老实实的使用一些免费的代理IP吧。

  • github上有很多开源的免费IP代理,我找了一个挺不错的,分享给大家。

    代理IP池

  • 关于代理IP配置

    根据他给的文档即可,写的很详细。里面可以使用redis和ssdb数据库,我使用的是redis数据库。

直接贴出我的代码:

在 utils中创建 getProxyIP.py文件

# author:dayin
# Date:2019/12/18 0016
import requests
def get_proxy():
    return 'http://' + requests.get("http://192.168.43.115:5555/get").json()['proxy']
def delete_proxy(proxy):
    if proxy:
        requests.get("http://192.168.43.115:5555/delete/?proxy={}".format(proxy))
if __name__ == '__main__':
    print(get_proxy())

这里解释一下,我没有使用该代理IP池自带的API接口,我是自己创建一个轻量的Flask框架。根据自己的需求,自定制一个api

工具有了,接下来就创建 代理中间件

在目录下,创建 ProxyMiddleWare.py文件

# author:dayin
# Date:2019/12/18 0018

# 设置IP代理池 中间件
from FangTianXiaSingle.utils.getProxyIP import *


class ProxyMiddleWare(object):

    def process_request(self, request, spider):
        try:
            proxy = get_proxy()
            request.meta['proxy'] = proxy
        except:
            # 有异常,使用本机IP进行爬取
            print('代理池里没有IP了....只能使用自己的啦')

    def process_exception(self, request, spider, exception):
        print('----' * 100)
        print("这个代理IP超时,把它删了吧,换下一个...")
        delete_proxy(request.meta.get('proxy'))
        request.meta['proxy'] = get_proxy()
        print('----' * 100)
        return request.replace(dont_filter=True)

这样就基本搞定了IP代理池了,少年,你可以放肆的去爬了。

PS,免费的代理毕竟不稳定,经常会连接不上。心累,没钱买好用的代理

对了,记得去settings.py文件中加入该中间件!

DOWNLOADER_MIDDLEWARES = {
    'FangTianXiaSingle.ProxyMiddleWare.ProxyMiddleWare': 300,
    'FangTianXiaSingle.UserAgentMiddleware.UserAgentMiddleware': 543,
}

四. 既然已经有了反爬虫策略,那么接下去便是分别解析各个城市二手房和新房的链接了

具体页面就不去分析了,给出代码,供大家参考:

# -*- coding: utf-8 -*-
import scrapy
import re

from FangTianXiaSingle.items import OldHouseItem, NewHouseItem


class SoufangSpider(scrapy.Spider):
    name = 'soufang'
    allowed_domains = ['fang.com']
    start_urls = ['https://www.fang.com/SoufunFamily.htm']

    def parse(self, response):
        trs = response.xpath("//div[@id='c02']//tr")
        province = None
        for tr in trs:
            tds = tr.xpath(".//td[not(@class)]")
            province_td = tds[0].xpath(".//text()").get().strip()
            if province_td:
                province = province_td
            city_links = tds[1].xpath(".//a")
            # 不需要爬取国外的
            if province == '其它':
                continue
            for city_link in city_links:
                city_name = city_link.xpath(".//text()").get()
                city_url = city_link.xpath(".//@href").get()
                scheme, domain = city_url.split("//")
                # 北京的新房和二手房链接需要特别处理
                if 'bj.' in domain:
                    newHouseLink = 'http://newhouse.fang.com/house/s'
                    oldHouseLink = 'http://esf.fang.com/'
                else:
                    newHouseLink = scheme + "//" + "newhouse." + domain + "house/s"
                    oldHouseLink = scheme + "//" + "esf." + domain

                yield scrapy.Request(url=newHouseLink, callback=self.parse_newhouse,
                                     meta={'info': (province, city_name)})

                yield scrapy.Request(url=oldHouseLink, callback=self.parse_oldhouse,
                                     meta={'info': (province, city_name)})

    def parse_newhouse(self, response):
        province, city = response.meta.get("info")
        lis = response.xpath(".//div[contains(@class,'nl_con')]/ul//li")
        for li in lis:
            # 房子的名称
            name = li.xpath(".//div[@class='nlcd_name']//text()").getall()
            if name:
                name = re.sub(r'[\s\n]', '', ''.join(name))
                # 价格
                price = li.xpath(".//div[@class='nhouse_price']//text()").getall()
                price = re.sub(r'[\s\n广告]', '', ''.join(price))
                # 居式
                rooms = li.xpath(".//div[contains(@class,'house_type')]//text()").getall()
                rooms = re.sub('-', '一共', re.sub(r'[\s\n]', '', ''.join(rooms)))
                # 地址
                address = li.xpath('.//div[@class="address"]/a/@title').get()
                address = re.sub(r'\[.+\]', '', address)
                # 地区
                district = li.xpath(".//div[@class='address']//text()").getall()
                try:
                    district = re.search(r'(\[.+\])', ''.join(district)).group(1)
                except:
                    district = ''
                # 是否在售
                is_sale = li.xpath(".//div[contains(@class,'fangyuan')]/span/text()").get()
                # 房源的链接
                orgin_url = response.urljoin(li.xpath(".//div[@class='nlcd_name']/a/@href").get())

                items = NewHouseItem(province=province, city=city, name=name, price=price, rooms=rooms, address=address,
                                     district=district,
                                     is_sale=is_sale, orgin_url=orgin_url)
                yield items

        next_url = response.xpath("//div[@class='page']//a[@class='next']/@href").get()
        if next_url:
            next_url = response.urljoin(next_url)
            yield scrapy.Request(next_url, callback=self.parse_newhouse, meta={'info': (province, city)})

    def parse_oldhouse(self, response):
        province, city = response.meta.get("info")
        lis = response.xpath("//div[contains(@class,'shop_list')]//dl[@dataflag='bg']")

        for li in lis:
            try:
                name = li.xpath(".//p[@class='add_shop']/a/@title").get()
                address = li.xpath(".//p[@class='add_shop']//span/text()").get()
                house_info = li.xpath(".//p[@class='tel_shop']//text()").getall()
                house_info = ''.join(house_info).split('|')
                house_info = list(map(lambda x: re.sub(r'[\r\n\s]', '', x), house_info))
                rooms, areas, floor, toward, year, *ags = house_info
                unit_price = li.xpath(".//dd[@class='price_right']/span[not(@class)]/text()").get()
                total_price = li.xpath(".//dd[@class='price_right']/span[@class='red']//text()").getall()
                total_price = ''.join(total_price)
                item = OldHouseItem(province=province, city=city, name=name, address=address, rooms=rooms, floor=floor,
                                    toward=toward, year=year, area=areas, unit_price=unit_price,
                                    total_price=total_price)
                yield item
            except:
                continue

        next_url = response.xpath("//div[@class='page_al']//span/following-sibling::p//a[text()='下一页']/@href").get()
        if next_url:
            yield scrapy.Request(response.urljoin(next_url), callback=self.parse_oldhouse,
                                 meta={'info': (province, city)})

页面信息获取并不难,只要掌握了xpath的语法规则,获取页面上的元素不是轻而易举?

最后,别忘记了。要在items.py文件中定义好字段噢

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy

class NewHouseItem(scrapy.Item):
    # 省份
    province = scrapy.Field()
    # 城市
    city = scrapy.Field()
    # 小区名字
    name = scrapy.Field()
    # 价格
    price = scrapy.Field()
    # 几居
    rooms = scrapy.Field()
    # 地址
    address = scrapy.Field()
    # 行政区
    district = scrapy.Field()
    # 是否在售
    is_sale = scrapy.Field()
    # 详情页面 url
    orgin_url = scrapy.Field()


class OldHouseItem(scrapy.Item):
    # 省份
    province = scrapy.Field()
    # 城市
    city = scrapy.Field()
    # 小区名字
    name = scrapy.Field()
    # 几室几厅
    rooms = scrapy.Field()
    # 几层
    floor = scrapy.Field()
    # 朝向
    toward = scrapy.Field()
    # 年代
    year = scrapy.Field()
    # 地址
    address = scrapy.Field()
    # 建筑面积
    area = scrapy.Field()
    # 单价
    unit_price = scrapy.Field()
    # 总价
    total_price = scrapy.Field()


五. 数据存储,将数据存储到MongoDB中

按照我的习惯,我不会在已经定义好的pipelines.py文件中创建,而是在目录下创建一个MongoPipeline.py文件

对了,最好把MongoDB的配置文件放在settings.py中,便于管理

# settings.py文件
# 设置Mongodb配置信息
MONGO_URI = '192.168.43.115'
MONGO_DB = 'FangTianXiaSingle'

代码如下:


# author:dayin
# Date:2019/12/18 0018

from pymongo import MongoClient

from FangTianXiaSingle.items import NewHouseItem, OldHouseItem


class MongoPipeline(object):

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DB')
        )

    def open_spider(self, spider):
        self.client = MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def process_item(self, item, spider):
        # 新房和二手房的数据分开存放
        if isinstance(item, NewHouseItem):
            self.db['newHouse'].insert(dict(item))
            print('[success] insert into the newHouse : ' + item.get('name') + ' to MongoDB')
        elif isinstance(item, OldHouseItem):
            self.db['oldHouse'].insert(dict(item))
            print('[success] insert into the oldHouse : ' + item.get('name') + ' to MongoDB')
        return item

    def close_spider(self, spider):
        self.client.close()

对咯,别忘记将它添加到settings.py文件中的ITEM_PIPELINES

ITEM_PIPELINES = {
    'FangTianXiaSingle.MongoPipeline.MongoPipeline': 300,
}

六. 关于优化

  • 优化1, 取消 重试中间件

    ‘scrapy.downloadermiddlewares.retry.RetryMiddleware’: None,’

  • 优化2,可以取消重定向中间件,自己写一个。

  • 优化3,可以设置超时下载时间。

    DOWNLOAD_TIMEOUT = 5


ok,单机版本的就基本上完成了。让我们看看效果

效果还行,几秒钟就爬了几千条数据。不过,这和分布式爬虫比起来,还是个弟弟。

单机版的完整代码,我已经上传到 github上 点击这里

有兴趣的可以去下载


接下来就是分布式爬虫了

这里,我不介绍原理概念什么的,相信有接触过 scrapy分布式爬虫的都门儿清。

说起来,单机版的能够成功爬取,那么只需要修改几个地方,就能轻松变成分布式爬虫,所以说啊,会写单机版本的爬虫才是王道

想必大家都懂,scrapy实现分布式爬虫,是基于 scrapy-redis,so,直接切入主题。我把修改的几个地方贴出来:

第一,在settings.py文件中增加 scrapy-redis配置

# 增加 Scrapy-redis配置

# 确保request存储到redis中
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 确保所有爬虫共享相同的去重指纹
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 可以实现暂停和恢复的功能
SCHEDULER_PERSIST = True

REDIS_HOST = '192.168.43.115'
REDIS_PORT = 6379
REDIS_PARAMS = {'password': 'chendayin'}

第二,修改soufang.py文件

主要改两个地方,第一个不在从scrapy.Spider中继承了,而是继承scrapy-redis中
RedisSpider,需要导入包:

from scrapy_redis.spiders import RedisSpider

第二个地方,去掉start_urls,增加 redis_key = ‘soufang’

完整代码如下:

import scrapy
import re

from FangTianXiaSingle.items import OldHouseItem, NewHouseItem
from scrapy_redis.spiders import RedisSpider

class SoufangSpider(RedisSpider):
    name = 'soufang'
    allowed_domains = ['fang.com']
    # start_urls = ['https://www.fang.com/SoufunFamily.htm']
    redis_key = 'soufang'
	

 	...中间省略...和之前单机版一样

第三,需要手动去redis数据库中创建一个列表,值为: https://www.fang.com/SoufunFamily.htm

lpush soufang https://www.fang.com/SoufunFamily.htm

只有这样,爬虫才会启动。否则一直阻塞

最后,你可以将项目拷贝到Linux服务器上,或者远程服务器上。启动它们,就完成了分布式的爬取。


待完善

  • 重定向中间件编写
  • MongoDB 集群
  • 数据分析处理
  • 数据可视化

ok,项目差不多就到这里了。有问题的,可以向我提问

项目源码在github上 -------> 点这里

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章