在帝都打拼的小夥伴都知道,要租個合適的房子真心不易。中介要收一個月的房租作為中介費。而且很多黑中介打著租房的旗號各種坑蒙拐騙。要想在茫茫帖子中找到真正的房東,宛如大海撈針,同時需要和各路黑中介鬥智鬥勇。接下來就講講我浴血奮戰的故事。
那麼,How to start? 我們先選一塊陣地。58趕集這樣的網站,可以說中介佔了大多數,地勢險峻,易守難攻,果斷放棄。閒魚呢,資源又太少,攻下來的意義也不大,所以也放棄。我把目標放在了豆瓣上。在帝都的童鞋大部分都知道,豆瓣小組裡面有很多租房小組,年輕人居多,很多都是轉租,但很大一部分是和房東籤的合同,省掉了中介費。我大致翻了一下,基本上一天內的更新量能刷到90頁,每頁25條資料,當然有一些是舊的被頂上來的。這個資料量已經不少了,雖然裡面也混雜著大量的中介,但是相對來說比其他地方好很多。
鄭重宣告:各位在爬取資料的時候一定要控制頻率,不要影響網站的正常訪問!而且頻率過高會被豆瓣幹掉,且爬且珍惜!
另外,請詳細閱讀註釋中的內容!
我們先分析一下要抓取頁面的結構。以大名鼎鼎的北京租房小組舉例。
首先我們點選下方的更多小組討論切換到列表頁面,這樣就可以分析頁面的分頁邏輯了。前後翻幾頁我們不難發現,豆瓣是利用url後面的引數來實現分頁的。比如第一頁的url為https://www.douban.com/group/beijingzufang/discussion?start=0
,第二頁為https://www.douban.com/group/beijingzufang/discussion?start=25
,每頁25條資料,很清晰明瞭了吧?
這時候,我們只需要分別獲取到每頁的資料,然後再做一些過濾,就可以極大減少篩選的時間了。我們選擇前二十個頁面來作為爬取物件,一方面不會對網站造成影響,另一方面也保證資料儘可能使最新。
好的,重點來了,作為一個前端,我使用node來做抓取,先引入一些必要的依賴。
import fs from `fs` // node的檔案模組,用於將篩選後的資料輸出為html
import path from `path` // node的路徑模組,用於處理檔案的路徑
// 以下模組非node.js自帶模組,需要使用npm安裝
// 客戶端請求代理模組
import superagent from "superagent"
// node端操作dom的利器,可以理解成node版jQuery,語法與jQuery幾乎一樣
import cheerio from "cheerio"
// 通過事件來決定執行順序的工具,下面用到時作詳解
import eventproxy from `eventproxy`
// async是一個第三方node模組,mapLimit用於控制訪問頻率
import mapLimit from "async/mapLimit"
複製程式碼
然後就可以把我們要抓取的頁面整理到一個陣列裡面了
let ep = new eventproxy() // 例項化eventproxy
let baseUrl = `https://www.douban.com/group/beijingzufang/discussion?start=`;
let pageUrls = [] // 要抓取的頁面陣列
let page = 20 // 抓取頁面數量
let perPageQuantity = 25 // 每頁資料條數
for (let i = 0; i < page; i++) {
pageUrls.push({
url: baseUrl + i * perPageQuantity
});
}
複製程式碼
簡單分析下頁面的dom結構。頁面中的有效資料全在table
中,第一個tr
是標題,接下來每個tr
對應一條資料。然後每個tr
下有4個td
。分別存放著標題,作者,回應數和最後修改時間。
我們先寫個入口函式,訪問所有要抓取的頁面並儲存我們需要的資料。話說,好久不寫jQuery都有點手生了。
function start() {
// 遍歷爬取頁面
const getPageInfo = (pageItem, callback) => {
// 設定訪問間隔
let delay = parseInt((Math.random() * 30000000) % 1000, 10)
pageUrls.forEach(pageUrl => {
superagent.get(pageUrl.url)
// 模擬瀏覽器
.set(`User-Agent`, `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36`)
// 如果你不乖乖少量爬資料的話,很可能被豆瓣kill掉,這時候需要模擬登入狀態才能訪問
// .set(`Cookie`,``)
.end((err, pres) => {
let $ = cheerio.load(pres.text) // 將頁面資料用cheerio處理,生成一個類jQuery物件
let itemList = $(`.olt tbody`).children().slice(1, 26) // 取出table中的每行資料,並過濾掉表格標題
// 遍歷頁面中的每條資料
for (let i = 0; i < itemList.length; i++) {
let item = itemList.eq(i).children()
let title = item.eq(0).children(`a`).text() || `` // 獲取標題
let url = item.eq(0).children(`a`).attr(`href`) || `` // 獲取詳情頁連結
// let author = item.eq(1).children(`a`).attr(`href`).replace(`https://www.douban.com/people`, ``).replace(///g, ``) || `` // 獲取作者id
let author = item.eq(1).children(`a`).text() || `` // 這裡改為使用作者暱稱而不是id的原因是發現有些中介註冊了好多賬號,打一槍換個地方。雖然同名也有,但是這麼小的資料量下,概率低到忽略不計
let markSum = item.eq(2).text() // 獲取回應數量
let lastModify = item.eq(3).text() // 獲取最後修改時間
let data = {
title,
url,
author,
markSum,
lastModify
}
// ep.emit(`事件名稱`, 資料內容)
ep.emit(`preparePage`, data) // 每處理完一條資料,便把這條資料通過preparePage事件傳送出去,這裡主要是起計數的作用
}
setTimeout(() => {
callback(null, pageItem.url);
}, delay);
})
})
}
}
複製程式碼
我們通過mapLimit來控制訪問頻率,mapLimit的細節參照官方文件。傳送門
mapLimit(pageUrls, 2, function (item, callback) {
getPageInfo(item, callback);
}, function (err) {
if (err) {
console.log(err)
}
console.log(`抓取完畢`)
});
複製程式碼
簡單說一下過濾的策略吧,首先在標題裡,過濾掉不合適的地點,以及中介最常用的話術。也可以自己新增想要的關鍵詞,有針對性的進行篩選。然後統計每個作者的發帖數,這裡的判斷條件是如果每個人發帖數在抓取的頁面中出現超過5次以上,則被認為是中介。如果某個帖子的回覆量巨大,要麼是個舊帖子被頂上來了,要麼很可能是有人在不停的刷排名,我這裡設定的閾值是100。試想一個正常的房東不會這麼喪心病狂的刷存在感,因為好房根本不愁租不出去,很可能是中介每天在刷舊帖子。即便是因為房子比較好所以大家都在圍觀,那其實你租到的概率已經很小了,所以直接過濾掉。
// 我們設定三個全域性變數來儲存一些資料
let result = [] // 存放最終篩選結果
let authorMap = {} // 我們以物件屬性的方式,來統計每個的發帖數
let intermediary = [] // 中介id列表,你也可以把這部分資料儲存起來,以後抓取的時候直接過濾掉!
// 還記得之前的ep.emit()嗎,它的每次emit都被這裡捕獲。ep.after(`事件名稱`,數量,事件達到指定數量後的callback())。
// 也就是說,總共有20*25(頁面數*每頁資料量)個事件都被捕獲到以後,才會執行這裡的回撥函式
ep.after(`preparePage`, pageUrls.length * page, function (data) {
// 這裡我們傳入不想要出現的關鍵詞,用`|`隔開 。比如排除一些位置,排除中介常用短語
let filterWords = /押一付一|短租|月付|蛋殼|有房出租|6號線|六號線/
// 這裡我們傳入需要篩選的關鍵詞,如沒有,可設定為空格
let keyWords = /西二旗/
// 我們先統計每個人的發帖數,並以物件的屬性儲存。這裡利用物件屬性名不能重複的特性實現計數。
data.forEach(item => {
authorMap[item.author] = authorMap[item.author] ? ++authorMap[item.author] : 1
if (authorMap[item.author] > 4) {
intermediary.push(item.author) // 如果發現某個人的發帖數超過5條,直接打入冷宮。
}
})
// 陣列去重,Set去重瞭解一下,可以查閱Set這種資料結構
intermediary = [...new Set(intermediary)]
// 再次遍歷抓取到的資料
data.forEach(item => {
// 這裡if的順序可是有講究的,合理的排序可以提升程式的效率
if (item.markSum > 100) {
console.log(`評論過多,丟棄`)
return
}
if (filterWords.test(item.title)) {
console.log(`標題帶有不希望出現的詞語`)
return
}
if(intermediary.includes(item.author)){
console.log(`發帖數過多,丟棄`)
return
}
// 只有通過了上面的層層檢測,才會來到最後一步,這裡如果你沒有設期望的關鍵詞,篩選結果會被統統加到結果列表中
if (keyWords.test(item.title)) {
result.push(item)
}
})
// .......
});
複製程式碼
到此為止,我們已經拿到了期望的結果列表,但是直接列印出來,並不那麼的好用,所以我們把它生成一個html。我們只需簡單的進行html的拼裝即可
// 設定html模板
let top = `<!DOCTYPE html>` +
`<html lang="en">` +
`<head>` +
`<meta charset="UTF-8">` +
`<style>` +
`.listItem{ display:block;margin-top:10px;text-decoration:none;}` +
`.markSum{ color:red;}` +
`.lastModify{ color:"#aaaaaa"}` +
`</style>` +
`<title>篩選結果</title>` +
`</head>` +
`<body>` +
`<div>`
let bottom = `</div> </body> </html>`
// 拼裝有效資料html
let content = ``
result.forEach(function (item) {
content += `<a class="listItem" href="${item.url}" target="_blank">${item.title}_____<span class="markSum">${item.markSum}</span>____<span class="lastModify">${item.lastModify}</span>`
})
let final = top + content + bottom
// 最後把生成的html輸出到指定的檔案目錄下
fs.writeFile(path.join(__dirname, `../tmp/result.html`), final, function (err) {
if (err) {
return console.error(err);
}
console.log(`success`)
});
複製程式碼
最後,我們只需把入口函式暴露出去即可
export default {
start
}
複製程式碼
由於我們是使用ES6的語法寫的,所以在使用的時候,需要藉助babel-node
。首先安裝babel-cli
,你可以選擇全域性安裝或者區域性安裝, npm i babel-cli -g
。同時別忘了文章開頭三個依賴的安裝。
最終我們在index.js檔案中引入上面的指令碼,並執行babel-node index.js
。我們看到了激動人心的success。
// index.js
import douban from `./src/douban.js`
douban.start()
複製程式碼
最後我們開啟HTML看一看效果吧,標紅的是回覆數量,點選標題可以直接跳轉到豆瓣對應的頁面。同時,利用a標籤點選過後變色的效果,我們可以方便的判斷是否已經看過這條資料。
我簡單設定了一些過濾條件,資料由500條直線下降到138條,極大的縮短了我們的篩選時間。如果我加一些指定的篩選關鍵詞,搜尋結果還會更精準!
好了,時候不早了,今天的分享就到此為止。如果大家覺得找房子比較費勁,還是要去找鏈家,我愛我家等這樣的大中介,比較靠譜省心。最後祝大家找到暖心的小窩!