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直聘上釋出的招聘資訊。這兩點導致樣本不具備代表性。
標籤的沒有統一的規定,各個企業給出的薪水範圍沒有具體的標準,導致難以設計薪水等級。