寫在最前面:
學習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上 -------> 點這裏