房價在手,天下我有 –反手就擼一個爬蟲(始)

我母雞啊!發表於2019-03-04

最近身邊的朋友都在看房子,天天沉浸在各大房價網站中,看了幾天和我抱怨還是對杭州的整個房價高低沒有一個具體的概念。優秀且敏感的我聽到了彷彿聞到了一絲需求的味道,既然他們擁有了這麼優秀的我,怎麼能讓他們一直這麼看房!

完成效果如下:

3.gif-806.1kB

憋說了!你們的房價由我來守護,是時候要拿出我的吃飯的傢伙了。

1.png-18kB

首先,看一下魔法裝備和任務

魔法裝備.png-160.5kB

很好,我們基於nuxt把基本骨架搭建出來,然後新增我們需要的檔案,最終的整個專案結構如下:

house.png-362kB

萬事開頭難,我們首先要優化一下nuxt生成的server/index.js

  • 我們要建立一個server類
  • 提取中介軟體並且在server建立的過程中插入我們的中介軟體

程式碼如下:

 import Koa from `koa`;
import { Nuxt, Builder } from `nuxt`;
import R from `ramda`;
import { resolve } from `path`

// Import and Set Nuxt.js options
let config = require(`../nuxt.config.js`)
config.dev = !(process.env === `production`)
const host = process.env.HOST || `127.0.0.1`
const port = process.env.PORT || 4000
const MIDDLEWARES = [`database`,`crawler`,`router`]
const r = path =>resolve(__dirname,path)

class Server {
  constructor(){
    this.app = new Koa();
    this.useMiddleWares(this.app)(MIDDLEWARES)
  }
  useMiddleWares(app){
    //載入不同的中介軟體
    return R.map(R.compose(
        R.map( i =>i(app)),
        require,
        i => `${r(`./middlewares`)}/${i}`
    ))
  }

  async start () {
    // Instantiate nuxt.js
    const nuxt = new Nuxt(config)
    // Build in development
    if (config.dev) {
      const builder = new Builder(nuxt)
      await builder.build()
    } 
    this.app.use(async (ctx, next) => {
      await next()
      ctx.status = 200 // koa defaults to 404 when it sees that status is unset
      return new Promise((resolve, reject) => {
        ctx.res.on(`close`, resolve)
        ctx.res.on(`finish`, resolve)
        nuxt.render(ctx.req, ctx.res, promise => {
          promise.then(resolve).catch(reject)
        })
      })
    })
    this.app.listen(port, host)
    console.log(`Server listening on ` + host + `:` + port) // eslint-disable-line no-console
  }
}

const app = new Server();

app.start()
複製程式碼

這時候我們要在根目錄下新建start.js

  • 專案裡用到了修飾器等es6的語法,要引入babel的解碼,這裡我偷懶了,就直接在start.js裡直接引用。
  • 引入server下的index.js

程式碼如下:

const { resolve } = require(`path`)
const r = path => resolve(__dirname, path)

require(`babel-core/register`)({
    `presets`:[
        `stage-3`,
        [
            `latest-node`, {
                "target": "current"
            }
        ]
    ],
    `plugins`: [
        `transform-decorators-legacy`,
        [`module-alias`, [
          { `src`: r(`./server`), `expose`: `~`},
          { `src`: r(`./server/database`), `expose`: `database`}
        ]]
      ]
})

require(`babel-polyfill`)
require(`./server/index`)
複製程式碼

前面的鋪墊都準備好了,那我們就可以愉快的宰雞了~~~

我們來分析一下頁面

WechatIMG30.jpeg-1497.9kB
  • 頁面地址
  • 所要爬去的資訊

按照思路,我們現在開始開始動手爬了。

拿出我們的神器:

import cheerio from `cheerio` //node裡的jquery,幫助我們解析頁面
複製程式碼

我們先分析一下思路:

  • 我們先去請求頁面地址,因為資料不止一頁,所以我們需要做一個迴圈,用頁面上的 下一頁 這個欄位來判讀,是否到了最後一頁。
  • 我們通過class名取拿到頁面上想要的資料。
  • 這裡,我做了一些處理篩選資料,一些資料不全的資料就直接捨去。
  • 資料的細化處理,爬去下來的文字,裡面有很多空格和【】,這些我們都是不想要的。
  • sleep方法,其實就是一個定時器,我們在每一頁爬取後都休息1秒鐘,然後繼續。避免請求的次數太多,被禁掉ip。

下面舉出一個檔案的例子:

import cheerio from `cheerio`
import rp from `request-promise`
import R from `ramda`
import _ from `lodash`
import { writeFileSync } from `fs`
import { resolve } from `path`;

const sleep = time => new Promise(resolve => setTimeout(resolve,time)) //發動一次休息

let _house = [];
let _area = ``
let _areaDetail= [];
export const gethouse = async ( page = 1,area = ``) =>{
    const options={
        uri:`https://hz.fang.anjuke.com/loupan/${area}/p${page}/`,
        transform: body => cheerio.load(body),
    }
    console.log("正在爬"+options.uri);
    const $ = await rp(options)
    let house = [];
    
    $(".key-list .item-mod").each(function(){ //這裡不能用箭頭函式,會拿不到this
        const name = $(this).find(".infos .lp-name .items-name").text();
        const adress =  $(this).find(".address .list-map").text();
        const huxing = $(this).find(".huxing").text();
        const favorPos = $(this).find(".favor-pos .price-txt").text();
        const aroundPrice = $(this).find(".favor-pos .around-price").text();
        house.push({
            name,
            huxing,
            favorPos,
            aroundPrice,
            adress
        })
    })

    //細化處理
    const fn = R.compose(
        R.map((house) =>{
            const r1 = house.huxing.replace(/s+/g,""); //去掉空格
            const r2 = house.aroundPrice.replace(/s+/g,"");
            const index1 = r2.indexOf("價");
            const index2 = r2.lastIndexOf("/");
            const price = r2.slice(index1+1,index2-1)
            const reg = /[^[]*[(.*)][^]]*/;
            const r3 = house.adress.match(reg);
            const i = house.adress.lastIndexOf("]")+1;
            house.adress = house.adress.slice(i).replace(/s+/g,"");
            house.huxing = r1;
            house.aroundPrice = price;
            house.area = r3[1]

            return house
        }),
        R.filter(house => house.name && house.adress && house.huxing && house.favorPos && house.aroundPrice) //判斷資料是否齊全,欄位不全則省去
    )

        house = fn(house);
        _house = _.union(_house,house)
        
    
    if($(`.next-page`).attr(`href`)){
        //writeFileSync("./static/House.json",JSON.stringify(_house,null,2),`utf-8`)
        console.log(`${area}共有${_house.length}條資料`)
        await sleep(1000);  
        page++;
        await gethouse(page,_area)
    }else{
        console.log("爬完了!"+_house.length)

        return _house
    }

}

//拿到了地區的分割槽,現在去檢索每個分割槽下的房價
export const getAreaDetail = async () =>{
    const area = require(resolve(__dirname,`../database/json/AreaDetail.json`))
    for(let i = 0; i<area.length; i++){
        let areaDetail = area[i][`areaDetail`];
        _areaDetail = _.union(_areaDetail,areaDetail)
        for(let j = 0; j< areaDetail.length; j++){
            _house=[]
            console.log(`正在爬取${areaDetail[j].text}`)
            _area = areaDetail[j]._id
            console.log(_area)
            await gethouse(1,_area)
            if(_house.length >0){
                areaDetail[j][`house`] = _house
            }
        }
    }
    writeFileSync("./server/database/json/detailHouse.json",JSON.stringify(area,null,2),`utf-8`)  
}

複製程式碼

這時候middleware的檔案裡新增crawler.js

  • 這裡引入crawler檔案下的爬蟲邏輯, 然後去執行裡面的方法

程式碼如下:

export const database = async app =>{
    /**
     * 一次引入需要爬取資料的方法
     */
    const area = require(`../crawler/area`)
    const house = require(`../crawler/house`)
    const areaHouse = require(`../crawler/areaHouse`)
    const detailhouse = require(`../crawler/detailHouse`)
    /**
     * 如果本地沒有json檔案,對應解開註釋進行資料的爬去
     */
    // await area.getarea()
    // await area.getAreaDetail()
    // await house.gethouse()
    // await areaHouse.getAreaDetail()
    // await detailhouse.getAreaDetail()
}
複製程式碼

這時候你就可以愉快的開到database/json下出現你爬到的資料啦~

  • 這個時候我沒有急著去把資料入庫,而是把拿到的json先去用echart渲染了一遍
  • 我對echart裡的api不是很熟悉,先拿json練練手,看需要的資料是哪一些
  • 我在這裡把前端的程式碼完成了,對於後面就只需要把非同步請求寫好就行了,感覺這樣心裡有底一些
  • 這裡還需要注意,nuxt裡引入第三發外掛的寫法,我是直接開了一個plugins檔案去管理第三方的外掛

程式碼如下:

根目錄nuxt.config.js

module.exports = {
  /*
  ** Headers of the page
  */
  head: {
    title: `starter`,
    meta: [
      { charset: `utf-8` },
      { name: `viewport`, content: `width=device-width, initial-scale=1` },
      { hid: `description`, name: `description`, content: `Nuxt.js project` }
    ],
    link: [
      { rel: `icon`, type: `image/x-icon`, href: `/favicon.ico` }
    ]
  },
  /*
  ** Global CSS
  */
  css: [`~static/css/main.css`],
  /*
  ** Customize the progress-bar color
  */
  loading: { color: `#3B8070` },
  /*
   ** Build configuration
   */
  build: {
    /*
     ** Run ESLINT on save
     */
    extend (config, ctx) {
      // if (ctx.isClient) {
      //   config.module.rules.push({
      //     enforce: `pre`,
      //     test: /.(js|vue)$/,
      //     loader: `eslint-loader`,
      //     exclude: /(node_modules)/
      //   })
      // }
    },
    vendor: [`~/plugins/echat`]
  },
  plugins: [`~/plugins/echat`]
}

複製程式碼

plugins/echart.js

import Vue from `vue`
import echarts from `echarts`
Vue.prototype.$echarts = echarts

複製程式碼

page/minHouse.vue

<template>
<div>
  <section class="container">
     <a @click="turnBack" class="back">返回</a>
    <div id="myChart" :style="{width: `auto`, height: `300px`}"></div>
  </section>
</div>
</template>

<script>
  import { mergeSort } from `../util/index`
  import Footer from `../components/layouts/Footer`
  import Header from `../components/layouts/Header`
  import {
    getAreaList,
    getAreaHouseList,
    getDetailList
  } from `../serverApi/area`

  export default {
    name: `hello`,
    data() {
      return {
        xAxis: [], //x軸的資料
        rate: [], //y軸的資料
        AreaHouse: [], //全部資料
        myChart:``, //chart
        _id:[],
        detail:[]
      }
    },
    created() {
    this.getAreaHouse()
    },
    mounted() {
    /**
    *基於準備好的dom,初始化echarts例項
    */
      this.myChart = this.$echarts.init(document.getElementById(`myChart`))
      this.clickBar()
    },
    methods: {
      /**
      * 返回邏輯
       */
      turnBack(){
        this.formateData(this.AreaHouse);
        this.drawLine()
      },
      /**
      * 點選bar的互動
       */
      clickBar(){
        let that = this
        this.myChart.on(`click`,function(params){
          ...
        })
      },
      /**
       *獲取小區域內房價
       */
      async getDetail({param}){
        await getDetailList(param).then((data)=>{
            if(data.code === 0){
            this.detail = data.area.areaDetail;
            this.formateData(this.detail);
            this.drawLine()
          }
        })
        
      },
      /**
       *獲取大區域的房價
       */
      async getAreaHouse(){
        await getAreaHouseList().then((data)=>{
          if(data.code === 0){
            this.AreaHouse = data.areaHouse;
            this.formateData(this.AreaHouse);
            this.drawLine()
          }
        })
        
      },
      /**
      * 資料處理,對資料裡的價格排序
       */
      formateData(data) {
        let textAry = [],_id=[],rate=[];
        for (let i = 0; i < data.length; i++) {
          textAry.push(data[i][`text`])
          _id.push(data[i][`_id`])
          let sortAry = mergeSort(data[i][`house`])
          data[i][`house`] = sortAry
          rate.push(sortAry[0][`aroundPrice`])
        }
        this.xAxis = textAry
        this._id = _id
        this.rate = rate
      },
      drawLine() {
      /** 
      * 繪製圖表
      */
        ...
    },
    components:{
       `my-footer`: Footer,
       `my-header`: Header
     }
  }
</script>


複製程式碼

到這裡,我們這個專案完成一半了,剩下就是路由的提取,介面的定義和json的資料入庫。
休息一下,優秀的你看到(做到)這裡,簡直要為你鼓掌。不如。。。

WechatIMG123jpeg-44.4kB

啊哈哈哈哈哈哈哈哈哈哈哈哈~

相關文章