概述
最近在學習python的各種數據分析庫,為了嘗試各種庫中各種分析算法的效果,陸陸續續爬取了一些真實的數據來。
順便也練習練習爬蟲,踩了不少坑,后續將采集的經驗逐步分享出來,希望能給后來者一些參考,也希望能夠得到先驅者的指點!
采集工具
其實基本沒用過什么現成的采集工具,都是自己通過編寫代碼來采集,雖然耗費一些時間,但是感覺靈活度高,可控性強,遇到問題時解決的方法也多。
一般根據網站的情況,如果提供API最好,直接寫代碼通過訪問API來采集數據。
如果沒有API,就通過解析頁面(html)來獲取數據。
本次采集的數據是鏈家網上的成交數據,因為是學習用,所以不會去大規模的采集,只采集了南京各個區的成交數據。
采集使用puppeteer庫,Puppeteer 是一個 Node 庫,它提供了高級的 API 並通過 DevTools 協議來控制 Chrome(或Chromium)。
通俗來說就是一個 headless chrome 瀏覽器: https://github.com/puppeteer/puppeteer
通過 puppeteer,可以模擬網頁的手工操作方式,也就是說,理論上,能通過瀏覽器正常訪問看到的內容就能采集到。
采集過程
其實數據采集的代碼並不復雜,時間主要花在頁面的分析上了。
鏈家網的成交數據不用登錄也可以訪問,這樣就省了很多的事情。
只要找出南京市各個區的成交數據頁面的URL,然后訪問就行。
頁面分析
下面以棲霞區的成交頁面為例,分析我們可能需要的數據。
頁面URL: https://nj.lianjia.com/chengjiao/qixia/
根據頁面,可以看出重復的主要是紅框內的數據,其中銷售人員的姓名涉及隱私,我們不去采集。
采集的數據分類為:(有的戶型可能沒有下面列的那么全,缺少房屋優勢字段,甚至成交價格字段等等)
- name: 小區名稱和房屋概要,比如:新城香悅瀾山 3室2廳 87.56平米
- houseInfo: 房屋朝向和裝修情況,比如:南 北 | 精裝
- dealDate: 成交日期,比如:2021.06.14
- totalPrice: 成交價格(單位: 萬元),比如:338萬
- positionInfo: 樓層等信息,比如:中樓層(共5層) 2002年建塔樓
- unitPrice: 成交單價,比如:38603元/平
- advantage: 房屋優勢,比如:房屋滿五年
- listPrice: 掛牌價格,比如:掛牌341萬
- dealCycleDays: 成交周期,比如:成交周期44天
核心代碼
鏈家網上采集房產成交數據很簡單,我在采集過程中遇到的唯一的限制就是根據檢索條件,只返回100頁的數據,每頁30條。
也就是說,不管什么檢索條件,鏈家網只返回前3000條數據。
可能這也是鏈家網控制服務器訪問壓力的一個方式,畢竟如果是正常用戶訪問的話,一般也不會看3000條那么多,返回100頁數據綽綽有余。
為了獲取想要的數據,只能自己設計下檢索條件,保證每個檢索條件下的數據不超過3000條,最后自己合並左右的采集結果,去除重復數據。
這里,只演示如何采集數據,具體檢索條件的設計,有興趣根據自己需要的數據嘗試下即可,沒有統一的方法。
通過puppeteer采集數據,主要步驟很簡單:
- 啟動瀏覽器,打開頁面
- 解析當前頁面,獲取需要的數據(也就是上面列出的9個字段的數據)
- 進入下一頁
- 如果是最后一頁,則退出程序
- 如果不是最后一頁,進入步驟2
初始化並啟動頁面
import puppeteer from "puppeteer";
(async () => {
// 啟動頁面,得到頁面對象
const page = await startPage();
})();
// 初始化瀏覽器
const initBrowser = async () => {
const browser = await puppeteer.launch({
args: ["--no-sandbox", "--start-maximized"],
headless: false,
userDataDir: "./user_data",
ignoreDefaultArgs: ["--enable-automation"],
executablePath:
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
});
return browser;
};
// 啟動頁面
const startPage = async (browser) => {
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
return page;
};
采集數據
import puppeteer from "puppeteer";
(async () => {
// 啟動頁面,得到頁面對象
const page = await startPage();
// 采集數據
await nanJin(page);
})();
const mapAreaPageSize = [
// { url: "https://nj.lianjia.com/chengjiao/gulou", name: "gulou", size: 2 }, // 測試用
{ url: "https://nj.lianjia.com/chengjiao/gulou", name: "gulou", size: 30 },
{ url: "https://nj.lianjia.com/chengjiao/jianye", name: "jianye", size: 20 },
{
url: "https://nj.lianjia.com/chengjiao/qinhuai",
name: "qinhuai",
size: 29,
},
{ url: "https://nj.lianjia.com/chengjiao/xuanwu", name: "xuanwu", size: 14 },
{
url: "https://nj.lianjia.com/chengjiao/yuhuatai",
name: "yuhuatai",
size: 14,
},
{ url: "https://nj.lianjia.com/chengjiao/qixia", name: "qixia", size: 14 },
{
url: "https://nj.lianjia.com/chengjiao/jiangning",
name: "jiangning",
size: 40,
},
{ url: "https://nj.lianjia.com/chengjiao/pukou", name: "pukou", size: 25 },
{ url: "https://nj.lianjia.com/chengjiao/liuhe", name: "liuhe", size: 4 },
{ url: "https://nj.lianjia.com/chengjiao/lishui", name: "lishui", size: 4 },
];
// 南京各區成交數據
const nanJin = async (page) => {
for (let i = 0; i < mapAreaPageSize.length; i++) {
const areaLines = await nanJinArea(page, mapAreaPageSize[i]);
// 分區寫入csv
await saveContent(
`./output/lianjia`,
`${mapAreaPageSize[i].name}.csv`,
areaLines.join("\n")
);
}
};
const nanJinArea = async (page, m) => {
let areaLines = [];
for (let i = 1; i <= m.size; i++) {
await page.goto(`${m.url}/pg${i}`);
// 等待頁面加載完成,這是顯示總套數的div
await page.$$("div>.total.fs");
await mouseDown(page, 800, 10);
// 解析頁面內容
const lines = await parseLianjiaData(page);
areaLines = areaLines.concat(lines);
// 保存頁面內容
await savePage(page, `./output/lianjia/${m.name}`, `page-${i}.html`);
}
return areaLines;
};
// 解析頁面內容
// 1. name: 小區名稱和房屋概要
// 2. houseInfo: 房屋朝向和裝修情況
// 3. dealDate: 成交日期
// 4. totalPrice: 成交價格(單位: 萬元)
// 5. positionInfo: 樓層等信息
// 6. unitPrice: 成交單價
// 7. advantage: 房屋優勢
// 8. listPrice: 掛牌價格
// 9. dealCycleDays: 成交周期
const parseLianjiaData = async (page) => {
const listContent = await page.$$(".listContent>li");
let lines = [];
for (let i = 0; i < listContent.length; i++) {
try {
const name = await listContent[i].$eval(
".info>.title>a",
(node) => node.innerText
);
const houseInfo = await listContent[i].$eval(
".info>.address>.houseInfo",
(node) => node.innerText
);
const dealDate = await listContent[i].$eval(
".info>.address>.dealDate",
(node) => node.innerText
);
const totalPrice = await listContent[i].$eval(
".info>.address>.totalPrice>.number",
(node) => node.innerText
);
const positionInfo = await listContent[i].$eval(
".info>.flood>.positionInfo",
(node) => node.innerText
);
const unitPrice = await listContent[i].$eval(
".info>.flood>.unitPrice>.number",
(node) => node.innerText + "元/平"
);
let advantage = "";
try {
advantage = await listContent[i].$eval(
".info>.dealHouseInfo>.dealHouseTxt>span",
(node) => node.innerText
);
} catch (err) {
console.log("err is ->", err);
advantage = "";
}
const [listPrice, dealCycleDays] = await listContent[i].$$eval(
".info>.dealCycleeInfo>.dealCycleTxt>span",
(nodes) => nodes.map((n) => n.innerText)
);
console.log("name: ", name);
console.log("houseInfo: ", houseInfo);
console.log("dealDate: ", dealDate);
console.log("totalPrice: ", totalPrice);
console.log("positionInfo: ", positionInfo);
console.log("unitPrice: ", unitPrice);
console.log("advantage: ", advantage);
console.log("listPrice: ", listPrice);
console.log("dealCycleDays: ", dealCycleDays);
lines.push(
`${name},${houseInfo},${dealDate},${totalPrice},${positionInfo},${unitPrice},${advantage},${listPrice},${dealCycleDays}`
);
} catch (err) {
console.log("數據解析失敗:", err);
}
}
return lines;
};
我是把要采集的頁面列在 const mapAreaPageSize
這個變量中,其中 url
是頁面地址,size
是訪問多少頁(根據需要,並不是每個檢索條件都要訪問100頁)。
采集數據的核心在 parseLianjiaData
函數中,通過 chrome 瀏覽器的debug模式,找到每個數據所在的頁面位置。
puppeteer提供強大的html 選擇器功能,通過html元素的 id
和 class
可以很快定位數據的位置(如果用過jQuery,很容易就能上手)。
這樣,可以避免寫復雜的正則表達式,提取數據更方便。
采集之后,我最后將數據輸出成 csv
格式。
注意事項
爬取數據只是為了研究學習使用,本文中的代碼遵守:
- 如果網站有 robots.txt,遵循其中的約定
- 爬取速度模擬正常訪問的速率,不增加服務器的負擔
- 只獲取完全公開的數據,有可能涉及隱私的數據絕對不碰