Python爬虫之薪资分析

准备环境

  • python3
  • BeautifulSoup
  • PyCharm
  • Echart

背景

想看看智联招聘上各个行业的评价薪资是多少,最后生成个图表,最好还能排除培训机构,因为培训机构并不招人但是招聘广告上的工资却很高…

最终效果

python计算绩效工资的步骤 jmupython计算薪资_薪资统计

从图上看出,我们会把需要行业的招聘信息抓取下来,然后讲他们的平均薪资记录下来生成柱状图,当点击其中的柱状图的时候,可以显示这个岗位的薪资分布

整体流程

整体会由以下几个模块组成:

  • url_manager: 用来管理所有的url的
  • htmldownloader: 根据url将页面上的数据下载下来
  • html_parser: 根据下载下来的数据,来解析出我们需要的平均薪资,并且排除掉一些常见的培训机构
  • html_outter: 将最后统计的结果输出成html页面
  • spider_main: 入口,并负责启动各个页面

流程

spider_main来初始化所有的模块 => 从url_manager中取出一个url => 将url交给html_downloader来下载html => 将下载好的页面交给html_parser来进行解析 => 最后将解析好的结果通过html_outter来输出成html页面

模块详情

html_manager

#!/usr/bin/env python3
# -*- coding: utf-8 -*-


import urllib.parse


class UrlManager(object):

    def __init__(self, keys, p):
        self.urls = set()
        # 基础URL没有搜索的关键字
        self.base_url = 'http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E5%A4%A7%E8%BF%9E&&isadv=0'
        for key in keys:
            for i in range(1, p + 1):
                self.add_url(self.base_url, key, i)

    def add_url(self, url, key, p):
        key = urllib.parse.quote(key)  # url编码
        url = '%s&kw=%s&p=%d' % (url, key, p)
        print(url)
        self.urls.add(url)
        pass

    def get_url(self):
        """
        :return: 未抓取的url
        """
        return self.urls.pop()

    def has_new_url(self):
        """
        :return: 是否还有url没有被抓取
        """
        return len(self.urls) != 0
  • 在初始化的时候,根据基础网址来拼接要抓取的关键字,和页数来生成指定的url
  • 所有的url放在一个set中进行存储
  • 当从集合中获取url的时候,会将这个url从集合中删除

html_downloader

#!/usr/bin/env python3
# -*- coding: utf-8 -*-


import urllib.request


class HtmlDownloader(object):
    def download(self, url):
        if url:
            req = urllib.request.Request(url)
            req.add_header("User-Agent",
                           "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36")
            with urllib.request.urlopen(req) as opener:
                return opener.read().decode('utf-8')
  • 这里使用urllib.request来进行网络请求
  • 添加User-Agent头来假装我们是一个浏览器
  • 最后在读取信息的时候需要decode(‘utf-8’)

html_parser

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from bs4 import BeautifulSoup
import urllib.parse

black_words = ['达内', '睿道', '文思海辉', '中软', '鹏讯']


class HtmlParser(object):

    def parser(self, page_url, html_cont):
        soup = BeautifulSoup(html_cont, 'html.parser')

        moneys = []

        result = urllib.parse.urlparse(page_url)
        params = urllib.parse.parse_qs(result.query, True)

        rows = soup.find_all('tr', class_="")
        for row in rows:
            try:
                company_node = row.find('td', class_="gsmc").find('a')
                company_name = company_node.get_text()
                for black in black_words:
                    if black in company_name:
                        print("黑名单:", company_name)
                        continue

                money_node = row.find('td', class_="zwyx")
                money = money_node.get_text()
                money_range = money.split('-')
                if len(money_range) == 2:
                    moneys.append(int(money_range[0]) + int(money_range[1]) / 2)

            except Exception as e:
                print("-------------")
                print(row)
                print("-------------------")
                print('can not parser:', e)

        return params['kw'][0], moneys
  • 使用BeautifulSoup来进行解析html页面,在使用之前需要安装BeautifulSoup
  • 在解析的过程中是有可能失败的,所以使用try来捕获异常防止程序崩溃
  • 在解析的时候,不光要解析薪资,还需要解析出公司的名字,如果包含培训机构的名字,我们就不统计它
  • 解析的时候其实就是去找网页的规律
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGY4GLA9-1623744492930)(http://47.93.60.69:88/img/pics/DAD4E2FFC4804DA2ACC7D2F87A83AB0F.png?x-oss-process=style/CfyInfo)]
  • 由于薪资都是个范围,所以在统计的时候取上限,和下限的平均值
  • 还有些薪资是面议,就不统计了
  • 最后将这一页的数据收集起来并返回

html_outter

#!/usr/bin/env python3
# -*- coding: utf-8 -*-


class HtmlOuter(object):

    def __init__(self):
        self.datas = {}

    def collect_data(self, k, m):

        if k not in self.datas:
            self.datas[k] = []
        self.datas[k] = self.datas[k] + m

    def output_html(self):
        fout = open('out/output.html', 'wb')
        fin = open('template/output_t.html', 'rb')

        result = []
        for k, v in self.datas.items():
            num = len(v) if len(v) != 0 else 1

            result.append([k, sum(v) / num])
            self._output_detail(k, v)

        print(result)

        for line in fin.readlines():
            line = line.decode("utf-8")

            line = line.replace('{{data}}', repr(result))

            fout.write(line.encode("utf-8"))

        fin.close()
        fout.close()

    def _output_detail(self, key, values):
        fout = open('out/%s.html' % key, 'wb')
        fin = open('template/detail_t.html', 'rb')

        result = [['5000以下', 0], ['5000-7000', 0], ['7000-10000', 0], ['10000以上', 0]]
        for value in values:
            if value < 5000:
                result[0][1] = result[0][1] + 1
            elif value < 7000:
                result[1][1] = result[1][1] + 1
            elif value < 10000:
                result[2][1] = result[2][1] + 1
            else:
                result[3][1] = result[3][1] + 1

        for line in fin.readlines():
            line = line.decode("utf-8")
            line = line.replace('{{data}}', repr(result))
            line = line.replace('{{title}}', key)
            fout.write(line.encode('utf-8'))

        fin.close()
        fout.close()
  • 输出的时候需要收集每一页的信息
  • 最后输出的时候实际上是先写好模板,然后将指定的字符串替换成数据

html模板

output_t.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="echarts.common.min.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个具备大小(宽高)的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
    // 基于准备好的dom,初始化echarts实例
    var myChart = echarts.init(document.getElementById('main'));

    // 指定图表的配置项和数据
    var option = {
        title: {
            text: '薪资分布',
            left: 'center',
            top: 20,
            textStyle: {
                color: '#000000'
            }
        },
        tooltip: {},
        legend: {},
        // 全局调色盘。
        xAxis: {type: 'category'},
        yAxis: {},
        dataset: {
            source: {{data}}
        },
        series: [
            {type: 'bar', color: '#c23531', // 高亮样式。
        emphasis: {
            itemStyle: {
                // 高亮时点的颜色。
                color:'#ea926b'
            },
            label: {
                show: true,
                // 高亮时标签的文字。
                color:'#314134'
            }
        }}
        ]

    };

    // 使用刚指定的配置项和数据显示图表。
    myChart.setOption(option);

    //点击事件
    myChart.on('click',function (params) {
        window.location.href='/CollectSalary/out/'+params.name+'.html'
    })
</script>
</body>
</html>

detail_t.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="echarts.common.min.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个具备大小(宽高)的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
    // 基于准备好的dom,初始化echarts实例
    var myChart = echarts.init(document.getElementById('main'));

    // 指定图表的配置项和数据
    var option = {
        backgroundColor: '#2c343c',

        title: {
            text: '{{title}}',
            left: 'center',
            top: 20,
            textStyle: {
                color: '#ccc'
            }
        },
        // 全局调色盘。
    color: ['#c23531','#2f4554', '#61a0a8', '#d48265', '#91c7ae','#749f83',  '#ca8622', '#bda29a','#6e7074', '#546570', '#c4ccd3'],
        dataset: {
            source: {{data}}
        },

        tooltip: {
            trigger: 'item',
            formatter: "{a} <br/>{b} : {c} ({d}%)"
        },

        visualMap: {
            show: false,
            min: 80,
            max: 600,
            inRange: {
                colorLightness: [0, 1]
            }
        },
        series: [
            {
                name: '职位个数',
                type: 'pie',
                radius: '55%',
                center: ['50%', '50%'],

                label: {
                    normal: {
                        textStyle: {
                            color: 'rgba(255, 255, 255, 0.3)'
                        }
                    }
                },
                labelLine: {
                    normal: {
                        lineStyle: {
                            color: 'rgba(255, 255, 255, 0.3)'
                        },
                        smooth: 0.2,
                        length: 10,
                        length2: 20
                    }
                },


                animationType: 'scale',
                animationEasing: 'elasticOut',
                animationDelay: function (idx) {
                    return Math.random() * 200;
                }
            }
        ]
    };

    // 使用刚指定的配置项和数据显示图表。
    myChart.setOption(option);


</script>
</body>
</html>

另外 这里是直接用pyCharm打开的网页,不是打开的静态页面

spider_main

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import html_downloader
import html_outter
import html_parser
import url_manager


class SpiderMain(object):
    def __init__(self, keys, p):
        self.urls = url_manager.UrlManager(keys, p)
        self.downloader = html_downloader.HtmlDownloader()
        self.parser = html_parser.HtmlParser()
        self.outer = html_outter.HtmlOuter()

    def craw(self):
        while self.urls.has_new_url():
            new_url = self.urls.get_url()
            html_content = self.downloader.download(new_url)
            k, m = self.parser.parser(new_url, html_content)
            print(f'craw: {new_url}')
            self.outer.collect_data(k, m)
        self.outer.output_html()


if __name__ == '__main__':
    key_words = ['JAVA', "Python", "运维", "Android", "大数据"]
    pages = 5
    SpiderMain(key_words, pages).craw()
  • 在初始化的时候,输入想要抓取的关键字和要抓取的页数
  • 这里没有抓取全部的,而是抓取了几页,如果一共岗位没有那么多的话,html是没有数据的,所以不会影响结果

目录结构:

python计算绩效工资的步骤 jmupython计算薪资_python计算绩效工资的步骤_02