數據來源
數據頁面: 鏈家網南京(https://nj.lianjia.com/chengjiao/)
鏈家網數據量很大,這里只用南京的二手房成交數據。
如下圖:

數據采集
鏈家網的頁面數據比較整齊,采集很簡單,為了避免影響別人使用,只采集的南京的二手房成交數據, 采集頻率也很低,總共花了一下午才采集完所有數據。
我主要采集以下 9 個數據,沒有采集房屋的圖片。
- 小區名稱和房屋概要
- 房屋朝向和裝修情況
- 成交日期
- 成交價格(單位: 萬元)
- 樓層等信息
- 成交單價
- 房屋優勢
- 掛牌價格
- 成交周期
爬蟲技術爭議比較多,詳細的過程就不多說了,采集完的數據我放在以下地址:https://databook.top/data/b2b49fff-ede4-4ce5-9d96-08c616d1e481/detail
已經整理成 csv 格式,需要的可以下載了用來做數據分析實驗。(數據截止到 2021/03/30)
數據采集的注意點
鏈家網的數據采集有個注意的地方,雖然打開這個網頁(https://nj.lianjia.com/chengjiao/), 我們看到目前共找到 8 萬多套成交房源,
但是鏈家網只顯示 100 頁的數據,每頁 30 條,也就是最多一次查詢出 3000 條數據。
所以,為了采集所有的數據,需要設置多種檢索條件,保證每次搜索的數據不超過 3000 條。 8 萬多條數據大概要設置 30 來種不同的搜索條件。
如下圖,我主要根據區域,售價和戶型來檢索的,也就是按區域如果超過 3000,再按售價,售價還超出再按戶型。 用這 3 個條件基本就夠了。

數據清理
合並和去重
采集的數據是根據不同搜索條件來的,所以有很多個 csv 文件。 csv 格式是統一的,先用 shell 腳本進行數據的合並和去重,我是按照南京的不同的區來合並數據的。
采集的時候我已經按照不同的區把數據放在不同的文件夾了。合並數據腳本示例是如下:
d="merged-files"
sed "" 建鄴區/*.csv > ${d}/建鄴區.csv
這里合並用的 sed 命令,沒有用如下 cat 命令:
d="merged-files"
cat 建鄴區/*.csv > ${d}/建鄴區.csv
用 cat 命令有個問題,前一個文件的最后一行會和下一個文件的第一行合並成一行。
合並之后就是去重:假設第一步合並后的文件都在 merged-files 文件夾下
d="merged-files"
for f in `ls ${d}/`
do
sort -u ${d}/${f} -o uniq-${f}
done
格式化
采集到的原始數據是如下格式:
一品驪城 2室1廳 71平米,南 | 精裝,2020.09.05,78,中樓層(共5層) 板樓,10916元/平,,掛牌82萬,成交周期134天
可以看出,除了成交價(78)是正常的數字,單價(10916 元/平),掛牌價(掛牌 82 萬),成交周期(成交周期 134 天)等都是數字和文字混合。 這些字段需要將數字剝離出來才能進行后續的分析。
我是通過一個簡單的 golang 程序來格式化原始數據,然后生成新的 csv。
func handleData(line []string) TradedHouse {
var houseData TradedHouse
fmt.Printf("record: %v\n", line)
// 1. 小區名稱和房屋概要
var arr = strings.Split(line[0], " ")
houseData.Name = arr[0]
houseData.HouseType = arr[1]
if len(arr) > 2 {
houseData.HouseArea = gutils.ParseFloat64WithDefault(strings.TrimRight(arr[2], "平米"), 0.0)
}
// 2. 房屋朝向和裝修情況
arr = strings.Split(line[1], " | ")
houseData.HouseDirection = arr[0]
houseData.HouseDecoration = arr[1]
// 3. 成交日期
houseData.TradingTime = line[2]
// 4. 成交價格(單位: 萬元)
houseData.TradingPrice = gutils.ParseFloat64WithDefault(line[3], 0.0)
// 5. 樓層等信息
houseData.FloorInfo = line[4]
// 6. 成交單價
houseData.UnitPrice = gutils.ParseFloat64WithDefault(strings.TrimRight(line[5], "元/平"), 0.0)
// 7. 房屋優勢
houseData.Advance = line[6]
// 8. 掛牌價格
if len(line) > 7 {
houseData.ListedPrice = gutils.ParseFloat64WithDefault(strings.TrimRight(strings.TrimLeft(line[7], "掛牌"), "萬"), 0.0)
}
// 9. 成交周期
if len(line) > 8 {
houseData.SellingTime, _ = strconv.Atoi(strings.TrimRight(strings.TrimLeft(line[8], "成交周期"), "天"))
}
return houseData
}
轉換后的 csv 格式如下:
一品驪城,2室1廳,精裝,中樓層(共5層) 板樓,71,10916,82,78,134,2020.09.05,南,
數值部分都分離出來了,可以進入數據分析的步驟了。
數據分析
最后的分析步驟使用的 python 腳本,主要使用 python 的 numpy 和 pandas 庫。
下面分析了 2019~2020 南京各區二手房的每個月的銷售套數,成交總額以及成交單價。
銷售套數
# -*- coding: utf-8 -*-
import os
import numpy as np
import pandas as pd
def read_csv(fp):
# 讀取2列 col9: 成交時間
# 其中成交時間進行處理:從 2020.01.01 ==> 2020.01
data = pd.read_csv(
fp,
usecols=[9],
header=None,
names=["time"],
converters={"time": lambda s: s[:7]},
)
data_mask = data["time"].str.contains("2019|2020")
data = data[data_mask]
data["count"] = 1
return data.groupby("time")
def write_csv(fp, data):
data.to_csv(fp)
def main():
# 讀取csv數據
csv_path = "../liangjia-go/output/converter"
output_path = "./成交數量統計.csv"
files = list(
map(
lambda f: os.path.join(csv_path, f + ".csv"),
[
"南京鼓樓區",
"南京建鄴區",
"南京江寧區",
"南京溧水區",
"南京六合區",
"南京浦口區",
"南京棲霞區",
"南京秦淮區",
"南京玄武區",
"南京雨花台區",
],
)
)
allData = None
for f in files:
data = read_csv(f)
data = data.sum()
data["area"] = os.path.basename(f).strip(".csv").strip("南京")
print(data)
if allData is None:
allData = data
else:
allData = allData.append(data)
write_csv(output_path, allData)
if __name__ == "__main__":
main()
成交總額
# -*- coding: utf-8 -*-
import os
import numpy as np
import pandas as pd
def read_csv(fp):
# 讀取2列 col9: 成交時間, col7: 成交價格(萬元)
# 其中成交時間進行處理:從 2020.01.01 ==> 2020.01
data = pd.read_csv(
fp,
usecols=[7, 9],
header=None,
names=["value", "time"],
converters={"time": lambda s: s[:7]},
)
data_mask = data["time"].str.contains("2019|2020")
data = data[data_mask]
return data.groupby("time")
def write_csv(fp, data):
data.to_csv(fp)
def main():
# 讀取csv數據,提取成交價格(col 7)
csv_path = "../liangjia-go/output/converter"
output_path = "./成交額統計.csv"
files = list(
map(
lambda f: os.path.join(csv_path, f + ".csv"),
[
"南京鼓樓區",
"南京建鄴區",
"南京江寧區",
"南京溧水區",
"南京六合區",
"南京浦口區",
"南京棲霞區",
"南京秦淮區",
"南京玄武區",
"南京雨花台區",
],
)
)
allData = None
for f in files:
data = read_csv(f)
data = data.sum()
data["area"] = os.path.basename(f).strip(".csv").strip("南京")
print(data)
if allData is None:
allData = data
else:
allData = allData.append(data)
# 萬元 => 元
allData["value"] = allData["value"] * 10000
write_csv(output_path, allData)
if __name__ == "__main__":
main()
成交單價
# -*- coding: utf-8 -*-
import os
import numpy as np
import pandas as pd
def read_csv(fp):
# 讀取2列 col9: 成交時間, col5: 成交單價(元/平米)
# 其中成交時間進行處理:從 2020.01.01 ==> 2020.01
data = pd.read_csv(
fp,
usecols=[5, 9],
header=None,
names=["value", "time"],
converters={"time": lambda s: s[:7]},
)
data_mask = data["time"].str.contains("2019|2020")
data = data[data_mask]
return data.groupby("time")
def write_csv(fp, data):
data.to_csv(fp)
def main():
# 讀取csv數據,提取成交價格(col 7)
csv_path = "../liangjia-go/output/converter"
output_path = "./成交單價統計.csv"
files = list(
map(
lambda f: os.path.join(csv_path, f + ".csv"),
[
"南京鼓樓區",
"南京建鄴區",
"南京江寧區",
"南京溧水區",
"南京六合區",
"南京浦口區",
"南京棲霞區",
"南京秦淮區",
"南京玄武區",
"南京雨花台區",
],
)
)
allData = None
for f in files:
data = read_csv(f)
data = data.mean()
data["area"] = os.path.basename(f).strip(".csv").strip("南京")
print(data)
if allData is None:
allData = data
else:
allData = allData.append(data)
write_csv(output_path, allData)
if __name__ == "__main__":
main()
分析結果展示
分析后生成的 csv,我寫了另外一個工具,可以直接轉換成小視頻。
工具是基於 antv G2 和 ffmpeg 做的,還不是很成熟,以后會發布到官網上,同時在博客中詳細介紹。
生成的視頻已經放在我的視頻號了,感興趣可以看看。

總結
雖然上面的數據量不是很大,但這是我平時做一次數據分析的的整個過程(從數據采集到可視化展示)。
- 采集的部分使用的方式比較雜,根據具體情況看,有時我用 python 或者 golang 寫爬蟲,有時用現成的工具,比如八爪魚之類的。
- 采集之后對數據的初步整理,我基本上是用 shell,強大的 shell 命令可以極大的減少代碼的編寫。
- 對數據的精細化整理,我一般用 golang,開發效率和執行效率都高且便於對接各種存儲(上面的例子只是簡單的生成 csv)。
- 數據的分析我一般用 python,這個不用多說了,現成的分析庫實在太強大。建議安裝 miniconda,我另一個博客有介紹:debian10下miniconda環境配置
- 最后的分析結果展示,也有很多現成的工具,我選擇了用 antv 家族的庫來自己實現(主要是想試試能不能做一些差異化的展示)。
