爬取兩萬多租房數據,告訴你廣州房租現狀(4)

概述

  • 前言
  • 統計結果
  • 爬蟲代碼實現
  • 爬蟲分析實現
  • 後記

前言

建議在看這篇文章之前,請看完這三篇文章,因為本文是依賴於前三篇文章的:

爬蟲利器初體驗(1)

聽說你的爬蟲又被封了?(2)

爬取數據不保存,就是耍流氓(3)

八月份的時候,由於腦洞大開,決定用 python 爬蟲爬取了深圳的租房數據,並寫了文章《用Python告訴你深圳房租有多高》,文章得到了一致好評和眾多轉載。由於我本身的朋友圈大多都在廣州、深圳,因此,早就有挺多小夥伴叫我分析一下廣州的租房價格現狀,這不,文章就這樣在眾多呼聲中出爐了。然後,此次爬蟲技術也升級了,完善了更多細節。源碼值得細細探究。此次分析採集了廣州 11 個區,23339 條數據,如下圖:

爬取兩萬多租房數據,告訴你廣州房租現狀(4)

樣本數據

其中後半部分地區數據量偏少,是由於該區房源確實不足。因此,此次調查也並非非常準確,權且當個娛樂項目,供大家觀賞。

統計結果

我們且先看統計結果,然後再看技術分析。

廣州房源分佈:(按區劃分)

其中天河佔據了大部分房源。但這塊地的房租可是不菲啊。

爬取兩萬多租房數據,告訴你廣州房租現狀(4)

房源分佈

房租單價:(每月每平方米單價 -- 平均數)

即是 1 平方米 1 個月的價格。方塊越大,代表價格越高。

爬取兩萬多租房數據,告訴你廣州房租現狀(4)

房租單價:平方米/月

可以看出天河、越秀、海珠都越過了 50 大關,分別是 75.042 、64.249、59.621 ,是其他地區的幾倍。如果在天河租個 20 平方的房間:

75.042 x 20 = 1500.84

再來個兩百的水電、物業:

1500.84 + 200 = 1700.84

我們按正常生活來算的話,每天早餐 10 塊,中午 15 塊,晚飯 15 塊:

1700.84 + 40 x 30 = 2700.84

那麼平時的日常生活需要 2700.84 塊。

隔斷時間下個館子,每個月買些衣服,交通費,談個女朋友,與女朋友出去逛街,妥妥滴加個 2500

2700.84 + 2500 = 5200.84

給爸媽一人一千:

5200.84 + 2000 = 7200.84

月薪一萬還是有點存款的,比深圳好一點,但是可能廣州的薪資就沒深圳那麼高了。

房租單價:(每日每平方米單價 -- 平均數)

即是 1 平方米 1 天的價格。

爬取兩萬多租房數據,告訴你廣州房租現狀(4)

租房單價:平方米/日

哈哈,感受一下寸土寸金的感覺。[捂臉]


爬取兩萬多租房數據,告訴你廣州房租現狀(4)


崩潰

戶型

戶型主要以 3 室 2 廳與 2 室 2 廳為主。與小夥伴抱團租房是最好的選擇了,不然與不認識的人一起合租,可能會發生一系列讓你不舒服的事情。字體越大,代表戶型數量越多。

爬取兩萬多租房數據,告訴你廣州房租現狀(4)

戶型

爬取兩萬多租房數據,告訴你廣州房租現狀(4)

戶型

租房面積統計

其中 30 - 90 平方米的租房佔大多數,如今之計,也只能是幾個小夥伴一起租房,抱團取暖了。

爬取兩萬多租房數據,告訴你廣州房租現狀(4)

租房面積統計

租房描述詞雲

這是爬取的租房描述,其中字體越大,標識出現的次數越多。其中【住家、全套、豪華、齊全】佔據了很大的部分,說明配套設施都是挺齊全的。

爬取兩萬多租房數據,告訴你廣州房租現狀(4)

租房描述

爬蟲技術分析

  • 請求庫:scrapy、requests
  • HTML 解析:BeautifulSoup
  • 詞雲:wordcloud
  • 數據可視化:pyecharts
  • 數據庫:MongoDB
  • 數據庫連接:pymongo

爬蟲代碼實現

跟上一篇文章不一樣,這是使用了 scrapy 爬蟲框架來爬取數據,各個方面也進行了優化,例如:自動生成各個頁面的地址。

由於房某下各個區域的首頁地址和首頁以外的地址的形式是不一樣的,但是又一定的規律,所以需要拼接各個部分的地址。

首頁地址案例:

# 第一頁
http://gz.zu.fang.com/house-a073/

非首頁地址:

# 第二頁
http://gz.zu.fang.com/house-a073/i32/
# 第三頁
http://gz.zu.fang.com/house-a073/i33/
# 第四頁
http://gz.zu.fang.com/house-a073/i34/


爬取兩萬多租房數據,告訴你廣州房租現狀(4)


爬取兩萬多租房數據,告訴你廣州房租現狀(4)

先解析首頁 url

def head_url_callback(self, response):
soup = BeautifulSoup(response.body, "html5lib")
dl = soup.find_all("dl", attrs={"id": "rentid_D04_01"}) # 獲取各地區的 url 地址的 dl 標籤
my_as = dl[0].find_all("a") # 獲取 dl 標籤中所有的 a 標籤,
for my_a in my_as:
if my_a.text == "不限": # 不限地區的,特殊處理
self.headUrlList.append(self.baseUrl)
self.allUrlList.append(self.baseUrl)
continue
if "周邊" in my_a.text: # 清除周邊地區的數據
continue
# print(my_a["href"])
# print(my_a.text)
self.allUrlList.append(self.baseUrl + my_a["href"])
self.headUrlList.append(self.baseUrl + my_a["href"])
print(self.allUrlList)
url = self.headUrlList.pop(0)
yield Request(url, callback=self.all_url_callback, dont_filter=True)

再解析非首頁 url

這裡先獲取到各個地區一共有多少頁,才能拼接具體的頁面地址。

爬取兩萬多租房數據,告訴你廣州房租現狀(4)

# 再根據頭部 url 拼接其他頁碼的url
def all_url_callback(self, response): # 解析並拼接所有需要爬取的 url 地址
soup = BeautifulSoup(response.body, "html5lib")
div = soup.find_all("div", attrs={"id": "rentid_D10_01"}) # 獲取各地區的 url 地址的 dl 標籤
span = div[0].find_all("span") # 獲取 dl 標籤中所有的 span 標籤,
span_text = span[0].text
for index in range(int(span_text[1:len(span_text) - 1])):
if index == 0:
pass
# self.allUrlList.append(self.baseUrl + my_a["href"])
else:
if self.baseUrl == response.url:
self.allUrlList.append(response.url + "house/i3" + str(index + 1) + "/")
continue
self.allUrlList.append(response.url + "i3" + str(index + 1) + "/")
if len(self.headUrlList) == 0:
url = self.allUrlList.pop(0)
yield Request(url, callback=self.parse, dont_filter=True)
else:
url = self.headUrlList.pop(0)
yield Request(url, callback=self.all_url_callback, dont_filter=True)

最後解析一個頁面的數據

def parse(self, response): # 解析一個頁面的數據
self.logger.info("==========================")
soup = BeautifulSoup(response.body, "html5lib")
divs = soup.find_all("dd", attrs={"class": "info rel"}) # 獲取需要爬取得 div
for div in divs:
ps = div.find_all("p")
try: # 捕獲異常,因為頁面中有些數據沒有被填寫完整,或者被插入了一條廣告,則會沒有相應的標籤,所以會報錯
for index, p in enumerate(ps): # 從源碼中可以看出,每一條 p 標籤都有我們想要的信息,故在此遍歷 p 標籤,

text = p.text.strip()
print(text) # 輸出看看是否為我們想要的信息
roomMsg = ps[1].text.split("|")
area = roomMsg[2].strip()[:len(roomMsg[2]) - 1]
item = RenthousescrapyItem()
item["title"] = ps[0].text.strip()
item["rooms"] = roomMsg[1].strip()
item["area"] = int(float(area))
item["price"] = int(ps[len(ps) - 1].text.strip()[:len(ps[len(ps) - 1].text.strip()) - 3])
item["address"] = ps[2].text.strip()
item["traffic"] = ps[3].text.strip()
if (self.baseUrl+"house/") in response.url: # 對不限區域的地方進行區分
item["region"] = "不限"
else:
item["region"] = ps[2].text.strip()[:2]
item["direction"] = roomMsg[3].strip()
print(item)
yield item
except:
print("糟糕,出現 exception")
continue
if len(self.allUrlList) != 0:
url = self.allUrlList.pop(0)
yield Request(url, callback=self.parse, dont_filter=True)

數據分析實現

這裡主要通過 pymongo 的一些聚合運算來進行統計,再結合相關的圖標庫,來進行數據的展示。

數據分析:

 # 求一個區的房租單價(平方米/元)
def getAvgPrice(self, region):
areaPinYin = self.getPinyin(region=region)
collection = self.zfdb[areaPinYin]
totalPrice = collection.aggregate([{'$group': {'_id': '$region', 'total_price': {'$sum': '$price'}}}])
totalArea = collection.aggregate([{'$group': {'_id': '$region', 'total_area': {'$sum': '$area'}}}])
totalPrice2 = list(totalPrice)[0]["total_price"]

totalArea2 = list(totalArea)[0]["total_area"]
return totalPrice2 / totalArea2
# 獲取各個區 每個月一平方米需要多少錢
def getTotalAvgPrice(self):
totalAvgPriceList = []
totalAvgPriceDirList = []
for index, region in enumerate(self.getAreaList()):
avgPrice = self.getAvgPrice(region)
totalAvgPriceList.append(round(avgPrice, 3))
totalAvgPriceDirList.append({"value": round(avgPrice, 3), "name": region + " " + str(round(avgPrice, 3))})
return totalAvgPriceDirList
# 獲取各個區 每一天一平方米需要多少錢
def getTotalAvgPricePerDay(self):
totalAvgPriceList = []
for index, region in enumerate(self.getAreaList()):
avgPrice = self.getAvgPrice(region)
totalAvgPriceList.append(round(avgPrice / 30, 3))
return (self.getAreaList(), totalAvgPriceList)
# 獲取各區統計樣本數量
def getAnalycisNum(self):
analycisList = []
for index, region in enumerate(self.getAreaList()):
collection = self.zfdb[self.pinyinDir[region]]
print(region)
totalNum = collection.aggregate([{'$group': {'_id': '', 'total_num': {'$sum': 1}}}])
totalNum2 = list(totalNum)[0]["total_num"]
analycisList.append(totalNum2)
return (self.getAreaList(), analycisList)
# 獲取各個區的房源比重
def getAreaWeight(self):
result = self.zfdb.rent.aggregate([{'$group': {'_id': '$region', 'weight': {'$sum': 1}}}])
areaName = []
areaWeight = []
for item in result:
if item["_id"] in self.getAreaList():
areaWeight.append(item["weight"])
areaName.append(item["_id"])
print(item["_id"])
print(item["weight"])
# print(type(item))
return (areaName, areaWeight)
# 獲取 title 數據,用於構建詞雲
def getTitle(self):
collection = self.zfdb["rent"]

queryArgs = {}
projectionFields = {'_id': False, 'title': True} # 用字典指定需要的字段
searchRes = collection.find(queryArgs, projection=projectionFields).limit(1000)
content = ''
for result in searchRes:
print(result["title"])
content += result["title"]
return content
# 獲取戶型數據(例如:3 室 2 廳)
def getRooms(self):
results = self.zfdb.rent.aggregate([{'$group': {'_id': '$rooms', 'weight': {'$sum': 1}}}])
roomList = []
weightList = []
for result in results:
roomList.append(result["_id"])
weightList.append(result["weight"])
# print(list(result))
return (roomList, weightList)
# 獲取租房面積
def getAcreage(self):
results0_30 = self.zfdb.rent.aggregate([
{'$match': {'area': {'$gt': 0, '$lte': 30}}},
{'$group': {'_id': '', 'count': {'$sum': 1}}}
])
results30_60 = self.zfdb.rent.aggregate([
{'$match': {'area': {'$gt': 30, '$lte': 60}}},
{'$group': {'_id': '', 'count': {'$sum': 1}}}
])
results60_90 = self.zfdb.rent.aggregate([
{'$match': {'area': {'$gt': 60, '$lte': 90}}},
{'$group': {'_id': '', 'count': {'$sum': 1}}}
])
results90_120 = self.zfdb.rent.aggregate([
{'$match': {'area': {'$gt': 90, '$lte': 120}}},
{'$group': {'_id': '', 'count': {'$sum': 1}}}
])
results120_200 = self.zfdb.rent.aggregate([
{'$match': {'area': {'$gt': 120, '$lte': 200}}},
{'$group': {'_id': '', 'count': {'$sum': 1}}}
])
results200_300 = self.zfdb.rent.aggregate([
{'$match': {'area': {'$gt': 200, '$lte': 300}}},
{'$group': {'_id': '', 'count': {'$sum': 1}}}
])
results300_400 = self.zfdb.rent.aggregate([
{'$match': {'area': {'$gt': 300, '$lte': 400}}},
{'$group': {'_id': '', 'count': {'$sum': 1}}}
])

results400_10000 = self.zfdb.rent.aggregate([
{'$match': {'area': {'$gt': 300, '$lte': 10000}}},
{'$group': {'_id': '', 'count': {'$sum': 1}}}
])
results0_30_ = list(results0_30)[0]["count"]
results30_60_ = list(results30_60)[0]["count"]
results60_90_ = list(results60_90)[0]["count"]
results90_120_ = list(results90_120)[0]["count"]
results120_200_ = list(results120_200)[0]["count"]
results200_300_ = list(results200_300)[0]["count"]
results300_400_ = list(results300_400)[0]["count"]
results400_10000_ = list(results400_10000)[0]["count"]
attr = ["0-30平方米", "30-60平方米", "60-90平方米", "90-120平方米", "120-200平方米", "200-300平方米", "300-400平方米", "400+平方米"]
value = [
results0_30_, results30_60_, results60_90_, results90_120_, results120_200_, results200_300_, results300_400_, results400_10000_
]
return (attr, value)

數據展示:

 # 展示餅圖
def showPie(self, title, attr, value):
from pyecharts import Pie
pie = Pie(title)
pie.add("aa", attr, value, is_label_show=True)
pie.render()
# 展示矩形樹圖
def showTreeMap(self, title, data):
from pyecharts import TreeMap
data = data
treemap = TreeMap(title, width=1200, height=600)
treemap.add("深圳", data, is_label_show=True, label_pos='inside', label_text_size=19)
treemap.render()
# 展示條形圖
def showLine(self, title, attr, value):
from pyecharts import Bar
bar = Bar(title)
bar.add("深圳", attr, value, is_convert=False, is_label_show=True, label_text_size=18, is_random=True,
# xaxis_interval=0, xaxis_label_textsize=9,
legend_text_size=18, label_text_color=["#000"])
bar.render()
# 展示詞雲
def showWorkCloud(self, content, image_filename, font_filename, out_filename):
d = path.dirname(__name__)

# content = open(path.join(d, filename), 'rb').read()
# 基於TF-IDF算法的關鍵字抽取, topK返回頻率最高的幾項, 默認值為20, withWeight
# 為是否返回關鍵字的權重
tags = jieba.analyse.extract_tags(content, topK=100, withWeight=False)
text = " ".join(tags)
# 需要顯示的背景圖片
img = imread(path.join(d, image_filename))
# 指定中文字體, 不然會亂碼的
wc = WordCloud(font_path=font_filename,
background_color='black',
# 詞雲形狀,
mask=img,
# 允許最大詞彙
max_words=400,
# 最大號字體,如果不指定則為圖像高度
max_font_size=100,
# 畫布寬度和高度,如果設置了msak則不會生效
# width=600,
# height=400,
margin=2,
# 詞語水平擺放的頻率,默認為0.9.即豎直襬放的頻率為0.1
prefer_horizontal=0.9
)
wc.generate(text)
img_color = ImageColorGenerator(img)
plt.imshow(wc.recolor(color_func=img_color))
plt.axis("off")
plt.show()
wc.to_file(path.join(d, out_filename))
# 展示 pyecharts 的詞雲
def showPyechartsWordCloud(self, attr, value):
from pyecharts import WordCloud
wordcloud = WordCloud(width=1300, height=620)
wordcloud.add("", attr, value, word_size_range=[20, 100])
wordcloud.render()

後記

距離上一篇租房市場的分析已經3、4 個月了,我的技術水平也得到了一定的提高。所以努力編碼才是成長的捷徑。最後,應對外界條件的變動,我們還是應該提升自己的硬實力,這樣才能提升自己的生存能力。後臺回覆【scrapy初體驗】獲取源碼。


分享到:


相關文章: