1. 程式人生 > >BOSS直聘網站資料分析崗位資訊爬取

BOSS直聘網站資料分析崗位資訊爬取

          感謝BOSS直聘上比較可靠的招聘資訊,讓我們有機會對資料分析崗位進行簡單的爬取與分析。

語言:Python3

目錄

一、資訊爬取

二、資料分析

     2.1 資料解析

     2.2 資料分析

         2.2.1 資料清洗

         2.2.2 檢視單個特徵分佈

         2.2.3 分析特徵與標籤的關係

 三、建模

ps:這裡推薦一個學習Python3爬蟲非常好的網址,https://cuiqingcai.com/5052.html。內容來自於《Python3網路爬蟲開發實戰》一書。

首先來參觀下頁面資訊,要爬取的資訊有:崗位名稱,地區,工作經驗,學歷,企業資訊,薪水。我們會爬取北上廣深四個城市的招聘資訊。

一、資訊爬取

需要匯入的庫如下,庫的安裝參考上面給出的連結。

# coding:utf-8
import requests
import csv
import pandas as pd
from bs4 import BeautifulSoup
from requests.exceptions import RequestException

requests相對urllib更加強大,更加友好。雖然與高大上的scrapy相比low了不少,但我們只是從網頁上簡單爬取一些資訊,所以選用requests庫。使用BeautifulSoup解析庫對爬取的網頁資訊進行解析。使用pandas.DataFrame將資料儲存為csv格式,使用這種方式儲存非常方便。

下面我們逐步來完成程式碼的編寫

首先我們需要定義一個函式來獲取網站每頁的內容。這裡使用了一個代理IP。首先,構建一個最簡單的GET請求,網站會判斷如果客戶端發起的是GET請求的話,它返回相應的請求資訊。

def get_one_page(url):
	try:
		headers = {
			'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) ' + 
			'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'
		}
		html = requests.get(url, headers=headers)
		if html.status_code == 200:
			return html.text
		return None
	except RequestException:
		return None

來看下我們爬取的頁面資訊原始碼長啥樣(檢視網頁原始碼的方法這裡就不敘述了,網上很多教程),裡面的部分黑色字型就是我們要爬取的資料。

獲取了一個頁面的資訊後,就可以對資訊進行解析,我們定義一個可以一次解析一個頁面的函式,如下。首先定義一個全域性變數,用於儲存每個公司的招聘資訊。(ps:再次說一下,對程式碼中函式用法不明白的,或者對網頁結構不懂的同學,先去文章最上面給出的連結看看)

網頁結構可以看成是樹結構,上圖中<div class='job-primary'>~~</div>可看成一棵子樹,包含了一個企業該崗位的全部招聘資訊,該子樹包含了<div class='info-primary'>~~</div>和<div class='info-company'>~~</div>兩個子節點。

下面程式碼中使用find_all函式獲取當前頁面中所有<div class='job-primary'>~~</div>子樹,即當前頁面中所有企業該崗位的招聘資訊。通過一個for 迴圈來遍歷companies中的每棵子樹,對每棵子樹呼叫parse_one_company()函式進行解析。然後將解析得到的資料儲存到result_all列表中。

result_all = [] # 用於儲存樣本

def parse_one_page(html):
	soup = BeautifulSoup(html, 'lxml')
	companies = soup.find_all('div', 'job-primary', True)
	
	for com in companies:
		res = parse_one_company(com)
		result_all.append(res)

parse_one_company()函式用來對每個<div class='job-primary'>~~</div>子樹解析,也就是對每個企業該崗位的招聘資訊的解析。在<div class='info-company'>~~</div>節點下解析得到企業所屬行業和規模兩個資訊,即對應網頁原始碼中的“網際網路”和“20-99”。在<div class='info-primary'>~~</div>節點下解析得到崗位名稱,薪水,企業地址,工作經驗和學歷要求資訊,即對應網頁原始碼中的“資料分析師”,“15k-25k”,“廣州 番禺區 東環”,“1-3年”,“本科”。最後將這些資料儲存到result列表中,並返回。

def parse_one_company(comp):
	result = []
	company_soup = comp.find('div', class_='info-company')
	com_desc = company_soup.find('p').text

	primary_soup = comp.find('div', class_='info-primary')
	job_name = primary_soup.find('div').text
	salary = primary_soup.find('span').text
	requirement = primary_soup.find('p').text

	result.append(com_desc)
	result.append(job_name)
	result.append(salary)
	result.append(requirement)
	return result

上面只是爬取了一個頁面一個地區的資訊。接下來我們完成要對BOSS直聘上北上廣深四個地區的所有資料分析師崗位資訊進行爬取。這也是程式碼的最後一部分。

我們定義了parse_all_page()函式用於爬取四個地區的所有資訊。函式中給出了四個地區的url連結,引數num用於指定要爬取的地區,offset引數用於指定要爬取的頁面。在最下面的兩個for迴圈中呼叫parse_all_page()函式。

最後一行生成檔案程式碼中,引數mode='a'可以不用設定,使用mode='w'也可以。encoding='utf_8_sig'一定要設定,否者csv檔案會亂碼。

def parse_all_page(num, offset):
	
	url1 = 'https://www.zhipin.com/c101280100/h_101280100/?query=資料分析師&page='+str(offset)+'&ka=page-'+str(offset) # 廣州
	url2 = 'https://www.zhipin.com/c101280600/h_101280600/?query=資料分析師&page='+str(offset)+'&ka=page-'+str(offset) # 深圳
	url3 = 'https://www.zhipin.com/c101010100/h_101010100/?query=資料分析師&page='+str(offset)+'&ka=page-'+str(offset) # 北京
	url4 = 'https://www.zhipin.com/c101020100/h_101020100/?query=資料分析師&page='+str(offset)+'&ka=page-'+str(offset) # 上海
	urldict = {'1':url1, '2':url2, '3':url3, '4':url4}

	html = get_one_page(urldict[str(num)])
	parse_one_page(html)
	


if __name__ == '__main__':
	for j in range(1, 5):
		for i in range(1,11):
			parse_all_page(j, i)
	file = pd.DataFrame(result_all, columns=['公司資訊', '崗位', '薪水', '其他'])
	# encoding='utf_8_sig解決儲存到CSV檔案後顯示亂碼問題
	file.to_csv('Bosszhiping_four_city.csv', mode='a', index=True, encoding='utf_8_sig')

至此,程式碼已完成!Python爬蟲有很多種方法,只要掌握一種即可,如果只是對網頁進行爬取,requests庫就可以滿足了,如果是對整個網站進行爬取,那就得用scrapy了。

二、資料分析

2.1 資料解析

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import re

# 匯入資料,第一列的序號不匯入
data_df = pd.read_csv('Bosszhiping_four_city.csv', usecols=[1,2,3,4])

# 檢視資料前5行資訊,簡單瞭解下資料
data_df.head()

表中“公司資訊”和“其他”特徵存在多種資訊,因此需要對其進行解析。

首先對“其他”特徵進行解析,目的是得到“城市”,“城區”,“工作經驗”,“學歷”四個特徵

others = list(data_df['其他'])
others[0:5]

# 工作經驗和學歷沒有空格隔開,我們暫時將兩者放到edu列表中
city, area, edu= [], [], []
leng = len(others)
for s in others:
    temp = s.split(' ')
    city.append(temp[0])
    area.append(temp[1])
    edu.append(temp[2])
print(city[0:5])
print(area[0:5])
print(edu[0:5])
print(len(city), len(area), len(edu))

# 對edu列表進一步解析,得到“工作經驗”和“學歷”特徵
for i in range(leng):
    edu[i] = edu[i].replace('應屆生','0-Graduate ')
    edu[i] = edu[i].replace('經驗不限', '0-Unlimited ')
    edu[i] = edu[i].replace('年', ' ') 
experience = [] # 經驗特徵
education = [] # 學歷特徵
for s in edu:
    temp = s.split(' ')
    experience.append(temp[0])
    education.append(temp[1][-2:])
print(experience[0:10])
print(education[0:10])

for i in range(leng):
    temp = re.findall(r'\d\-?\d?\w*', experience[i]) # 使用正則表示式
    if len(temp) != 0:
        experience[i] = temp[0]
    else:
        experience[i] = None
print(experience[0:10])

然後對“公司資訊”特徵進行解析,目的是得到“所屬行業”,“是否上市”,“企業規模”特徵

company = list(data_df['公司資訊'])
print(company[0:5])

# 獲取企業規模特徵
CompSize = [None] * leng #企業規模
for i in range(leng):
    temp = re.findall(r'\d+\-?\d+', company[i])
    if temp is not None:
        CompSize[i] = temp[0]
print(CompSize[0:5])

# 獲取企業是否上市特徵,上市為1,非上市為0
CompType = [0] * leng # 企業型別,是否上市
for i in range(leng):
    if '已上市' in company[i]:
        CompType[i] = 1
print(CompType[0:10])

# 獲取企業所屬行業特徵
# 由於MAC 的matplotlib中文顯示問題,將中文換成英文
IndustryCate = [None] * leng # 企業所屬行業
for i in range(leng):
    # 不能使用 if '網際網路' or ‘網路’ in company[i]:
    # 因為 '網際網路' or ‘網路’ 這構成一個判斷語句,兩邊都非空,因此輸出True, if條件中永遠成立。
    if '電子商務' in company[i]:
        IndustryCate[i] = 'E-Commerence'
    elif '網際網路' in company[i]:
        IndustryCate[i] = 'Internet' 
    elif '網路' in company[i]:
        IndustryCate[i] = 'Internet'
    elif '計算機' in company[i]:
        IndustryCate[i] = 'Computer soft'
    elif '資料服務' in company[i]:
        IndustryCate[i] = 'Data service'
    elif '醫療' in company[i]:
        IndustryCate[i] = 'Medical care'
    elif '健康' in company[i]:
        IndustryCate[i] = 'Medical care'
    elif '遊戲' in company[i]:
        IndustryCate[i] = 'Game'
    elif '教育' in company[i]:
        IndustryCate[i] = 'Education'
    elif '生活' in company[i]:
        IndustryCate[i] = 'Life service'
    elif '旅遊' in company[i]:
        IndustryCate[i] = 'Life service'
    elif '物流' in company[i]:
        IndustryCate[i] = 'Logistics'
    elif '廣告' in company[i]:
        IndustryCate[i] = 'Advertisement'
    elif '零售' in company[i]:
        IndustryCate[i] = 'Retail'
    elif '諮詢' in company[i]:
        IndustryCate[i] = 'Consulting'
    elif '進出口' in company[i]:
        IndustryCate[i] = 'Foreign trade'
    else:
        IndustryCate[i] = 'Others'

print(IndustryCate[0:10])

salary = list(data_df['薪水'])
salary_int = [] 
salary_str = [0] * leng
for s in salary:
    temp = re.findall(r'\d+', s)
    temp1 = int(temp[0])
    temp2 = int(temp[1])
    #print(temp1, temp2)
    avg = (temp2 + temp1)/2
    #print(avg)
    salary_int.append(avg)
    
for i in range(len(salary)):
    if salary_int[i] <= 6:
        salary_str[i] = '0-6k'
    elif salary_int[i] > 6 and salary_int[i] <= 10:
        salary_str[i] = '6-10k'
    elif salary_int[i] >10 and salary_int[i] <= 15:
        salary_str[i] = '10-15k'
    elif salary_int[i] > 15 and salary_int[i] <=20:
        salary_str[i] = '15-20k'
    elif salary_int[i] > 20 and salary_int[i] <=30:
        salary_str[i] = '20-30k'
    elif salary_int[i] >30:
        salary_str[i] = '30+k'

print(salary_int[0:5])
print(salary_str[0:5])  

特徵解析完成後,將新得到的特徵和原來的“崗位”,“薪水”特徵一起組成一個新的資料

#education_ = list(data_df['學歷'])
education = map(lambda s:[s, 'Doctor'][s=='博士'], education)
education = map(lambda s:[s, 'Master'][s=='碩士'], education)
education = map(lambda s:[s, 'Undergraduate'][s=='本科'], education)
education = map(lambda s:[s, 'Specialty'][s=='大專'], education)
education = map(lambda s:[s, 'Second special'][s=='中技'], education)
education = map(lambda s:[s, 'High school'][s=='高中'], education)
education = map(lambda s:[s, 'Unlimited'][s=='不限'], education)
education = list(education)
job = list(data_df['崗位'])
dicts = {'崗位':job, '城市':city, '城區':area, '經驗/年':experience, '企業規模/人':CompSize, \
         '學歷':education, '是否上市':CompType, '所屬行業':IndustryCate, '薪水str':salary_str, '薪水int/k':salary_int}
newdata = pd.DataFrame(dicts)
newdata.head()

# 生成檔案
newdata.to_csv('newDataEng.csv', encoding='utf_8_sig')

2.2 資料分析

import numpy as np
import pandas as pd
import re
import matplotlib.pyplot as plt
import matplotlib as mpl
from pyecharts import Map

2.2.1 資料清洗

# 載入資料
data = pd.read_csv('newDataEng.csv')

data.head()

leng = len(data)
print(leng)

1200

# 刪除實習生崗位
job = data['崗位']
j = 0
for i in range(leng):
    if '實習生' in job[i]:
        j += 1 
        data.drop(axis=0, index=i, inplace=True)
leng = len(data)
print(j, leng)

29, 1171

data.info()

2.2.2 檢視單個特徵分佈

# 檢視學歷分類及分佈
data['學歷'].value_counts()

labels = list(data['學歷'].value_counts().index)
fracs = list(data['學歷'].value_counts().values)
explode = [0, 0.1, 0.2, 0.3, 0.5, 1.5, 2.8]
plt.pie(x=fracs, labels=labels, explode=explode, autopct='%.3f')
plt.show()

主要以本科學歷為主,說明該崗位工作難度不大

# 檢視經驗分佈
data['經驗/年'].value_counts()

由於是社招招聘資訊,所以有工作經驗要求,1-3,3-5年為主

labels = list(data['經驗/年'].value_counts().index)
fracs = list(data['經驗/年'].value_counts().values)
explode = [0, 0.1, 0.2, 0.3, 0.5, 1, 1.5]
plt.pie(x=fracs, labels=labels, explode=explode, autopct='%.3f')
plt.show()

# 檢視企業規模分佈
data['企業規模/人'].value_counts().plot(kind='bar')

崗位大都來自於中大型企業,小企業需求少

# 檢視企業型別分佈
data['是否上市'].value_counts().plot(kind='bar')

# 檢視企業所屬行業分佈
data['所屬行業'].value_counts().plot(kind='barh')

以網際網路企業居多,遠超其他行業類企業。說明網際網路企業更注重資料價值,或者是網際網路企業流量更多

# 檢視薪水分佈
data['薪水str'].value_counts()

labels = list(data['薪水str'].value_counts().index)
fracs = list(data['薪水str'].value_counts().values)
plt.pie(x=fracs, labels=labels, autopct='%.3f')
plt.show()

# 企業所屬行業詞雲圖
import wordcloud
word_list = list(data['所屬行業'])
word = ''
for s in word_list:
    word = word + s + ' '
print(word[0:100])
mywc = wordcloud.WordCloud(width=600, height=400, min_font_size=20).generate(word)
plt.imshow(mywc, interpolation='bilinear')
plt.axis('off')
plt.imsave('HangyeWordC.png', mywc)

# 檢視廣州市各區崗位數量
area_job = data['城區'].groupby(data['城市']).value_counts()
area_city = list(area_job['廣州'].index)
area_num = list(area_job['廣州'].values)
map = Map('廣州市各區崗位分佈圖', width=1200, height=600)
map.add('', area_city, area_num, visual_range=[0, 150], visual_text_color='#000', is_visualmap=True,
        is_label_show=True, maptype='廣州')
#map.render('./job_city_area.html')

天河區企業數量最多

2.2.3 分析特徵與標籤的關係

# 定義特徵-薪水柱狀圖
def plotbar(data_list, data_sala, m, n, stack=False):
    # data_list 特徵變數種類, data_sala 薪水按特徵分組後的資料
    # (m, n) 設定圖片大小,stack設定是否使用堆疊柱狀圖
    xlabel_list = ['0-6k', '6-10k', '10-15k', '15-20k', '20-30k', '30+k']
    n = len(xlabel_list)
    lengSize = len(data_list)
    total_width = 0.8
    width = total_width / n
    x = np.arange(n)
    x = x - (total_width - width) / 2

    plt.figure(figsize=(m, n))
    #num_list = [0] * n
    for j in range(lengSize):
        num_list = [0] * n
        init_num_list = list(data_sala[data_list[j]].values)
        index_list = list(data_sala[data_list[j]].index)
        # 用0補充缺失值
        for i in range(len(index_list)):
            if index_list[i] in xlabel_list:
                index = xlabel_list.index(index_list[i])
                num_list[index] = init_num_list[i]
        if stack:
            plt.bar(x, num_list, width=width, label=data_list[j])
        else:
            plt.bar(x+j*width, num_list, width=width, label=data_list[j])
        # 設定橫座標刻度標籤
        plt.xticks(x, xlabel_list)
        plt.legend()
# 是否上市與薪水關係
comType_list = list(data['是否上市'].value_counts().index)
comType_sala = data['薪水str'].groupby(data['是否上市']).value_counts(sort=False) 
plotbar(comType_list, comType_sala, 10, 8, False)

# 企業規模與薪水的關係
comSize_list = list(data['企業規模/人'].value_counts().index)
comSize_sala = data['薪水str'].groupby(data['企業規模/人']).value_counts(sort=False)
plotbar(comSize_list, comSize_sala, 15, 8)

# 定義折線-柱狀圖函式
def plotlinebar(dataType_list, data_sala, data_num):
    # dataType_list 橫座標軸, data_sala 特徵下的平均薪水, data_num 資料在特徵下分組的數量
    dataType_sala = []
    for i in range(len(dataType_list)):
        numa = list(data_sala[dataType_list[i]].index)
        numb = list(data_sala[dataType_list[i]].values)
        result = sum(np.multiply(np.array(numa), np.array(numb)))/sum(numb)
        dataType_sala.append(result)
    
    ax1 = plt.figure(figsize=(10,8)).add_subplot(111)
    plt.xticks(range(len(dataType_list)), dataType_list) # 防止折線圖點連線順序混亂,自定義橫座標刻度
    ax1.plot(dataType_sala, 'or-')
    ax1.set_ylabel('Mean salary k/month')

    ax2 = ax1.twinx()
    ax2.bar(range(len(dataType_list)),data_num, alpha=0.5) #上面已經自定義了橫座標軸,這裡與其保持一致
    ax2.set_ylabel('numbers')

    plt.show()
# 學歷與薪水關係
eduType_list = list(data['學歷'].value_counts().index)
edu_sala = data['薪水int/k'].groupby(data['學歷']).value_counts(sort=False)
edu_num = list(data['學歷'].value_counts())
plotlinebar(eduType_list, edu_sala, edu_num)

# 經驗與薪水的關係
expType_list = list(data['經驗/年'].value_counts().index)
exp_sala = data['薪水int/k'].groupby(data['經驗/年']).value_counts(sort=False)
exp_num = list(data['經驗/年'].value_counts())
plotlinebar(expType_list, exp_sala, exp_num)

#coding:utf-8
import seaborn as sns
import matplotlib as mpl
#mpl.rcParams['font.sans-serif'] = ['simhei']
#mpl.rcParams['axes.unicode_minus'] = False 
# 廣州不同地區薪酬比較
gz_data = data[data['城市']=='廣州']
sns.boxplot(x=gz_data['城區'], y=gz_data['薪水int/k'])
# matplotlib中文顯示亂碼

# 定義雷達圖函式
def plotlinebar(dataType_list, data_sala, data_num):
    # dataType_list 橫座標軸, data_sala 特徵下的平均薪水, data_num 資料在特徵下分組的數量
    dataType_sala = []
    for i in range(len(dataType_list)):
        numa = list(data_sala[dataType_list[i]].index)
        numb = list(data_sala[dataType_list[i]].values)
        result = sum(np.multiply(np.array(numa), np.array(numb)))/sum(numb)
        dataType_sala.append(result)
    dataType_sala = np.array(dataType_sala)
    
    angles = np.linspace(0, 2*np.pi, len(dataType_list), endpoint=False)
    data = np.concatenate((dataType_sala, [dataType_sala[0]]))
    angles = np.concatenate((angles, [angles[0]]))
                            
    ax = plt.figure(figsize=(10, 10)).add_subplot(111, polar=True)
    ax.plot(angles, data, 'bo-', linewidth=2)
    ax.fill(angles, data, facecolor='r', alpha=0.3)
    ax.set_thetagrids(angles*180/np.pi, dataType_list)    
    plt.show()
# 所屬行業與薪水關係
indcateType_list = list(data['所屬行業'].value_counts().index)
indcate_sala = data['薪水int/k'].groupby(data['所屬行業']).value_counts()
indcate_num = list(data['所屬行業'].value_counts())
plotlinebar(indcateType_list, indcate_sala, indcate_num)

三、 建模

from sklearn.model_selection import train_test_split, cross_val_score, learning_curve
from sklearn.linear_model import LogisticRegression, LinearRegression
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
from sklearn.metrics import accuracy_score, mean_squared_error
import seaborn as sns
import warnings

warnings.filterwarnings('ignore')

劃分特徵和標籤,薪水str是分類問題

label_str = data['薪水str']
train_data = data.drop(columns=['崗位', '薪水int/k', '薪水str'])
# one-hot 編碼
train_data = pd.get_dummies(train_data)
# 學習曲線圖
train_sizes, tr_loss, te_loss = learning_curve(LogisticRegression(), train_data, label_str,\
                                              cv=10, scoring='accuracy')
tr_loss_m = np.mean(tr_loss, axis=1)
te_loss_m = np.mean(te_loss, axis=1)

plt.figure()
plt.plot(train_sizes, tr_loss_m, 'o-', color='r', label='train acc')
plt.plot(train_sizes, te_loss_m, 'o-', color='b', label='test acc')
plt.xlabel('train sizes')
plt.ylabel('accuracy')
plt.legend(loc='best')


# 劃分分類問題的訓練集和測試集
train_xs, test_xs, train_ys, test_ys = train_test_split(train_data, label_str, test_size=0.25)

model_lr = LogisticRegression()
scores = cross_val_score(model_lr, train_xs, train_ys, scoring='accuracy', cv=5)
model_lr.fit(train_xs, train_ys)
pred_lr = model_lr.predict(test_xs)
print(accuracy_score(test_ys, pred_lr))
0.4402730375426621

一點點說明

模型預測結果差,是因為樣本不具備代表性,存在太多噪音,標籤的設定也有待商榷。

本文只從BOSS直聘網站上爬取了資訊,這直接導致樣本不能代表全網站的招聘資訊。BOSS網站上只顯示了30頁資料,因此爬取的樣本甚至都不能代表BOSS直聘上釋出的招聘資訊。這兩點導致樣本不具備代表性。

標籤的沒有統一的規定,各個企業給出的薪水範圍沒有具體的標準,導致難以設計薪水等級。