深圳数据分析职位 招聘数据研究

一、数据获取

以下为数据获取的完整代码,直接使用 get 请求数据,网站只需验证正确的 User-Agent。同时在 JobSpider 中初步解析整理数据,使用 BeautifulSoup + CSS 解析数据,使用正则表达式整理数据完善字段。具体的解析逻辑参考网页源码。

# -*- coding: utf-8 -*-
"""
爬取 51Job-深圳-数据分析 招聘职位数据
"""

import re
import time
import urllib
import pandas as pd
import requests as rq
from bs4 import BeautifulSoup


class JobSpider():
    '''Get job data in 51Job with specific keyword and city.'''

    def __init__(self, city, keyword, pages):
        ''' --------------- Init Params ------------------
        city:       str in ('北京', '上海', '广州', '深圳')
        keyword:    search keyword
        pages:      get how many pages
        '''

        cdict = {'北京': '010000', '上海': '020000', '广州': '030200', '深圳': '040000'}
        self.city = cdict[city]
        self.keyword = urllib.parse.quote(keyword).replace('%', '%25')
        self.pages = pages
        self.url = f'https://search.51job.com/list/{self.city},000000,0000,00,9,99,{self.keyword}'+',2,{0}.html?'
        self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}


    def get_job_links(self, page):
        '''Get all job href in one page'''
        
        res = rq.get(self.url.format(page), headers=self.headers)
        soup = BeautifulSoup(res.text, 'lxml')
        tags = soup.find_all('p', {'class': 't1'})
        get_href = lambda tag: re.compile('href="(.*?)"').findall(str(tag))[0]
        links = [get_href(tag) for tag in tags]

        return links


    def get_one_job(self, link):
        '''Parse a single job details'''
        
        res = rq.get(link, headers=self.headers)
        res.encoding = 'gbk'
        html = res.text.replace('\n', '')
        soup = BeautifulSoup(html, 'lxml')

        keys = ['job_title', 'job_attribute', 'job_description', 
                'wage', 'welfare', 
                'company', 'company_attribute', 'compny_details']
        job = dict.fromkeys(keys, None)

        job[keys[0]] = soup.select('div.tHeader.tHjob > div > div.cn > h1')[0].text
        job[keys[1]] = soup.select('div.cn > p.msg.ltype')[0].text.replace('\xa0', '').split('|')
        job[keys[2]] = re.compile('
(.*?)
').findall(html)[0] job[keys[3]] = soup.select('div.tHeader.tHjob > div > div.cn > strong')[0].text job[keys[4]] = [w.text for w in soup.find_all('span', {'class': 'sp4'})] job[keys[5]] = soup.select('div.com_msg > a > p')[0].text job[keys[6]] = [w.text.replace('\n', '') for w in soup.find_all('p', {'class': 'at'})] job[keys[7]] = soup.select('div.tCompany_main > div:nth-child(2) > div')[0].text.replace('\xa0', '') return job def parse_dict(self, job): '''Unpack stacked job details''' keys = list(job.keys()) fields = [ 'job_title', 'location', 'experience', 'education', 'hire_num', 'release_date', 'job_description', 'wage', 'welfare', 'company', 'company_type', 'company_size', 'business_field', 'company_description' ] def get_element(i, lst): try: r = lst[i] # except IndexError: except: r = None return r new_job = dict.fromkeys(fields) new_job[fields[0]] = job[keys[0]] for i in range(5): new_job[fields[i+1]] = get_element(i, job[keys[1]]) new_job[fields[6]] = re.sub('<[^<]+?>', '', job[keys[2]]).strip() new_job[fields[7]] = job[keys[3]] new_job[fields[8]] = '|'.join(job[keys[4]]) new_job[fields[9]] = job[keys[5]] for i in range(3): new_job[fields[i+10]] = get_element(i, job[keys[6]]) new_job[fields[13]] = job[keys[7]] return new_job def main(self, start=1, delay=1): '''Main function''' for page in range(start, self.pages+1): links = self.get_job_links(page) job_list = [] for link in links: try: job = self.get_one_job(link) new_job = self.parse_dict(job) job_list.append(new_job) print('\t' + link) except: print('[ERROR]' + link) continue time.sleep(delay) pd.DataFrame(job_list).to_csv(f'page{page}.csv', index=False) print(f'>>> Saved page{page}.csv')
if __name__ == "__main__":

    # get data
    # 由于 51Job 直接搜索会模糊匹配,只取前 24 页跟关键词相关性高的内容
    pages = 24
    js = JobSpider('深圳', '数据', pages)
    js.main(1)

    # join csv
    jobs = pd.read_csv('page1.csv')
    for i in range(1, pages):
        df = pd.read_csv(f'page{i+1}.csv')
        jobs = jobs.append(df)
    jobs.to_csv('raw.csv', index=False)
    
    # generate profile report
    import pandas_profiling
    profile = pandas_profiling.ProfileReport(jobs)
    profile.to_file('profile.html')

二、数据清洗

根据不同字段的性质进行不同的清洗。例如工作描述,公司描述等字段,主要用作词频统计,不需要特别清洗。对于分类变量,需要统一取值并排除异常值,例如工作地点变量。对于连续变量,例如工资,需要转为数值型。对于时间戳,需要统一格式。以下为部分字段的示例。

# location 字段:统一取值,删除非深圳的职位
def unify_value(value, level1, level2=[]):
    # 当 level1 中的匹配目标包含 level2 中的字符,则需要使用两个 level
    for i1 in level1:
        if i1 in value:
            return i1
    for i2 in level2:
        if i2 in value:
            return i2
    # 失配取 None
    return None

regions = ['南山区', '福田区', '龙岗区', '宝安区', '龙华新区', '罗湖区', '光明新区', '坪山区', '盐田区', '大鹏新区']
df[keys[1]] = df[keys[1]].apply(unify_value, level1=regions, level2=['深圳'])

df.drop(np.where(df[keys[1]].isna())[0], axis=0, inplace=True)
df.reset_index(inplace=True, drop=True)

# 大部分字段类似,主要使用 unify_value 函数
# 发布日期:简化取值,异常值取空值
df[keys[5]] = ['2019-' + v.replace('发布', '') if '发布' in str(v)
                else None for v in df[keys[5]]]
# 工资:转成数值元组,效果:1-1.5万/月 --> (10, 15)
def string_replace(string, dct):
    for k, v in dct.items():
        string = string.replace(k, v)
    return string
rpl_dict = {'/': '-', '年': '12', '月': '1', '千': '-3', '万': '-4'}
df[keys[7]] = [string_replace(str(v), rpl_dict).split('-') for v in df[keys[7]]]
def get_tuple(lst):
    if len(lst) == 4:
        t1 = float(lst[0]) * 10 ** float(lst[2]) / float(lst[3])
        t2 = float(lst[1]) * 10 ** float(lst[2]) / float(lst[3])
        return (int(t1/1000), int(t2/1000))
df[keys[7]] = df[keys[7]].apply(get_tuple)

# 增加字段,将上面的元组拆了
df['wage_low'] = [w[0] if w else None for w in df['wage']]
df['wage_high'] = [w[1] if w else None for w in df['wage']]

最终结果如下:

RangeIndex: 1116 entries, 0 to 1115
Data columns (total 16 columns):
 #   Column               Non-Null Count  Dtype
---  ------               --------------  -----
 0   job_title            1116 non-null   object
 1   location             1116 non-null   object
 2   experience           1010 non-null   object
 3   education            982 non-null    object
 4   hire_num             982 non-null    object
 5   release_date         982 non-null    object
 6   job_description      1116 non-null   object
 7   wage                 1054 non-null   object
 8   welfare              900 non-null    object
 9   company              1116 non-null   object
 10  company_type         1116 non-null   object
 11  company_size         1062 non-null   object
 12  business_field       1116 non-null   object
 13  company_description  1116 non-null   object
 14  wage_low             1054 non-null   float64
 15  wage_high            1054 non-null   float64
dtypes: float64(2), object(14)
memory usage: 139.6+ KB

三、数据可视化

可视化主要涉及分类字段的分布以及数值变量的分组聚合运算,逻辑并不复杂,在 Tableau 中可以很容易地绘图。对于行业领域和福利待遇两个字段,由于是嵌套列表的形式,将其转化为字符串列表再再 Tableau 中绘制词云图。

# 行业领域
fields = '/'.join(
    df.business_field.map(lambda x: x.strip()).map(lambda x: x.replace(' ', '/'))
)
pd.DataFrame({'行业': fields.split('/')}, ).to_csv('business_fields.csv', index=False)

# 福利待遇
welfare = '|'.join(df.welfare[-(df.welfare.isna())])
pd.DataFrame({'福利': welfare.split('|')}).to_csv('welfare.csv', index=False)

对于岗位描述字段,在 python 中进行分词。由于较多空泛的词组,这里使用正则表达式获取英文字符(表示技术栈),再进行词云可视化。具体操作如下。

# jieba 分词,非全模式分词
split_list = jieba.cut(str(' '.join(df.job_description)), cut_all=False)
text = list(split_list)

# 用正则匹配所有英文词组
uniq_words = pd.Series(text).unique()
eng_words = re.compile(r'[a-zA-Z]+?\|').findall('|'.join(uniq_words))
# 删除刚刚插入的辅助符号 “|”
eng_words = [v.replace('|', '') for v in eng_words]
# 去除上面 text 列表中的中文项(使用布尔型索引)
text = pd.Series(text)[pd.Series(text).map(lambda x: x in eng_words)]

# 增加其他停止词
stopwords = set(list(string.printable))     # 去除单个字符
stop_list = ['OR', 'with', 'ability', 'to', 'HR', 'on', 'including', 
    'the', 'have', 'other', 'of', 'be', 'at', 'our', 'as', 'related',
    'and', 'year', 'Good', 'will', 'partner', 'you', 'technical', 'nbsp']
for s in stop_list:
    stopwords.add(s)

# 绘制词云
wc = WordCloud(width=1920, height=1080, 
            background_color='#1a1a1a',
            font_path = './fonts/consola.ttf', max_font_size=400,
            stopwords=stopwords, 
            random_state=50)

wc.generate_from_text(' '.join(text))
plt.imshow(wc)
plt.axis('off')
plt.show()

四、分析报告

公司类型 | 公司规模 | 学历要求 | 经验要求

深圳数据分析职位 招聘数据研究_第1张图片

行业领域(一个职位对应多个领域)

深圳数据分析职位 招聘数据研究_第2张图片

福利待遇(一个职位对应多种福利)

深圳数据分析职位 招聘数据研究_第3张图片

不同区域的工资均值以及职位数量

深圳数据分析职位 招聘数据研究_第4张图片

工资的具体分布(工资下限)

深圳数据分析职位 招聘数据研究_第5张图片

技能要求(提取自工作描述)

深圳数据分析职位 招聘数据研究_第6张图片

你可能感兴趣的:(爬虫,数据分析)