本文將展示一個 Python 爬蟲,其標的網站是『拉勾網』;題圖是其執行的結果,這個爬蟲透過指定『關鍵字』抓取所有相關職位的『任職要求』,過濾條件有『城市』、『月薪範圍』。並透過百度的分詞和詞性標註服務(免費的),提取其中的關鍵字,這個爬蟲有什麼用?
有那麼一個問題模板,xx 語言 / 方向 xx 月薪需要掌握什麼技能
對於這種問題,招聘網站上的資訊大概是最為『公正客觀』,所以這個爬蟲的輸出可以『公正客觀』的作為求職者的技能樹發展指南……個屁;如果全盤相信招聘網上寫的,估計離涼涼就不遠了。其上面寫的東西一般都是泛泛而談,大機率是這樣的場景:
-
先用 5 分鐘,把工作中用的各種系統先寫上去,比如有一個介面呼叫是 HDFS 寫檔案,那就寫上『熟悉 Hadoop 生態和分散式檔案系統優先』,這樣顯得工作比較高大上;一定不能讓人看出我們就是一個野雞公司;
-
再用 5 分鐘,寫些 比如『有較強的學習能力』、『責任感強』之類面試官都不一定有(多半沒有)的廢話;
-
最後 5 分鐘,改改錯別字,強調下價值觀之類的,搞定收工。
所以這篇文章的目的,不是透過『抓取資料』然後透過對『資料的分析』自動的生成各種職位的『技能需求』。它僅僅是透過一個『短小』、『可以執行的』的程式碼,展示下如何抓取資料,併在這個具體實體中,介紹幾個工具和一些爬蟲技巧;引入分詞有兩個目的 1)對分詞有個初步印象,嘗試使用新的工具挖掘潛在的資料價值 2)相對的希望大家可以客觀看待機器學習的能力和適用領域,指望一項技術可以解決所有問題是不切實際的。
1.資料源
『拉勾網』
2.抓取工具
Python 3,並使用第三方庫 Requests、lxml、AipNlp,程式碼共 100 + 行。
-
安裝 Python 3,Download Python
-
Requests: 讓 HTTP 服務人類 ,Requests 是一個結構簡單且易用的 Python HTTP 庫,幾行程式碼就可以發起一個 HTTP 請求,並且有中文檔案
-
Processing XML and HTML with Python ,lxml 是用於解析 HTML 頁面結構的庫,功能強大,但在程式碼裡我們只需要用到其中一個小小的功能
-
語言處理基礎技術-百度AI,AipNlp 是百度雲推出的自然語言處理服務庫。其是遠端呼叫後臺介面,而不是使用本地模型執行,所以不能離線使用。之前寫過一篇文章介紹了幾個分詞庫 Python 中的那些中文分詞器,這裡為什麼選用百度雲的分詞服務,是因為經過對拉勾的資料驗證(其實就是拍腦袋),百度雲的效果更好。該服務是免費的,具體如何申請會在 4.4 描述
-
以上 三個庫 都可以透過 pip 安裝,一行命令
3.實現程式碼
見本文末尾。
4.邏輯拆解
以下過程建議對比 Chrome 或 Firefox 瀏覽器的開發者工具。
4.1 拉取『關鍵字』的相關職位串列
透過構造『拉勾網』的搜尋 HTTP 請求,拉取『關鍵字』的相關職位串列:
1)同時指定過濾條件『城市』和『月薪範圍』
2)HTTP 響應的職位串列是 Json 格式,且是分頁結構,需要指定頁號多次請求才能獲取所有相關職位串列
def fetch_list(page_index):
essay-headers = {"User-Agent": USER_AGENT, "Referer": REFERER, "Cookie": COOKIE}
params = {"px": "default", "city": CITY, "yx": SALARY}
data = {"first": page_index == 1, "pn": page_index, "kd": KEY}
#這是一個 POST 請求,請求的 URL 是一個固定值 https://www.lagou.com/jobs/positionAjax.json
#附帶的資料 HTTP body,其中 pn 是當前分頁頁號,kd 是關鍵字
#附帶的 Query 引數,city 是城市(如 北京),yx 是工資範圍(如 10k-15k)
#附帶 essay-header,全部是固定值
s = requests.post(BASE_URL, essay-headers=essay-headers, params=params, data=data)
return s.json()
這裡會附帶這些 essay-header,是為了避免『拉勾網』的反爬蟲策略。這裡如果移除 referer 或修改 referer 值,會發現得不到期望的 json 響應;如果移除 cookie,會發現過幾個請求就被封了。其傳回 json 格式的響應:
#串列 json 結構
{
...
"content": {
"pageNo": 當前串列分頁號
...
"positionResult": {
...
resultSize: 該串列的招聘職位數量,如果該值為 0,則代表所有資訊也被獲取
result: 陣列,該頁中所有招聘職位的相關資訊
...
},
}
...
}
#招聘職位資訊 json 結構
{
...
"companyFullName": "公司名稱",
"city": "城市",
"education": "學歷要求",
"salary": "月薪範圍",
"positionName": "職位名稱",
"positionId": "職位 ID,後續要使用該 ID 抓取職位的詳情頁資訊"
}
透過遍歷傳回 json 結構中 [“positionResult”][“result”] 即可得到該頁所有職位的簡略資訊。
4.2 拉取『某職位』的詳細資訊
當透過 4.1 獲取某一頁職位串列時,同時會得到這些職位的 ID。透過 ID,可以獲取這些這些職位的詳細資訊:
def fetch_detail(id):
essay-headers = {"User-Agent": USER_AGENT, "Referer": REFERER, "Cookie": COOKIE}
url = DETAIL_URL.format(id)
#這是一個 GET 請求
#請求的 URL 是 https://www.lagou.com/jobs/職位 ID.html
#附帶 essay-header,全部是固定值
s = requests.get(url, essay-headers=essay-headers)
#傳回的是一個 HTML 結構
return s.text
這個 URL 可以透過瀏覽器直接訪問,比如 爬蟲工程師招聘-360招聘-拉勾網
4.3 從『某職位』的詳細資訊中提取『任職要求』
從獲取到的 HTML 中提取該職位的文字描述,這裡是使用 lxml 的 xpath 來提取:
//dd[@class="job_bt"]/div/p/text()
這個 xpath 語法,獲取以下
標簽內的所有內容,傳回 [‘文字內容’, ‘文字內容’, ‘文字內容’]:
<html>
...
<dd class="job_bt">
...
<div>
...
<p>文字內容p>
<p>文字內容p>
<p>文字內容p>
…
div>
dd>
…
html>
xpath 的基礎語法學習,參考 XPath 教程。它和 css 選擇器語法可以認為是爬蟲必須掌握的基本知識。
獲取到這些文字陣列後,為了提取『任職要求』,使用了一個非常粗暴的正則運算式:
\w?[\.、 ::]?(任職要求|任職資格|我們希望你|任職條件|崗位要求|要求:|職位要求|工作要求|職位需求)
標記文字陣列中職位要求的開始,並將後續所有以符號 – 或 數字 開頭的文字認為為『任職要求』。這樣我們就從 爬蟲工程師招聘-360招聘-拉勾網 獲取到『任職要求』:
-
有扎實的資料結構和演演算法功底;
-
工作認真細緻踏實,有較強的學習能力,熟悉常用爬蟲工具;
-
熟悉linux開發環境,熟悉python等;
-
理解http,熟悉html, DOM, xpath, scrapy優先;
-
有爬蟲,資訊抽取,文字分類相關經驗者優先;
瞭解Hadoop、Spark等大資料框架和流處理技術者優先。
以上提取『任職要求』的方法存在一定的錯誤率,也會遺漏一些。這是因為『拉勾網』的『職位詳情』文字描述多樣性,以及粗暴的正則過濾邏輯導致的。有興趣的同學可以考慮結合實際進行改進。
4.4 使用百度 AipNlp 進行分詞和詞性標註
分詞和詞性標註服務非常容易使用
from aip import AipNlp
client = AipNlp(APP_ID, API_KEY, SECRET_KEY)
text = "瞭解Hadoop、Spark等大資料框架和流處理技術者優先。"
client.lexer(text)
程式碼中,除了呼叫該介面,會進一步對傳回結構進行加工。具體程式碼見本文末尾,在 segment 方法中。簡略用文字描述,把結果中詞性為其他專名和命令物體型別詞單獨列出來,其餘名詞性的詞也提取出來並且如果連在一起則合併在一起(這麼做,只是觀察過幾個例子後決定的;工程實踐中,需要制定一個標準並對比不同方法的優劣,不應該像這樣拍腦袋決定)。百度分詞服務的詞性標註含義 自然語言處理-常見問題-百度雲
『任職要求』經過分詞和詞性標註處理後的結果如下:
Hadoop/Spark/http/爬蟲/xpath/資料框架/scrapy/資訊/資料結構/html/學習能力/開發環
境/linux/爬蟲工具/演演算法功底/DOM/流處理技術者/python/文字分類相關經驗者
這樣我們就完成了這整套邏輯,透過迴圈請求 4.1,完成『關鍵字』的所有職位資訊的抓取和『任職要求』的提取 / 分析。
百度的分詞和詞性標註服務需要申請,申請後得到 APP_ID, API_KEY, SECRET_KEY 並填入程式碼從來正常工作,申請流程如下,點選連結 語言處理基礎技術-百度AI。
5.抓取結果
6 結語
-
如果實在不想申請百度雲服務,可以使用其他的分詞庫 Python 中的那些中文分詞器;對比下效果,也許有驚喜
-
示例實現了一個基本且完整的結構,在這基礎有很多地方可以很容易的修改 1)抓取多個城市以及多個薪資範圍 2)增加過濾條件,比如工作經驗和行業 3)將分詞和爬蟲過程分離,解耦邏輯,也方便斷點續爬 4)分析其他資料,比如薪資和城市關係、薪資和方向的關係、薪資和『任職要求』的關係等
-
Mac 上實現的,Windows 沒測過,理論上應該同樣沒問題。如果有同學爬過並願意給我說下結果,那實在太感謝了
-
寫爬蟲,有個節操問題,不要頻次太高。特別這種出於興趣的程式碼,裡面的 sleep 時間不要改小
附 程式碼和部分註釋
#coding: utf-8
import time
import re
import urllib.parse
import requests
from lxml import etree
KEY = "爬蟲" #抓取的關鍵字
CITY = "北京" #標的城市
# 0:[0, 2k), 1: [2k, 5k), 2: [5k, 10k), 3: [10k, 15k), 4: [15k, 25k), 5: [25k, 50k), 6: [50k, +inf)
SALARY_OPTION = 3 #薪資範圍,值範圍 0 ~ 6,其他值代表無範圍
#進入『拉勾網』任意頁面,無需登入
#開啟 Chrome / Firefox 的開發者工具,從中複製一個 Cookie 放在此處
#防止被封,若無法拉取任何資訊,首先考慮換 Cookie
COOKIE = "JSESSIONID=ABAAABAACBHABBI7B238FB0BC8B6139070838B4D2D31CED; _ga=GA1.2.201890914.1522471658; _gat=1; Hm_lvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1522471658; Hm_lpvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1522471674; user_trace_token=20180331124738-a3407f45-349e-11e8-a62b-525400f775ce; LGSID=20180331124738-a34080db-349e-11e8-a62b-525400f775ce; PRE_UTM=; PRE_HOST=; PRE_SITE=; PRE_LAND=https%3A%2F%2Fwww.lagou.com%2F; LGRID=20180331124753-ac447493-349e-11e8-b664-5254005c3644; LGUID=20180331124738-a3408251-349e-11e8-a62b-525400f775ce; _gid=GA1.2.24217288.1522471661; index_location_city=%E6%88%90%E9%83%BD; TG-TRACK-CODE=index_navigation"
def init_segment():
#按照 4.4 的方式,申請百度雲分詞,並填寫到下麵
APP_ID = "xxxxxxxxx"
API_KEY = "xxxxxxxxx"
SECRET_KEY = "xxxxxxxxx"
from aip import AipNlp
#保留如下詞性的詞 https://cloud.baidu.com/doc/NLP/NLP-FAQ.html#NLP-FAQ
retains = set(["n", "nr", "ns", "s", "nt", "an", "t", "nw", "vn"])
client = AipNlp(APP_ID, API_KEY, SECRET_KEY)
def segment(text):
'''
對『任職資訊』進行切分,提取資訊,併進行一定處理
'''
try:
result = []
#呼叫分詞和詞性標註服務,這裡使用正則過濾下輸入,是因為有特殊字元的存在
items = client.lexer(re.sub('\s', '', text))["items"]
cur = ""
for item in items:
#將連續的 retains 中詞性的詞合併起來
if item["pos"] in retains:
cur += item["item"]
continue
if cur:
result.append(cur)
cur = ""
#如果是 命名物體型別 或 其它專名 則保留
if item["ne"] or item["pos"] == "nz":
result.append(item["item"])
if cur:
result.append(cur)
return result
except Exception as e:
print("fail to call service of baidu nlp.")
return []
return segment
#以下無需修改,拉取『拉勾網』的固定引數
SALARY_INTERVAL = ("2k以下", "2k-5k", "5k-10k", "10k-15k", "15k-25k", "25k-50k", "50k以上")
if SALARY_OPTION < len(SALARY_INTERVAL) and SALARY_OPTION >= 0:
SALARY = SALARY_INTERVAL[SALARY_OPTION]
else:
SALARY = None
USER_AGENT = "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.5 Safari/534.55.3"
REFERER = "https://www.lagou.com/jobs/list_" + urllib.parse.quote(KEY)
BASE_URL = "https://www.lagou.com/jobs/positionAjax.json"
DETAIL_URL = "https://www.lagou.com/jobs/{0}.html"
#抓取職位詳情頁
def fetch_detail(id):
essay-headers = {"User-Agent": USER_AGENT, "Referer": REFERER, "Cookie": COOKIE}
try:
url = DETAIL_URL.format(id)
print(url)
s = requests.get(url, essay-headers=essay-headers)
return s.text
except Exception as e:
print("fetch job detail fail. " + url)
print(e)
raise e
#抓取職位串列頁
def fetch_list(page_index):
essay-headers = {"User-Agent": USER_AGENT, "Referer": REFERER, "Cookie": COOKIE}
params = {"px": "default", "city": CITY, "yx": SALARY}
data = {"first": page_index == 1, "pn": page_index, "kd": KEY}
try:
s = requests.post(BASE_URL, essay-headers=essay-headers, params=params, data=data)
return s.json()
except Exception as e:
print("fetch job list fail. " + data)
print(e)
raise e
#根據 ID 抓取詳情頁,並提取『任職資訊』
def fetch_requirements(result, segment):
time.sleep(2)
requirements = {}
content = fetch_detail(result["positionId"])
details = [detail.strip() for detail in etree.HTML(content).xpath('//dd[@class="job_bt"]/div/p/text()')]
is_requirement = False
for detail in details:
if not detail:
continue
if is_requirement:
m = re.match("([0-9]+|-)\s*[\.::、]?\s*", detail)
if m:
words = segment(detail[m.end():])
for word in words:
if word not in requirements:
requirements[word] = 1
else:
requirements[word] += 1
else:
break
elif re.match("\w?[\.、 ::]?(任職要求|任職資格|我們希望你|任職條件|崗位要求|要求:|職位要求|工作要求|職位需求)", detail):
is_requirement = True
return requirements
#迴圈請求職位串列
def scrapy_jobs(segment):
#用於過濾相同職位
duplications = set()
#從頁 1 開始請求
page_index = 1
job_count = 0
print("key word {0}, salary {1}, city {2}".format(KEY, SALARY, CITY))
stat = {}
while True:
print("current page {0}, {1}".format(page_index, KEY))
time.sleep(2)
content = fetch_list(page_index)["content"]
# 全部頁已經被請求
if content["positionResult"]["resultSize"] == 0:
break
results = content["positionResult"]["result"]
total = content["positionResult"]["totalCount"]
print("total job {0}".format(total))
# 處理該頁所有職位資訊
for result in results:
if result["positionId"] in duplications:
continue
duplications.add(result["positionId"])
job_count += 1
print("{0}. {1}, {2}, {3}".format(job_count, result["positionName"], result["salary"], CITY))
requirements = fetch_requirements(result, segment)
print("/".join(requirements.keys()) + "\n")
#把『任職資訊』資料統計到 stat 中
for key in requirements:
if key not in stat:
stat[key] = requirements[key]
else:
stat[key] += requirements[key]
page_index += 1
return stat
segment = init_segment()
stat = scrapy_jobs(segment)
#將所有『任職資訊』根據提及次數排序,輸出前 10 位
import operator
sorted_stat = sorted(stat.items(), key=operator.itemgetter(1))
print(sorted_stat[-10:])
本文轉載自
知乎專欄:https://zhuanlan.zhihu.com/p/35140404
作者:鄧卓
《Python人工智慧和全棧開發》2018年07月23日即將在北京開課,120天衝擊Python年薪30萬,改變速約~~~~
*宣告:推送內容及圖片來源於網路,部分內容會有所改動,版權歸原作者所有,如來源資訊有誤或侵犯權益,請聯絡我們刪除或授權事宜。
– END –
更多Python好文請點選【閱讀原文】哦
↓↓↓