python任职要求_Python —— 一个『拉勾网』的小爬虫

本文将展示一个 Python 爬虫,其目标网站是『拉勾网』;题图是其运行的结果,这个爬虫通过指定『关键字』抓取所有相关职位的『任职要求』,过滤条件有『城市』、『月薪范围』。并通过百度的分词和词性标注服务(免费的),提取其中的关键字(如题图),这个爬虫有什么用了?有那么一个问题模板,xx 语言 / 方向 xx 月薪需要掌握什么技能

对于这种问题,招聘网站上的信息大概是最为『公正客观』,所以这个爬虫的输出可以『公正客观』的作为求职者的技能树发展指南......个屁;如果全盘相信招聘网上写的,估计离凉凉就不远了。其上面写的东西一般都是泛泛而谈,大概率是这样的场景先用 5 分钟,把工作中用的各种系统先写上去,比如有一个接口调用是 HDFS 写文件,那就写上『熟悉 Hadoop 生态和分布式文件系统优先』,这样显得工作比较高大上;一定不能让人看出我们就是一个野鸡公司

再用 5 分钟,写些 比如『有较强的学习能力』、『责任感强』之类面试官都不一定有(多半没有)的废话

最后 5 分钟,改改错别字,强调下价值观之类的,搞定收工

所以这篇文章的目的,不是通过『抓取数据』然后通过对『数据的分析』自动的生成各种职位的『技能需求』。它仅仅是通过一个『短小』、『可以运行的』的代码,展示下如何抓取数据,并在这个具体实例中,介绍几个工具和一些爬虫技巧;引入分词有两个目的 1)对分词有个初步印象,尝试使用新的工具挖掘潜在的数据价值 2)相对的希望大家可以客观看待机器学习的能力和适用领域,指望一项技术可以解决所有问题是不切实际的

1 数据源

2 抓取工具

Python 3,并使用第三方库 Requests、lxml、AipNlp,代码共 100 + 行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):

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)

#附带 header,全部是固定值

s = requests.post(BASE_URL, headers=headers, params=params, data=data)

return s.json()

这里会附带这些 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):

headers = {"User-Agent": USER_AGENT, "Referer": REFERER, "Cookie": COOKIE}

url = DETAIL_URL.format(id)

#这是一个 GET 请求

#请求的 URL 是 https://www.lagou.com/jobs/职位 ID.html

#附带 header,全部是固定值

s = requests.get(url, headers=headers)

#返回的是一个 HTML 结构

return s.text

这个 URL 可以通过浏览器直接访问,比如 爬虫工程师招聘-360招聘-拉勾网

4.3 从『某职位』的详细信息中提取『任职要求』

从获取到的 HTML 中提取该职位的文字描述,这里是使用 lxml 的 xpath 来提取

//dd[@class="job_bt"]/div/p/text()

这个 xpath 语法,获取以下

标签内的所有内容,返回 ['文本内容', '文本内容', '文本内容']

...

...

...

文本内容

文本内容

文本内容

...

...

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点击 立即使用,进入登录页面 百度帐号(贴吧、网盘通用)点击创建应用,随便填写一些信息即可申请后,把 AppID、API Key、Secret Key 填入代码

5 抓取结果5 / 6 / 7 没有『任职要求』输出,是漏了还是真的没有?还是北京工资高,成都只有 1 个可能在 25k 以上的爬虫职位

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):

headers = {"User-Agent": USER_AGENT, "Referer": REFERER, "Cookie": COOKIE}

try:

url = DETAIL_URL.format(id)

print(url)

s = requests.get(url, headers=headers)

return s.text

except Exception as e:

print("fetch job detail fail. " + url)

print(e)

raise e

#抓取职位列表页

def fetch_list(page_index):

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, headers=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:])

欢迎关注我的专栏面向工资编程 —— 收录 Java 语言的面试向文章​zhuanlan.zhihu.com编程的基础和一些理论​zhuanlan.zhihu.com编程的日常娱乐 —— 用 Python 干些有趣的事​zhuanlan.zhihu.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值