本次主要围绕数据分析岗位的招聘情况, 进行一个简单的数据分析
环境
win8, python3.7, pycharm, jupyter notebook
正文
1. 明确分析目的
了解数据分析岗位的最新招聘情况, 包括地区分布, 学历要求, 经验要求, 薪资水平等.
2. 数据收集
这里借助爬虫, 爬取招聘网站的招聘信息, 进而分析出相关的薪资以及招聘要求.
 2.1 目标站点分析
通过对目标站点的分析, 我们需要确定目标站点的请求方式, 以及网页结构.
 2.2 新建scrapy项目
1. 在cmd命令行窗口中任意路径下执行以下代码, 比如在"D:\python\Tests"目录下新建zhaopin项目.
d:
cd D:\python\Tests
scrapy startproject zhaopin
2. 在完成了zhaopin项目创建之后, 接下来就是在zhaopin项目文件夹中新建spider爬虫主程序
cd zhaopin
scrapy genspider zhaopinSpider zhaopin.com
这样就完成项目zhaopin的创建, 开始编写我们的程序吧.
2.3 定义items
在items.py文件中定义需要爬取的招聘信息.
复制代码
import scrapy
from scrapy.item import Item, Field
class zhaopinItem(Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    JobTitle = Field()                      #职位名称
    CompanyName = Field()                   #公司名称
    CompanyNature = Field()                 #公司性质
    CompanySize = Field()                   #公司规模
    IndustryField = Field()                 #所属行业
    Salary = Field()                        #薪水
    Workplace = Field()                     #工作地点
    Workyear = Field()                      #要求工作经验
    Education = Field()                     #要求学历
    RecruitNumbers = Field()                #招聘人数
    ReleaseTime = Field()                   #发布时间
    Language = Field()                      #要求语言
    Specialty = Field()                     #要求专业
    PositionAdvantage = Field()             #职位福利
复制代码
2.4 编写爬虫主程序
在zhaopinSpider.py文件中编写爬虫主程序
复制代码
import scrapy
from scrapy.selector import Selector
from scrapy.spiders import CrawlSpider
from scrapy.http import Request
from zhaopin.items import zhaopinItem
class ZhaoPinSpider(scrapy.Spider):
    name = "ZhaoPinSpider"
    allowed_domains = ['zhaopin.com']
    start_urls = ['https://xxxx.com/list/2,{0}.html?'.format(str(page)) for page in range(1, 217)]
    def parse(self, response):
        '''
        开始第一页
        :param response:
        :return:
        '''
        yield Request(
            url = response.url,
            callback = self.parse_job_url,
            meta={},
            dont_filter= True
        )
    def parse_job_url(self, response):
        '''
        获取每页的职位详情页url
        :param response:
        :return:
        '''
        selector = Selector(response)
        urls = selector.xpath('//div[@class="el"]/p/span')
        for url in urls:
            url = url.xpath('a/@href').extract()[0]
            yield Request(
                url = url,
                callback = self.parse_job_info,
                meta = {},
                dont_filter = True
            )
    def parse_job_info(self, response):
        '''
        解析工作详情页
        :param response:
        :return:
        '''
        item = Job51Item()
        selector = Selector(response)
        JobTitle = selector.xpath('//div[@class="cn"]/h1/text()').extract()[0].strip().replace(' ','').replace(',',';')
        CompanyName = selector.xpath('//div[@class="cn"]/p[1]/a[1]/text()').extract()[0].strip().replace(',',';')
        CompanyNature = selector.xpath('//div[@class="tCompany_sidebar"]/div/div[2]/p[1]/text()').extract()[0].strip().replace(',',';')
        CompanySize = selector.xpath('//div[@class="tCompany_sidebar"]/div/div[2]/p[2]/text()').extract()[0].strip().replace(',',';')
        IndustryField = selector.xpath('//div[@class="tCompany_sidebar"]/div/div[2]/p[3]/text()').extract()[0].strip().replace(',',';')
        Salary = selector.xpath('//div[@class="cn"]/strong/text()').extract()[0].strip().replace(',',';')
        infos = selector.xpath('//div[@class="cn"]/p[2]/text()').extract()
        Workplace = infos[0].strip().replace('  ','').replace(',',';')
        Workyear = infos[1].strip().replace('  ','').replace(',',';')
        if len(infos) == 4:
            Education = ''
            RecruitNumbers = infos[2].strip().replace('  ', '').replace(',',';')
            ReleaseTime = infos[3].strip().replace('  ', '').replace(',',';')
        else:
            Education = infos[2].strip().replace('  ', '').replace(',',';')
            RecruitNumbers = infos[3].strip().replace('  ', '').replace(',',';')
            ReleaseTime = infos[4].strip().replace('  ', '').replace(',',';')
        if len(infos) == 7:
            Language, Specialty = infos[5].strip().replace('  ',''), infos[6].strip().replace('  ','').replace(',',';')
        elif len(infos) == 6:
            if (('英语' in infos[5]) or ('话' in infos[5])):
                Language, Specialty = infos[5].strip().replace('  ','').replace(',',';'), ''
            else:
                Language, Specialty = '', infos[5].strip().replace('  ','').replace(',',';')
        else:
            Language, Specialty = '', ''
        Welfare = selector.xpath('//div[@class="t1"]/span/text()').extract()
        PositionAdvantage = ';'.join(Welfare).replace(',', ';')
        item['JobTitle'] =JobTitle
        item['CompanyName'] =CompanyName
        item['CompanyNature'] =CompanyNature
        item['CompanySize'] = CompanySize
        item['IndustryField'] = IndustryField
        item['Salary'] =Salary
        item['Workplace'] = Workplace
        item['Workyear'] =Workyear
        item['Education'] =Education
        item['RecruitNumbers'] = RecruitNumbers
        item['ReleaseTime'] =ReleaseTime
        item['Language'] = Language
        item['Specialty'] = Specialty
        item['PositionAdvantage'] = PositionAdvantage
        yield item
复制代码
2.5 保存到csv文件
通过pipelines项目管道保存至csv文件
复制代码
class Job51Pipeline(object):
    def process_item(self, item, spider):
        with open(r'D:\Data\ZhaoPin.csv','a', encoding = 'gb18030') as f:
            job_info = [item['JobTitle'], item['CompanyName'], item['CompanyNature'], item['CompanySize'], item['IndustryField'], item['Salary'], item['Workplace'], item['Workyear'], item['Education'], item['RecruitNumbers'], item['ReleaseTime'],item['Language'],item['Specialty'],item['PositionAdvantage'],'\n']
            f.write(",".join(job_info))
        return item
复制代码
2.6 配置setting
设置用户代理, 下载延迟0.5s, 关闭cookie追踪, 调用pipelines
复制代码
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
DOWNLOAD_DELAY = 0.5
COOKIES_ENABLED = False
ITEM_PIPELINES = {
   'job51.pipelines.Job51Pipeline': 300,
}
复制代码
2.7 运行程序
新建main.py文件, 并执行以下代码
from scrapy import cmdline
cmdline.execute('scrapy crawl zhaopin'.split())
这样开始了数据爬取, 最终爬取到9000多条数据, 在分析这些数据之前, 先看看数据都是什么样, 进入数据概览环节.
3. 数据概览
 3.1 读取数据
复制代码
import pandas as pd
df = pd.read_csv(r'D:\aPython\Data\DataVisualization\shujufenxishiJob51.csv')
#由于原始数据中没有字段, 需要为其添加字段
df.columns = ['JobTitle','CompanyName','CompanyNature','CompanySize','IndustryField','Salary','Workplace','Workyear','Education','RecruitNumbers', 'ReleaseTime','Language','Specialty','PositionAdvantage']
df.info()
复制代码
抛出异常: UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbd in position 0: invalid start byte
解决办法; 用Notepad++将编码转换为utf-8 bom格式
转换之后, 再次执行
抛出异常: ValueError: Length mismatch: Expected axis has 15 elements, new values have 14 elements
解决办法: 在列表['JobTitle.....PositionAdvantage']后面追加'NNN', 从而补齐15个元素.
追加之后, 再次执行, 执行结果为:
RangeIndex: 9948 entries, 0 to 9947
Data columns (total 15 columns):
JobTitle             9948 non-null object
CompanyName          9948 non-null object
CompanyNature        9948 non-null object
CompanySize          9948 non-null object
IndustryField        9948 non-null object
Salary               9948 non-null object
Workplace            9948 non-null object
Workyear             9948 non-null object
Education            7533 non-null object
RecruitNumbers       9948 non-null object
ReleaseTime          9948 non-null object
Language             901 non-null object
Specialty            814 non-null object
PositionAdvantage    8288 non-null object
NNN                  0 non-null float64
dtypes: float64(1), object(14)
memory usage: 1.1+ MB
可以了解到的信息: 目前的数据维度9948行X15列,  Education, Language, Specialty, PositionAdvantage有不同程度的缺失(NNN是最后添加, 仅仅是用来补齐15元素), 14个python对象(1个浮点型)
3.2 描述性统计
由于我们所需信息的数据类型都是python对象, 故使用以下代码
#注意是大写的字母o
df.describe(include=['O'])
从以下信息(公司名称部分我没有截图)中可以得到:
职位名称中'数据分析师'最多, 多为民营公司, 公司规模150-500人最多, 行业领域金融/投资/证券最多, 薪资中6-8千/月最多,  大多对工作经验没有要求, 学历要求多为本科, 多数均招1人等信息.
职位名称的种类就有4758种, 他们都是我们本次分析的数据分析师岗位吗, 先来确认下:
zhaopin.JobTitle.unique()
array(['零基础免费培训金融外汇数据分析师', '数据分析师(周末双休+上班舒适)', '数据分析师', ...,
       '数据分析实习(J10635)', '数据分析实习(J10691)', '数据分析实习(J10713)'], dtype=object)
这仅仅显示了职位名称中的一部分,而且还都符合要求, 换种思路先看20个
JobTitle = zhaopin.groupby('JobTitle', as_index=False).count()
JobTitle.JobTitle.head(20)
0                             (AI)机器学习开发工程师讲师
1                           (ID67391)美资公司数据分析
2                           (ID67465)美资公司数据分析
3             (ID67674)500强法资汽车制造商数据分析专员(6个月)
4                 (ID67897)知名500强法资公司招聘数据分析专员
5                         (Senior)DataAnalyst
6                           (免费培训)数据分析师+双休+底薪
7            (实习职位)BusinessDataAnalyst/业务数据分析
8                                   (急)人力销售经理
9                               (提供食宿)银行客服+双休
10    (日语)股票数据分析员/EquityDataAnalyst-Japanese/
11     (越南语)股票数据分析员/EquityDataAnalyst-Vietnam
12                           (跨境电商)产品专员/数据分析师
13       (韩语)股票数据分析员/EquityDataAnalyst-Korean
14                                    ***数据分析
15                               -数据分析师助理/实习生
16                      -数据分析师助理/统计专员+双休五险+住宿
17                        -无销售不加班金融数据分析师月入10k
18                          -金融数据分析师助理6k-1.5w
19                             -金融数据分析师双休岗位分红
Name: JobTitle, dtype: object
可以看到还有机器学习开发讲师, 人力销售经理, 银行客服等其他无效数据.
现在我们对数据有了大致的认识, 下来我们开始数据预处理.
4. 数据预处理
4.1 数据清洗
数据清洗的目的是不让有错误或有问题的数据进入加工过程, 其主要内容包括: 重复值, 缺失值以及空值的处理
4.1.1 删除重复值
如果数据中存在重复记录, 而且重复数量较多时, 势必会对结果造成影响, 因此我们应当首先处理重复值.
#删除数据表中的重复记录, 并将删除后的数据表赋值给zhaopin
zhaopin = df.drop_duplicates(inplace = False)
zhaopin.shape
(8927, 15)
对比之前的数据, 重复记录1021条.
4.1.2 过滤无效数据
我们了解到职位名称中存在无效数据, 我们对其的处理方式是过滤掉.
#筛选名称中包含'数据'或'分析'或'Data'的职位
zhaopin = zhaopin[zhaopin.JobTitle.str.contains('.*?数据.*?|.*?分析.*?|.*?Data.*?')]
zhaopin.shape
(7959, 15)
4.1.3 缺失值处理
在pandas中缺失值为NaN或者NaT, 其处理方式有多种:
1. 利用均值等集中趋势度量填充
2. 利用统计模型计算出的值填充
3. 保留缺失值
4. 删除缺失值
#计算每个特征中缺失值个数
zhaopin.isnull().sum()
JobTitle                0
CompanyName             0
CompanyNature           0
CompanySize             0
IndustryField           0
Salary                  0
Workplace               0
Workyear                0
Education            1740
RecruitNumbers          0
ReleaseTime             0
Language             7227
Specialty            7244
PositionAdvantage    1364
NNN                  7959
dtype: int64
-- Education: 缺失值占比1740/7959 = 21.86%, 缺失很有可能是"不限学历", 我们就用"不限学历"填充
zhaopin.Education.fillna('不限学历', inplace=True)
-- Language: 缺失值占比7227/7959 = 90.80%, 缺失太多, 删除特征
-- Specialty: 缺失值占比7244/7959 = 91.02%, 同样缺失很多, 删除
zhaopin.drop(['Specialty','Language'], axis=1, inplace = True)
-- PositionAdvantage: 缺失占比1364/7959 = 17.14%, 选用众数中的第一个'五险一金'填充
zhaopin.PositionAdvantage.fillna(zhaopin.PositionAdvantage.mode()[0], inplace = True)
-- NNN: 没有任何意义, 直接删除
zhaopin.drop(["NNN"], axis=1, inplace = True)
最后, 检查缺失值是否处理完毕
zhaopin.isnull().sum()
JobTitle             0
CompanyName          0
CompanyNature        0
CompanySize          0
IndustryField        0
Salary               0
Workplace            0
Workyear             0
Education            0
RecruitNumbers       0
ReleaseTime          0
PositionAdvantage    0
dtype: int64
4.2 数据加工
由于现有的数据不能满足我们的分析需求, 因此需要对现有数据表进行分列, 计算等等操作.
需要处理的特征有: Salary, Workplace
1. Salary
将薪资分为最高薪资和最低薪资, 另外了解到薪资中单位有元/小时, 元/天, 万/月, 万/年, 千/月, 统一将其转化为千/月
复制代码
import re
#将5种单元进行编号
zhaopin['Standard'] = np.where(zhaopin.Salary.str.contains('元.*?小时'), 0, 
                               np.where(zhaopin.Salary.str.contains('元.*?天'), 1,
                                        np.where(zhaopin.Salary.str.contains('千.*?月'), 2, 
                                                 np.where(zhaopin.Salary.str.contains('万.*?月'), 3, 
                                                          4))))
#用'-'将Salary分割为LowSalary和HighSalary
SalarySplit = zhaopin.Salary.str.split('-', expand = True)
zhaopin['LowSalary'], zhaopin['HighSalary'] = SalarySplit[0], SalarySplit[1]
#Salary中包含'以上', '以下'或者两者都不包含的进行编号
zhaopin['HighOrLow'] = np.where(zhaopin.LowSalary.str.contains('以.*?下'), 0, 
                                np.where(zhaopin.LowSalary.str.contains('以.*?上'), 2, 
                                         1))
#匹配LowSalary中的数字, 并转为浮点型
Lower = zhaopin.LowSalary.apply(lambda x: re.search('(\d+\.?\d*)', x).group(1)).astype(float)
#对LowSalary中HighOrLow为1的部分进行单位换算, 全部转为'千/月'
zhaopin.LowSalary = np.where(((zhaopin.Standard==0)&(zhaopin.HighOrLow==1)), Lower*8*21/1000,
                             np.where(((zhaopin.Standard==1)&(zhaopin.HighOrLow==1)), Lower*21/1000,
                                      np.where(((zhaopin.Standard==2)&(zhaopin.HighOrLow==1)), Lower,
                                               np.where(((zhaopin.Standard==3)&(zhaopin.HighOrLow==1)), Lower*10,
                                                        np.where(((zhaopin.Standard==4)&(zhaopin.HighOrLow==1)), Lower/12*10,
                                                                 Lower)))))
#对HighSalary中的缺失值进行填充, 可以有效避免匹配出错.
zhaopin.HighSalary.fillna('0千/月', inplace =True)
#匹配HighSalary中的数字, 并转为浮点型
Higher = zhaopin.HighSalary.apply(lambda x: re.search('(\d+\.?\d*).*?', str(x)).group