房价在手,天下我有 --反手就撸一个爬虫(始)

栏目: 编程工具 · 发布时间: 6年前

内容简介:最近身边的朋友都在看房子,天天沉浸在各大房价网站中,看了几天和我抱怨还是对杭州的整个房价高低没有一个具体的概念。优秀且敏感的我听到了仿佛闻到了一丝需求的味道,既然他们拥有了这么优秀的我,怎么能让他们一直这么看房!完成效果如下:憋说了!你们的房价由我来守护,是时候要拿出我的吃饭的家伙了。

最近身边的朋友都在看房子,天天沉浸在各大房价网站中,看了几天和我抱怨还是对杭州的整个房价高低没有一个具体的概念。优秀且敏感的我听到了仿佛闻到了一丝需求的味道,既然他们拥有了这么优秀的我,怎么能让他们一直这么看房!

完成效果如下:

房价在手,天下我有 --反手就撸一个爬虫(始)

憋说了!你们的房价由我来守护,是时候要拿出我的吃饭的家伙了。

房价在手,天下我有 --反手就撸一个爬虫(始)

首先,看一下魔法装备和任务

房价在手,天下我有 --反手就撸一个爬虫(始)

很好,我们基于nuxt把基本骨架搭建出来,然后添加我们需要的文件,最终的整个项目结构如下:

房价在手,天下我有 --反手就撸一个爬虫(始)

万事开头难,我们首先要优化一下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')
复制代码

前面的铺垫都准备好了,那我们就可以愉快的宰鸡了~~~

我们来分析一下页面

房价在手,天下我有 --反手就撸一个爬虫(始)
  • 页面地址
  • 所要爬去的信息

按照思路,我们现在开始开始动手爬了。

拿出我们的神器:

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的数据入库。 休息一下,优秀的你看到(做到)这里,简直要为你鼓掌。不如。。。

房价在手,天下我有 --反手就撸一个爬虫(始)

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Design Accessible Web Sites

Design Accessible Web Sites

Jeremy Sydik / Pragmatic Bookshelf / 2007-11-05 / USD 34.95

It's not a one-browser web anymore. You need to reach audiences that use cell phones, PDAs, game consoles, or other "alternative" browsers, as well as users with disabilities. Legal requirements for a......一起来看看 《Design Accessible Web Sites》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具