Journey

The journey is the destination.

0%

104人力銀行 網路爬蟲

先前曾發過一篇透過 Selenium 的版本 爬蟲_104人力銀行工作清單 ,但隨著爬蟲技術的提升,就想要來更新一下爬蟲的程式 xD

註:本篇文章僅供研究使用,請勿惡意大量爬取資料造成對方公司的負擔

104人力銀行 爬蟲要點

  1. 每次查詢職缺的結果最多回傳20筆*150個分頁的資料量,也就是說當我們查詢的條件超過3000筆時更多的資料就會被截掉。怎麼樣才能抓完完整的資料呢?
    • 解決方式是透過條件來將查詢的結果細緻化,例如按照區域、職務類型將查詢結果細分,最後再進行合併
  2. 因為資料量相當多,如果要抓完完整的資料會需要約1整天的時間,那麼一次抓不完的話要怎麼辦呢?
    • 一種解決方式是用多線程的方式來爬資料,加速爬蟲的效率
    • 如果是用一般的方式爬蟲的話,可以將爬取的結果分階段保存資料,而下次爬蟲時只需要繼續爬未完成的資料即可
  3. 承1,由於我們將查詢結果按照地區與職務進行細分,大多數組合並不會都有150個分頁的資料量,如果將每個組合都送 150 次 request 無疑會浪費相當多時間,怎麼樣可以更有效率呢?
    • 解決方式是檢查每次查詢的結果,如果回傳20筆職缺,表示下一個分頁還有資料,但如果小於20個分頁就表示後面沒有資料了
  4. 雖然 104 沒有設定反爬蟲機制,但是當 request 的速度太快時仍偶爾會有連線失敗的情況發生,但我們不想因此就讓每次 request 都 sleep 一次而降低爬蟲的效率,這時候該如何處理呢?
    • 解決方式是透過 try-except,當 try 成功時才繼續前往下一個分頁,如果失敗了就將同一頁的網址再送一次 request 取資料

104人力銀行 爬蟲流程

載入使用套件

1
2
3
4
5
6
7
8
import requests
from bs4 import BeautifulSoup
import pandas as pd
import json
import re
import time
import os
from IPython.display import clear_output

設定 request 參數

出於習慣就順手加上 user-agents了

1
headers = {'User-Agent':'GoogleBot'}

細分查詢結果

同第 1 個爬蟲要點所述,因為資料量龐大,我們需要將查詢結果細分才能確保抓到完整的資料,而在這裡細分的方式是依照地區代碼和職務類型進行查詢。

地區代碼

1
2
3
4
5
6
7
8
9
10
11
url = 'https://static.104.com.tw/category-tool/json/Area.json'
resp = requests.get(url)
df1 = []
for i in resp.json()[0]['n']:
ndf = pd.DataFrame(i['n'])
ndf['city'] = i['des']
df1.append(ndf)
df1=pd.concat(df1, ignore_index=True)
df1 = df1.loc[:,['city','des','no']]
df1 = df1.sort_values('no')
df1

職務代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
url= 'https://static.104.com.tw/category-tool/json/JobCat.json'
resp = requests.get(url)
df2 = []
for i in resp.json():
for j in i['n']:
ndf = pd.DataFrame(j['n'])
ndf['des1'] = i['des']# 職務大分類
ndf['des2'] = j['des']# 職務小分類
df2.append(ndf)
df2 = pd.concat(df2, ignore_index=True)
df2 = df2.loc[:,['des1', 'des2', 'des', 'no']]
df2 = df2.sort_values('no')
df2

讀取先前的爬取結果

同第 2 個爬蟲要點所述,如果是第一次執行程式這段語法可以不用執行,而如果先前有保存結果的話會在這裡偵測先前爬過哪些資料

1
2
3
4
tmp = pd.DataFrame([re.sub('\.pkl','',file)for file in os.listdir('./data')],columns=['no'])
df1 = pd.merge(df1, tmp, how='left',on='no',indicator=True)
df1 = df1.loc[df1['_merge']!='both',:]
df1

爬取職缺資料

  • 這裡是處理第 3 和第 4 點的爬蟲要點,因為不會每個查詢組合都有 150 個分頁的資料,因此需要偵測 request 回來幾筆職缺的資料,等於 20 筆就繼續爬下一個分頁的資料;而小於 20 筆資料則打破迴圈轉去爬下一個組合的職缺資料。
  • 而 request 太快時偶爾會失敗,因此需要用 try-except 函數,當 request 失敗就重新再爬一次資料
  • 最後就是要從 request 回來的結果做一些解析,讓我們從半結構化的資料中萃取成結構化的資料,因為是比較瑣碎的定位 elemnet 在這裡就不多說明了!
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    columns = ['公司名稱','公司編號','公司類別','公司類別描述', '公司連結','職缺名稱','職務性質','職缺大分類', '職缺中分類','職缺小分類', '職缺編號', '職務內容','更新日期', '職缺連結', '標籤','公司地址','地區','經歷','學歷']

    for areades, areacode in zip(df1['des'],df1['no']):
    values = []
    for jobdes1, jobdes2, jobdes, jobcode in zip(df2['des1'], df2['des2'], df2['des'], df2['no']):
    print(areades, ' | ', jobdes1, ' - ', jobdes2, ' - ' ,jobdes)
    page = 1
    while page <150:
    try:
    url = 'https://www.104.com.tw/jobs/search/?ro=0&jobcat={}&jobcatExpansionType=1&area={}&order=11&asc=0&page={}&mode=s&jobsource=2018indexpoc'.format(jobcode, areacode, page)
    print(url)
    resp = requests.get(url,headers=headers)
    soup = BeautifulSoup(resp.text)
    soup2 = soup.find('div',{'id':'js-job-content'}).findAll('article',{'class':'b-block--top-bord job-list-item b-clearfix js-job-item'})
    print(len(soup2))

    for job in soup2:

    update_date = job.find('span',{'class':'b-tit__date'}).text
    update_date = re.sub('\r|\n| ','',update_date)

    try:
    address = job.select('ul > li > a')[0]['title']
    address = re.findall('公司住址:(.*?)$',address)[0]
    except:
    address = ''

    loc = job.find('ul',{'class':'b-list-inline b-clearfix job-list-intro b-content'}).findAll('li')[0].text
    exp = job.find('ul',{'class':'b-list-inline b-clearfix job-list-intro b-content'}).findAll('li')[1].text
    try:
    edu = job.find('ul',{'class':'b-list-inline b-clearfix job-list-intro b-content'}).findAll('li')[2].text
    except:
    edu = ''

    try:
    content = job.find('p').text
    except:
    content = ''
    try:
    tags = [tag.text for tag in soup2[0].find('div',{'class':'job-list-tag b-content'}).findAll('span')]
    except:
    tags = []


    value = [job['data-cust-name'], # 公司名稱
    job['data-cust-no'], # 公司編號
    job['data-indcat'], # 公司類別
    job['data-indcat-desc'], # 公司類別描述
    job.select('ul > li > a')[0]['href'], # 公司連結
    job['data-job-name'],# 職缺名稱
    job['data-job-ro'], # 職務性質 _判斷全職兼職 1全職/2兼職/3高階/4派遣/5接案/6家教
    jobdes1, # 職缺大分類
    jobdes2, # 職缺中分類
    jobdes, # 職缺小分類
    job['data-job-no'],# 職缺編號
    content, # 職務內容
    update_date, # 更新日期
    job.find('a',{'class':'js-job-link'})['href'], # 職缺連結
    tags, # 標籤
    address,# 公司地址
    loc, # 地區
    exp,# 經歷
    edu # 學歷
    ]
    values.append(value)

    page+=1
    print(len(values))
    if len(soup2) < 20:
    break
    except:
    print('Retry')

    df = pd.DataFrame()
    df = pd.DataFrame(values, columns=columns)
    df.to_pickle('./data/' + areacode + '.pkl')
    clear_output()
    print('=================================== Save Data ===================================')

組合爬蟲結果

  • 由於先前將職缺資訊分成許多小查詢來抓資料並存成不同的檔案,因此在這裡我們就寫個簡單的迴圈來讀取與合併資料吧!
    1
    2
    3
    4
    5
    6
    df = []
    for i in os.listdir('./data/'):
    ndf = pd.read_pickle('./data/' + i)
    df.append(ndf)
    df = pd.concat(df, ignore_index=True)
    df.info()
  • 如圖所示,我們抓到的許多有價值的資訊,包含
    • 公司名稱
    • 公司編號
    • 公司類別
    • 公司類別描述
    • 公司連結
    • 職缺名稱
    • 職務性質
    • 職缺大分類
    • 職缺中分類
    • 職缺小分類
    • 職缺編號
    • 職務內容
    • 更新日期
    • 職缺連結
    • 標籤
    • 公司地址
    • 地區
    • 經歷要求
    • 學歷要求

後記

有了這些資料我們就可以進行許多有價值的分析囉,包含各縣市的職缺數量、產業結構差異,並且可以進一步幫助求職者們在選擇工作時可以評估自己個興趣與期望的工作地點等等!

附件