2

前言

图片描述

今天是2018的第一天,首先祝各位小伙伴元旦快乐!
又到了新的一年,虽然离春节还有一段时间,但是程序狗打工不易啊,不关注薪资怎么行。今天要做的就是用图表统计一下现在各公司的薪资状况(虽然很多公司不能按照招聘上他们给的薪资来给)。

数据爬取

本次使用scrapy来做数据爬取,这是一个python的框架。因为本人在成都从事web前端,所以这次爬取的关键词既是:成都,web前端

scrapy startproject lagou

首先通过运行命令,得到一个爬虫项目的基础结构。

接着按照scrapy的中文教程,通过在

start_urls = [
        "https://www.lagou.com/jobs/list_web%E5%89%8D%E7%AB%AF?labelWords=sug&fromSearch=true&suginput=web"
    ]

spider中的start_urls配置好,应该就能把拉勾网页面拉取下来,然后再分析dom,提取字符串就可以了,无奈这种方法并不行。

起初也不知道,就用xpath一直找,后来发现找不到会报错,这些各种错误对于我这个爬虫萌新还是懵逼的。仔细查看他的network发现,他的招聘信息都是在另外的ajax请求当中,并且还是整理好的。

图片描述

因为本人工作1年多,所以主要关注点是3年以下及3-5年,就提前选好了,城市和工作年限。该请求的传参是formdata,其中first是首页(其实写代码的时候并没有注意这个参数,所以一直传的是true,貌似也没什么影响),pn是当前页数,kd是关键词。

图片描述

于是乎就去文档查阅了一下,如何在scrapy中循环发送formdata请求。最终得到这样一段可以执行的代码。

def start_requests(self):
        url = "https://www.lagou.com/jobs/positionAjax.json?gj=3%E5%B9%B4%E5%8F%8A%E4%BB%A5%E4%B8%8B%2C3-5%E5%B9%B4&xl=%E6%9C%AC%E7%A7%91&px=default&city=%E6%88%90%E9%83%BD&needAddtionalResult=false&isSchoolJob=0"
        for i in range(1, 14):
            formdata = {'first': 'true', 'pn': str(i), 'kd': 'web前端'}
            yield scrapy.FormRequest(str(url), callback=self.parseJson, formdata=formdata)

start_requests是发送post请求的方法,FormRequest这个方法接收请求url,传递数据formdata,以及回调函数parseJson。parseJson在这里主要是接收获取的数据。

仅仅有这个是不够的,因为貌似拉勾网有反爬虫,没有header好像得不到数据(这个还待论证,至少我这边是)。然后再settings.py文件中做了一些配置,配置主要有:

  • 请求的header(主要是这几项)
DEFAULT_REQUEST_HEADERS={
Accept:application/json, text/javascript, */*; q=0.01
Host:www.lagou.com
Origin:https://www.lagou.com
Referer:https://www.lagou.com/jobs/list_web%E5%89%8D%E7%AB%AF?px=default&gj=3%E5%B9%B4%E5%8F%8A%E4%BB%A5%E4%B8%8B,3-5%E5%B9%B4&city=%E6%88%90%E9%83%BD
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
}
  • FEED_EXPORT_ENCODING(因为爬取到的中文是unicode字符)
FEED_EXPORT_ENCODING = 'utf-8'
  • ROBOTSTXT_OBEY(这是一个爬虫机器的协议,如果是true,表示遵守,有些网站禁止爬取的话,这个如果是true就爬不到了)
ROBOTSTXT_OBEY = False

  • DOWNLOAD_DELAY(延时,这个也是去避免被反爬虫,我这边直接设置了比较长的时间,也没有去测试多少合适,因为不设置也是会报错的)
DOWNLOAD_DELAY = 10

基础的配置项配置完毕之后,就是写数据存储的模型了,因为我只想去简单统计一下,所以只存了薪资和工资这两个字段,想要统计更多的信息,就直接继续加就好了,这个比较简单,在items.py中编写

class LaGou(scrapy.Item):
    salary = scrapy.Field()
    company = scrapy.Field()

经过这几项配置,运行命令

scrapy crawl lagou -o a.json

就可以得到一份a.json,里面就是成都web前端相关,工作年限为0-5年的数据信息了。有了这份数据,接下来要做的就是数据处理了。

数据处理

在之前的a.json当中,大致可以得到一份之下的数据,总计195条

[
{"salary": "8k-16k", "company": "xx有限公司"},
......
]

为了前端处理方便,直接改为js文件加一个变量引入html,即

var a = [
    {"salary": "8k-16k", "company": "xx有限公司"},
    ......
    ]

这组数据的薪资是一个范围,不方便我统计,于是为了便于操作数据把薪资取平均值,并统计提供相同的薪资的公司数目。
js代码如下:

var arr = data.map(function (value) {
        return value.salary && value.salary.replace(/k|K/g, "").split('-').reduce(function (pV, nV) {
            return pV + nV / 2
        }, 0)
    }).reduce(function (pV, nV) {
        nV in pV ? pV[nV]++ : (pV[nV] = 1);
        return pV;
    }, {})
    //这里的data既是上边的a变量

这段代码主要作用是把薪资范围计算成平均数,然后再统计数组中相同的平均数的个数。代码写的随意,可读性较差,见谅。这段代码处理过后,可得到类似如下数据:

{'8':1,'8.5':3}

key是薪资均值,value是个数。

于是将key,value分别存入数组。这里遇到一个问题,就是开始我是这样操作的

var xData=[...Object.keys(arr)]
var yData=[...Object.values(arr)]

这么做有一个问题就是浏览器对于对象的遍历规则,导致输出的数组,小数都到了最外边(比如这样[1,2,1.5]),这样在echarts下的图表是乱序的。也没有想到好的办法去解决,就是对数组进行一次排序,然后再根据排好的key生成相对应的value数组,最终代码:

    var xData = [...Object.keys(arr).sort(function (a, b) {
        return a - b
    })]
    var yData = xData.map(function (v) {
        return arr[v]
    })

echarts比较简单不赘述。将这两组横纵坐标输入echarts,得到最终效果:
图片描述

总结

本次做这个统计很多地方没想清楚怎么更好的去表现,所以做的很简单,其实细致一点还可以去分类统计,按照公司融资情况,领域等等内容,只要数据拿到都好说。另外很多地方可能写的不够好,主要我目前也不太会写,比如之前反爬虫那块,貌似去做动态的用户代理也能行,但我还是增加了延时,选择了比较笨的方法。另外也不会python,但还好python比较好读。因为这一块才开始学习,相信以后会越写越好的,新的一年,加油!

update 2018/01/03

昨天又把爬虫优化了一下,去掉了之前的延时,增加了动态用户代理和动态IP代理,解决了之前爬虫的效率问题,也扩大了数据量。

动态IP代理

通过网上搜索免费的ip代理,获取了如下一组ip:

PROXIES = [
    {'ip_port': '106.39.179.244:80'},
    {'ip_port': '65.52.223.99:80'},
    {'ip_port': '1.52.248.207:3128'},
    {'ip_port': '45.77.198.207:3128'},
    {'ip_port': '177.125.119.16:8080'},
    {'ip_port': '174.138.65.233:3128'},
]

该IP过一段时间可能会失效,请自行搜索,如http://www.xicidaili.com/
在middlewares.py中声明该IP,之后声明动态IP代理类

    import random
    class ProxyMiddleware(object):
        def process_request(self, request, spider):
            proxy = random.choice(PROXIES)
                request.meta['proxy'] = "http://%s" % proxy['ip_port']
                print("**************ProxyMiddleware no pass************" + proxy['ip_port'])

在settings.py文件中声明该中间件

DOWNLOADER_MIDDLEWARES = {
    'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': 110,
    'tutorial.middlewares.ProxyMiddleware': 100,
}

动态用户代理

在middlewares.py中声明动态用户代理类

class RandomUserAgent(object):
    """Randomly rotate user agents based on a list of predefined ones"""

    def __init__(self, agents):
        self.agents = agents

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.getlist('USER_AGENTS'))

    def process_request(self, request, spider):
        # print "**************************" + random.choice(self.agents)
        request.headers.setdefault('User-Agent', random.choice(self.agents))

同样在settings.py的中间件里声明
DOWNLOADER_MIDDLEWARES = {

'tutorial.middlewares.RandomUserAgent': 1,
'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': 110,
'tutorial.middlewares.ProxyMiddleware': 100,

}
再次运行scrapy crawl lagou,即可得到新的数据。

增加薪资筛选

在原有基础上增加了对于工作年限和公司规模的筛选,并计算了平均值。
更新代码如下:

// 指定图表的配置项和数据
initData();  
function initData() {
        average = 0;
        arr = temData.map(function (value) { //之前正则筛选字符串有点问题,没有考虑到有些公司格式为10k以上这种。
            return value.salary && value.salary.replace(/[k|K\u4e00-\u9fa5]/g, "").split('-').reduce(function (pV, nV, i, array) {
                if (array.length > 1) {
                    average = Number(average) + pV + nV / 2
                    return pV + nV / 2
                } else {
                    average = +average + Number(nV)
                    return nV
                }
                // return array.length > 1 ? pV + nV / 2 : nV
            }, 0)
        }).reduce(function (pV, nV) {
            nV in pV ? pV[nV]++ : (pV[nV] = 1);
            return pV;
        }, {})
        average = (average / temData.length).toFixed(2)
    }

暂时这样,通过之后的学习,还会不断的优化。

展示效果:
图片描述

源码地址:https://github.com/jiwenjiang...


j_bleach
2.5k 声望70 粉丝

1 >> 1