前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python —— 一个『拉勾网』的小爬虫

Python —— 一个『拉勾网』的小爬虫

作者头像
小小科
发布2018-05-04 12:19:35
1.3K0
发布2018-05-04 12:19:35
举报
文章被收录于专栏:北京马哥教育北京马哥教育

本文将展示一个 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 格式,且是分页结构,需要指定页号多次请求才能获取所有相关职位列表

代码语言:javascript
复制
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 格式的响应:

代码语言:javascript
复制
#列表 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,可以获取这些这些职位的详细信息:

代码语言:javascript
复制
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 来提取:

代码语言:javascript
复制
//dd[@class="job_bt"]/div/p/text()

这个 xpath 语法,获取以下 <p> 标签内的所有内容,返回 ['文本内容', '文本内容', '文本内容']:

代码语言:javascript
复制
<html>
...
  <dd class="job_bt">
    ...
    <div>
      ...
      <p>文本内容</p>
      <p>文本内容</p>
      <p>文本内容</p>
      ...
    </div>
  </dd>
...
</html>

xpath 的基础语法学习,参考 XPath 教程。它和 css 选择器语法可以认为是爬虫必须掌握的基本知识。

获取到这些文本数组后,为了提取『任职要求』,使用了一个非常粗暴的正则表达式:

代码语言:javascript
复制
\w?[\.、 ::]?(任职要求|任职资格|我们希望你|任职条件|岗位要求|要求:|职位要求|工作要求|职位需求)

标记文本数组中职位要求的开始,并将后续所有以符号 - 或 数字 开头的文本认为为『任职要求』。这样我们就从 爬虫工程师招聘-360招聘-拉勾网 获取到『任职要求』:

  • 有扎实的数据结构和算法功底;
  • 工作认真细致踏实,有较强的学习能力,熟悉常用爬虫工具;
  • 熟悉linux开发环境,熟悉python等;
  • 理解http,熟悉html, DOM, xpath, scrapy优先;
  • 有爬虫,信息抽取,文本分类相关经验者优先; 了解Hadoop、Spark等大数据框架和流处理技术者优先。

以上提取『任职要求』的方法存在一定的错误率,也会遗漏一些。这是因为『拉勾网』的『职位详情』文本描述多样性,以及粗暴的正则过滤逻辑导致的。有兴趣的同学可以考虑结合实际进行改进。

4.4 使用百度 AipNlp 进行分词和词性标注

分词和词性标注服务非常容易使用

代码语言:javascript
复制
from aip import AipNlp
client = AipNlp(APP_ID, API_KEY, SECRET_KEY)

text = "了解Hadoop、Spark等大数据框架和流处理技术者优先。"
client.lexer(text)

代码中,除了调用该接口,会进一步对返回结构进行加工。具体代码见本文末尾,在 segment 方法中。简略用文字描述,把结果中词性为其他专名和命令实体类型词单独列出来,其余名词性的词也提取出来并且如果连在一起则合并在一起(这么做,只是观察过几个例子后决定的;工程实践中,需要制定一个标准并对比不同方法的优劣,不应该像这样拍脑袋决定)。百度分词服务的词性标注含义 自然语言处理-常见问题-百度云

『任职要求』经过分词和词性标注处理后的结果如下:

代码语言:javascript
复制
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 时间不要改小

附 代码和部分注释

代码语言:javascript
复制
#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:])

本文转载自

知乎专栏:https://zhuanlan.zhihu.com/p/35140404 作者:邓卓

《Python人工智能和全栈开发》2018年07月23日即将在北京开课,120天冲击Python年薪30万,改变速约~~~~

*声明:推送内容及图片来源于网络,部分内容会有所改动,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

- END -


本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-04-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 马哥Linux运维 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
NLP 服务
NLP 服务(Natural Language Process,NLP)深度整合了腾讯内部的 NLP 技术,提供多项智能文本处理和文本生成能力,包括词法分析、相似词召回、词相似度、句子相似度、文本润色、句子纠错、文本补全、句子生成等。满足各行业的文本智能需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档