資料採集實戰(一)-- 鏈家網成交資料 (by puppeteer)
概述
最近在學習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,遵循其中的約定
- 爬取速度模擬正常訪問的速率,不增加伺服器的負擔
- 只獲取完全公開的資料,有可能涉及隱私的資料絕對不碰