python爬蟲: 爬取拉勾網職位並分析
0. 前言
本文從拉勾網爬取深圳市資料分析的職位資訊,並以CSV格式儲存至電腦, 之後進行資料清洗, 生成詞雲,進行描述統計和迴歸分析,最終得出結論.
1. 用到的軟體包
Python版本: Python3.6
requests: 下載網頁
math: 向上取整
time: 暫停程序
pandas:資料分析並儲存為csv檔案
matplotlib:畫圖
statsmodels:統計建模
wordcloud、scipy、jieba:生成中文詞雲
pylab:設定畫圖能顯示中文
2. 解析網頁
開啟Chrome,在拉勾網搜尋深圳市的“資料分析”職位,使用檢查功能檢視網頁原始碼,發現拉勾網有反爬蟲機制, 職位資訊並不在原始碼裡,而是儲存在JSON的檔案裡,因此我們直接下載JSON,並使用字典方法直接讀取資料.
抓取網頁時,需要加上頭部資訊, 才能獲取所需的資料.
def get_json(url,num):
'''''從網頁獲取JSON,使用POST請求,加上頭部資訊'''
my_headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36',
'Host':'www.lagou.com',
'Referer' :'https://www.lagou.com/jobs/list_%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90?labelWords=&fromSearch=true&suginput=',
'X-Anit-Forge-Code':'0',
'X-Anit-Forge-Token': 'None',
'X-Requested-With':'XMLHttpRequest'
}
my_data = {
'first' : 'true',
'pn':num,
'kd':'資料分析'}
res = requests.post(url, headers = my_headers, data = my_data)
res.raise_for_status()
res.encoding = 'utf-8'
# 得到包含職位資訊的字典
page = res.json()
return page
在搜尋結果的第一頁,我們可以從JSON裡讀取總職位數,按照每頁15個職位, 獲得要爬取的頁數. 再使用迴圈按頁爬取, 將職位資訊彙總, 輸出為CSV格式.
程式執行如圖:
抓取結果如圖:
3. 資料清洗
資料清洗佔資料分析工作量的大頭。在拉勾網搜尋深圳市的’資料分析’職位,結果得到369個職位。檢視職位名稱時,發現有4個實習崗位。由於我們研究的是全職崗位,所以先將實習崗位剔除。由於工作經驗和工資都是字串形式的區間,我們先用正則表示式提取數值,輸出列表形式。工作經驗取均值,工資取區間的四分位數值,比較接近現實。
# 資料清洗,剔除實習崗位
df.drop(df[df['職位名稱'].str.contains('實習')].index, inplace=True)
# print(df.describe())
# 由於CSV檔案內的資料是字串形式,先用正則表示式將字串轉化為列表,再取區間的均值
pattern = '\d+'
df['工作年限'] = df['工作經驗'].str.findall(pattern)
avg_work_year = []
for i in df['工作年限']:
# 如果工作經驗為'不限'或'應屆畢業生',那麼匹配值為空,工作年限為0
if len(i) == 0:
avg_work_year.append(0)
# 如果匹配值為一個數值,那麼返回該數值
elif len(i) == 1:
avg_work_year.append(int(''.join(i)))
# 如果匹配值為一個區間,那麼取平均值
else:
num_list = [int(j) for j in i]
avg_year = sum(num_list)/2
avg_work_year.append(avg_year)
df['經驗'] = avg_work_year
# 將字串轉化為列表,再取區間的前25%,比較貼近現實
df['salary'] = df['工資'].str.findall(pattern)
avg_salary = []
for k in df['salary']:
int_list = [int(n) for n in k]
avg_wage = int_list[0]+(int_list[1]-int_list[0])/4
avg_salary.append(avg_wage)
df['月工資'] = avg_salary
# 將清洗後的資料儲存,以便檢查
df.to_csv('draft.csv', index = False)
4. 詞雲
我們將職位福利這一列的資料彙總,生成一個字串,按照詞頻生成詞雲實現python視覺化。以下是原圖和詞雲的對比圖,可見五險一金在職位福利裡出現的頻率最高,平臺、福利、發展空間、彈性工作次之。
5. 描述統計
可知,資料分析師的均值在14.6K,中位數在12.5K,算是較有前途的職業。資料分析散佈在各個行業,但在高階層面上涉及到資料探勘和機器學習,在IT業有長足的發展。
我們再來看工資的分佈,這對於求職來講是重要的參考:
工資在10-15K的職位最多,在15-20K的職位其次。個人愚見,10-15K的職位以建模為主,20K以上的職位以資料探勘、大資料架構為主。
我們再來看職位在各區的分佈:
資料分析職位有62.9%在南山區,有25.8%在福田區,剩下少數分佈在龍崗區、羅湖區、寶安區、龍華新區。我們以小窺大,可知南山區和福田區是深圳市科技業的中心。
6. 實證統計
我們希望獲得工資與工作經驗、學歷的關係,由於學歷分三類,需設定3個虛擬變數:大專、本科、碩士。多元迴歸結果如下:
在0.05的顯著性水平下,F值為82.53,說明迴歸關係是顯著的。t檢驗和對應的P值都小於0.05表明,工作經驗和3種學歷在統計上都是顯著的。另外,R-squared的值為0.41,說明工作經驗和學歷僅僅解釋了工資變異性的41%。這點不難理解,即使職位都叫資料分析師,實際的工作內容差異比較大,有的只是用Excel做基本分析,有的用Python、R做資料探勘。另外,各個公司的規模和它願意開出的工資也不盡相同。而工作內容的差異和公司的大方程度是很難單憑招聘網頁上的宣傳而獲得實際資料,導致了模型的擬合優度不是很好這一現實。
由於迴歸模型總體是顯著的,我們可以將自變數的值代入迴歸方程,獲得各個學歷的工資的期望值E。對於資料分析職位,以1年工作經驗為例,大專學歷的期望工資是7.8K,本科學歷的期望工資是10.8K,碩士學歷的期望工資是17.6K。這證實了‘知識改變命運’這一說法。
7. 完整程式碼
由於每次執行爬蟲耗時約30分鐘,而執行資料分析耗時幾秒鐘,我們將兩部分的工作單獨執行,以節省資料分析的時間。
7.1 爬蟲部分的程式碼
import requests
import math
import pandas as pd
import time
def get_json(url,num):
'''''從網頁獲取JSON,使用POST請求,加上頭部資訊'''
my_headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36',
'Host':'www.lagou.com',
'Referer':'https://www.lagou.com/jobs/list_%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90?labelWords=&fromSearch=true&suginput=',
'X-Anit-Forge-Code':'0',
'X-Anit-Forge-Token': 'None',
'X-Requested-With':'XMLHttpRequest'
}
my_data = {
'first': 'true',
'pn':num,
'kd':'資料分析'}
res = requests.post(url, headers = my_headers, data = my_data)
res.raise_for_status()
res.encoding = 'utf-8'
# 得到包含職位資訊的字典
page = res.json()
return page
def get_page_num(count):
'''''計算要抓取的頁數'''
# 每頁15個職位,向上取整
res = math.ceil(count/15)
# 拉勾網最多顯示30頁結果
if res > 30:
return 30
else:
return res
def get_page_info(jobs_list):
'''''對一個網頁的職位資訊進行解析,返回列表'''
page_info_list = []
for i in jobs_list:
job_info = []
job_info.append(i['companyFullName'])
job_info.append(i['companyShortName'])
job_info.append(i['companySize'])
job_info.append(i['financeStage'])
job_info.append(i['district'])
job_info.append(i['positionName'])
job_info.append(i['workYear'])
job_info.append(i['education'])
job_info.append(i['salary'])
job_info.append(i['positionAdvantage'])
page_info_list.append(job_info)
return page_info_list
def main():
url = 'https://www.lagou.com/jobs/positionAjax.json?city=%E6%B7%B1%E5%9C%B3&needAddtionalResult=false'
# 先設定頁數為1,獲取總的職位數
page_1 = get_json(url,1)
total_count = page_1['content']['positionResult']['totalCount']
num = get_page_num(total_count)
total_info = []
time.sleep(20)
print('職位總數:{},頁數:{}'.format(total_count,num))
for n in range(1,num+1):
# 對每個網頁讀取JSON, 獲取每頁資料
page = get_json(url,n)
jobs_list = page['content']['positionResult']['result']
page_info = get_page_info(jobs_list)
total_info += page_info
print('已經抓取第{}頁, 職位總數:{}'.format(n, len(total_info)))
# 每次抓取完成後,暫停一會,防止被伺服器拉黑
time.sleep(30)
#將總資料轉化為data frame再輸出
df = pd.DataFrame(data = total_info,columns = ['公司全名','公司簡稱','公司規模','融資階段','區域','職位名稱','工作經驗','學歷要求','工資','職位福利'])
df.to_csv('lagou_jobs.csv',index = False)
print('已儲存為csv檔案.')
if __name__== "__main__":
main()
7.2 資料分析部分的程式碼
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
from wordcloud import WordCloud
from scipy.misc import imread
import jieba
from pylab import mpl
# 使matplotlib模組能顯示中文
mpl.rcParams['font.sans-serif'] = ['SimHei'] # 指定預設字型
mpl.rcParams['axes.unicode_minus'] = False # 解決儲存影象是負號'-'顯示為方塊的問題
# 讀取資料
df = pd.read_csv('lagou_jobs.csv', encoding = 'gbk')
# 資料清洗,剔除實習崗位
df.drop(df[df['職位名稱'].str.contains('實習')].index, inplace=True)
# print(df.describe())
# 由於CSV檔案內的資料是字串形式,先用正則表示式將字串轉化為列表,再取區間的均值
pattern = '\d+'
df['工作年限'] = df['工作經驗'].str.findall(pattern)
avg_work_year = []
for i in df['工作年限']:
# 如果工作經驗為'不限'或'應屆畢業生',那麼匹配值為空,工作年限為0
if len(i) == 0:
avg_work_year.append(0)
# 如果匹配值為一個數值,那麼返回該數值
elif len(i) == 1:
avg_work_year.append(int(''.join(i)))
# 如果匹配值為一個區間,那麼取平均值
else:
num_list = [int(j) for j in i]
avg_year = sum(num_list)/2
avg_work_year.append(avg_year)
df['經驗'] = avg_work_year
# 將字串轉化為列表,再取區間的前25%,比較貼近現實
df['salary'] = df['工資'].str.findall(pattern)
avg_salary = []
for k in df['salary']:
int_list = [int(n) for n in k]
avg_wage = int_list[0]+(int_list[1]-int_list[0])/4
avg_salary.append(avg_wage)
df['月工資'] = avg_salary
# 將清洗後的資料儲存,以便檢查
df.to_csv('draft.csv', index = False)
# 描述統計
print('資料分析師工資描述:\n{}'.format(df['月工資'].describe()))
# 繪製頻率直方圖並儲存
plt.hist(df['月工資'],bins = 12)
plt.xlabel('工資 (千元)')
plt.ylabel('頻數')
plt.title("工資直方圖")
plt.savefig('histogram.jpg')
plt.show()
# 繪製餅圖並儲存
count = df['區域'].value_counts()
# 將龍華區和龍華新區的資料彙總
count['龍華新區'] += count['龍華區']
del count['龍華區']
plt.pie(count, labels = count.keys(),labeldistance=1.4,autopct='%2.1f%%')
plt.axis('equal') # 使餅圖為正圓形
plt.legend(loc='upper left', bbox_to_anchor=(-0.1, 1))
plt.savefig('pie_chart.jpg')
plt.show()
# 繪製詞雲,將職位福利中的字串彙總
text = ''
for line in df['職位福利']:
text += line
# 使用jieba模組將字串分割為單詞列表
cut_text = ' '.join(jieba.cut(text))
color_mask = imread('cloud.jpg') #設定背景圖
cloud = WordCloud(
font_path = 'yahei.ttf',
background_color = 'white',
mask = color_mask,
max_words = 1000,
max_font_size = 100
)
word_cloud = cloud.generate(cut_text)
# 儲存詞雲圖片
word_cloud.to_file('word_cloud.jpg')
plt.imshow(word_cloud)
plt.axis('off')
plt.show()
# 實證統計,將學歷不限的職位要求認定為最低學歷:大專
df['學歷要求'] = df['學歷要求'].replace('不限','大專')
# 學歷分為大專\本科\碩士,將它們設定為虛擬變數
dummy_edu = pd.get_dummies(df['學歷要求'],prefix = '學歷')
# 構建迴歸陣列
df_with_dummy = pd.concat([df['月工資'],df['經驗'],dummy_edu],axis = 1)
# 建立多元迴歸模型
y = df_with_dummy['月工資']
X = df_with_dummy[['經驗','學歷_大專','學歷_本科','學歷_碩士']]
X=sm.add_constant(X)
model = sm.OLS(y,X)
results = model.fit()
print('迴歸方程的引數:\n{}\n'.format(results.params))
print('迴歸結果:\n{}'.format(results.summary()))