写在最前面:
学习scrapy也有一段时间了,准备写一个项目巩固巩固;也快要毕业了,毕业设计题目还没想好;索性先拿这个项目练练手。
ps:源码在文章末尾,有兴趣的自行下载
废话不多说,直接上任务
- 爬取网站 :房天下
- 爬取内容:各个省市所有的新房、二手房的信息
- 爬取策略:分布式爬取(会先从单机开始,之后再改成分布式)
- 存储位置:存储在 MongoDB上(有时间,会考虑使用集群)
- 数据分析:对爬取下的数据进行分析,如哪个省、市平均房价等等(有时间可以做做)
- 数据可视化:使用pyecharts或者自带的matplotlib(有时间做做)
- 待定…
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配置
根据他给的文档即可,写的很详细。里面可以使用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上 -------> 点这里