做爲前端,如何幫帝都的朋友租到合適的房子

  在帝都打拼的小夥伴都知道,要租個合適的房子真心不易。中介要收一個月的房租做爲中介費。並且不少黑中介打着租房的旗號各類坑蒙拐騙。要想在茫茫帖子中找到真正的房東,宛如大海撈針,同時須要和各路黑中介鬥智鬥勇。接下來就講講我浴血奮戰的故事。javascript

  那麼,How to start? 咱們先選一塊陣地。58趕集這樣的網站,能夠說中介佔了大多數,地勢險峻,易守難攻,果斷放棄。閒魚呢,資源又太少,攻下來的意義也不大,因此也放棄。我把目標放在了豆瓣上。在帝都的童鞋大部分都知道,豆瓣小組裏面有不少租房小組,年輕人居多,不少都是轉租,但很大一部分是和房東籤的合同,省掉了中介費。我大體翻了一下,基本上一天內的更新量能刷到90頁,每頁25條數據,固然有一些是舊的被頂上來的。這個數據量已經很多了,雖然裏面也混雜着大量的中介,可是相對來講比其餘地方好不少。html

鄭重聲明:各位在爬取數據的時候必定要控制頻率,不要影響網站的正常訪問!並且頻率太高會被豆瓣幹掉,且爬且珍惜!
另外,請詳細閱讀註釋中的內容!

  咱們先分析一下要抓取頁面的結構。以大名鼎鼎的北京租房小組舉例。前端


  首先咱們點擊下方的更多小組討論切換到列表頁面,這樣就能夠分析頁面的分頁邏輯了。先後翻幾頁咱們不難發現,豆瓣是利用url後面的參數來實現分頁的。好比第一頁的url爲https://www.douban.com/group/beijingzufang/discussion?start=0,第二頁爲https://www.douban.com/group/beijingzufang/discussion?start=25,每頁25條數據,很清晰明瞭了吧?java

  這時候,咱們只須要分別獲取到每頁的數據,而後再作一些過濾,就能夠極大減小篩選的時間了。咱們選擇前二十個頁面來做爲爬取對象,一方面不會對網站形成影響,另外一方面也保證數據儘量使最新。node

  好的,重點來了,做爲一個前端,我使用node來作抓取,先引入一些必要的依賴。git

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"

  而後就能夠把咱們要抓取的頁面整理到一個數組裏面了github

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。分別存放着標題,做者,迴應數和最後修改時間。npm

  咱們先寫個入口函數,訪問全部要抓取的頁面並保存咱們須要的數據。話說,很久不寫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條,極大的縮短了咱們的篩選時間。若是我加一些指定的篩選關鍵詞,搜索結果還會更精準!

  好了,時候不早了,今天的分享就到此爲止。若是你們以爲找房子比較費勁,仍是要去找鏈家,我愛我家等這樣的大中介,比較靠譜省心。最後祝你們找到暖心的小窩!

相關文章
相關標籤/搜索