Python爬虫 抓取拉勾招聘信息

Python爬虫 抓取拉勾招聘信息

我想用这个爬虫做什么

作为一个即将升入大四的学生,找工作这事不得不提上日程。所以我就想着我能不能编写一个爬虫来爬取相关的招聘信息,分析出目标岗位的普遍要求,整体的薪资待遇状况等,从而帮助我找到一个好的工作。

其实根据不要重复造轮子原则,我在网上也搜索了招聘爬虫方面别人已经写好的东西。可是基于3个原因我还是决定自己写。首先别人写好的与我想要的不同,比如选择的招聘网站,爬取的侧重点不同。其次是我也想锻炼自己这方面的技术。最后也是最重要的,我需要这个作为我第一篇博客的素材

首先从我需要爬取的信息方面来讲,主要分为两个方面(不过暂时我不会爬取公司相关信息)。

一方面是岗位相关信息。如岗位名,薪资,岗位要求,职位诱惑等。

另一方面是公司相关信息。如公司名,面试评价,公司产品,公司人数,公司类型标签等等

然后我需要将我爬取下来的数据进行处理分析。

比如可以分析岗位平均工资,工资中位数等统计信息,对某一岗位需要的最突出的能力等等

爬取

可以分为两个阶段,第一阶段是对页面进行分析,即url的规律,数据存储在哪里,json数据的结构等等;第二阶段就是具体的代码实现了。

页面分析

需要爬取数据的页面一共有3个:搜索页面,工作页面和公司页面
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

其中工作页面和公司页面比较简单,在返回的html页面中就有需要的数据

而搜索页面稍微复杂一点,数据是通过Ajax返回的

  • 搜索页面

    在这个页面我的主要目标是分析搜索页面,破解反爬措施,获得job_info信息并存储在csv文件中

    1. 分析page_url 规律

在这里插入图片描述

page url: https://www.lagou.com/jobs/list_java/p-city_2-gm_6-jd_1_7?px=default&gx=全职&xl=本科&hy=移动互联网#filterBox
从上图筛选条件和page url对比可以很容易得出规律 https://www.lagou.com/jobs/list_java/p-city_ {城市号}-gm_ {公司规模}-jd_{融资阶段}?px=default&gx={工作性质}&xl={学历}&hy={行业}#filterBox

  1. 分析Ajax url

    ajax url:https://www.lagou.com/jobs/positionAjax.json?xl=本科&hy=移动互联网&px=default&gx=全职&city=北京&needAddtionalResult=false

    ajax url 规律:https://www.lagou.com/jobs/positionAjax.json?xl={学历}&hy={行业}&px={排序方式}&gx={工作类型}&city={城市}&needAddtionalResult=false

  2. 分析json

在这里插入图片描述

  • 工作页面

    首先分析工作页面的url

    https://www.lagou.com/jobs/3970021.html?show=4cae259e7a4343e6a71d93141c78c53b

    从这个url和之前的ajax 获取的json对比很容易得到

    https://www.lagou.com/jobs/{position_id}.html?show={showId}

爬取中遇到的问题以及解决方案

  1. 在游览器中访问搜索页面的ajax url (https://www.lagou.com/jobs/positionAjax.json?xl=本科&hy=移动互联网&px=default&gx=全职&city=北京&needAddtionalResult=false) 得到了

    {"status":false,"msg":"您操作太频繁,请稍后再访问","clientIp":"","state":2402}

    我猜测这应该是拉勾的反爬策略,作为一个知名的互联网招聘网站,可以想象一定也有很多人爬过,所以我就到网上寻找拉勾反爬。我找到的解决方案就是先访问搜索页面获取cookie,然后再加上cookie访问ajax url

    """
    这是我找到的代码
    
    
    拉勾网反爬机制分析:
    通过两次请求来获取职位列表,
    第一次请求原始页面获取cookie
    第二次请求时利用第一次获取到的cookie
    
    """
    import requests
    # 第一次请求的URL
    first_url = 'https://www.lagou.com/jobs/list_Python?labelWords=&fromSearch=true&suginput='
    # 第二次请求的URL
    second_url = 'https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false'
    # 伪装请求头
    headers = {
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Accept-Encoding': 'gzip, deflate, br',
        'Accept-Language': 'zh-CN,zh;q=0.9',
        'Connection': 'keep-alive',
        'Content-Length': '25',
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'Host': 'www.lagou.com',
        'Origin': 'https://www.lagou.com',
        'Referer': 'https://www.lagou.com/jobs/list_Python?labelWords=&fromSearch=true&suginput=',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'same-origin',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36',
        'X-Anit-Forge-Code': '0',
        'X-Anit-Forge-Token': 'None',
        'X-Requested-With': 'XMLHttpRequest'
    }
    # 创建一个session对象
    session = requests.session()
    # 请求的数据
    data = {
        'first': 'true',
        'pn': '1',
        'kd': 'Python'
    }
    session.get(first_url, headers={
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36'
    })
    result = session.post(second_url, headers=headers, data=data, allow_redirects=False)
    print(result.json())
    
    # copy from https://blog.csdn.net/Deep___Learning/article/details/104064641 
    

    运行之后发现确实可行,仔细分析后发现,想要获取ajax json文件需要满足两个条件

    1. 需要通过post 特定数据

      数据有3个参数。first表示是否为第一页,pn表示当前页数,kd表示职位名称

    2. 需要用户的Session(Cookie)信息。

  2. 然而当我把first_url,second_url换成我自己的page_url,ajax_url新的错误又出现了

    'latin-1' codec can't encode characters in position 42-43: ordinal not in range(256)

    我从网上找到的解决方案是要先编码成bytes(utf-8)格式再解码为latin1。如下面代码所示

    ajax_url=ajax_url.encode("utf-8").decode('latin1')
    

    第一次使用这个方法的时候我确实成功了,可是当我第二天再次运行的时候却仍然报同样的错误。

    没有办法我只能再到网上搜索,可是搜到的结果却令我很失望,要么是上面所示的这种解决方法,要么是其他地方出现的这种错误(比如MySQL)

    在万般无奈之下,我不得不去搜索这个问题本身——在网上搜索python编码,看能不能找到一些思路。

    最后我确实得到了思路

    ajax_url=ajax_url.encode("utf-8")
    

    通过对编码的基本了解,我猜测很有可能post函数在处理含有不知道怎么编码的中文的ajax_url过程中产生了编码错误。那我就想能不能直接把ajax_url 转成我知道的编码utf-8呢?结果一试果然可以。

    在这一次处理中文编码的问题上,我暴露了两个缺点。第一是编码知识上的不足。第二个问题更为致命,是我编程习惯的问题,我在具体的编程中总是不求甚解,出了问题,搜到了答案,copy一下,只要不报错就不管了。既不思考为什么会报错也不思考为什么能解决

  3. 在解决了编码问题之后,很自然碰到了Max retries exceeded with url

    出现这种错误大概有4种可能:

    1. http连接太多没有关闭导致的。
    2. 机器的内存不够了。
    3. 还有一种可能就是:由于请求频率过快,被目标网站封IP了
    4. 请求的 url 地址错误

    显然我这个最大可能是第三种情况。那么就很好解决了,只要catch MaxRetryError然后通过代理池换ip就可以了

    proxies = get_proxy()
    def request_page_result(url, session, user_agent):
        try:
            r = session.get(url, headers={
                'User-Agent': user_agent
            }, proxies=proxies, allow_redirects=False)
    
            print("status code ", r.status_code)
        except MaxRetryError as e:
            print('exception ', e)
            user_agent = get_user_agent()
            proxies = get_proxy()
            r = session.get(url, headers={
                'User-Agent': user_agent
            }, proxies=proxies, allow_redirects=False)
            
    def get_proxy():
        response = requests.get(PROXY_POOL_URL)
        if response.status_code == 200:
            print(response.text)
            return {'http': 'http://' + response.text}
    
  4. 但是这并没有完,拉勾在多次访问之后会将爬虫重定向到这个网页

    <html><head><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/><meta name="renderer" content="webkit"/><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/></head><body><script src="/utrack/track.js?version=1.0.1.0" type="text/javascript"/><script type="text/javascript" src="https://www.lagou.com/utrack/trackMid.js?version=1.0.0.3&amp;t=1597665706"/><input type="hidden" id="KEY" value="SnDW0dsUx707Tsd43dKdj9CAtzQdCMtFxxyHWmeMlLu"/><script type="text/javascript">IqMqbVoy();</script>é¡µé¢åŠ è½½ä¸­...<script type="text/javascript" crossorigin="anonymous" src="https://www.lagou.com/upload/oss.js?v=1010"/></body></html>
    
    

    不过也很好破解,只要识别重定向的status_code,然后换ip就可以了

    def request_page_result(url, session, user_agent):
        global proxies
        print(type(session))
        print(proxies)
        try:
            r = session.get(url, headers={
                'User-Agent': user_agent
            }, proxies=proxies, allow_redirects=False)
    
            print("status code ", r.status_code)
            
            # 比上面多出来的一段代码
            # 如果被重定向就换ip
            if r.status_code == 302:
                user_agent = get_user_agent()
                proxies=get_proxy()
                r = session.get(url, headers={
                    'User-Agent': user_agent
                }, proxies=proxies, allow_redirects=False)
    
        except MaxRetryError as e:
            print('exception ', e)
            user_agent = get_user_agent()
            proxies = get_proxy()
            r = session.get(url, headers={
                'User-Agent': user_agent
            }, proxies=proxies, allow_redirects=False)
    

具体代码

import math
import random

import requests
from pyquery import PyQuery as pq
from urllib3.exceptions import MaxRetryError
import pandas as pd

# 使用了https://github.com/Python3WebSpider/ProxyPool代理池
PROXY_POOL_URL = 'http://localhost:5555/random'
PROXY_POOL_SIZE = 5
proxies = None

job_info_file = "job_info.csv"
company_info_file = "company_info.csv"
comment_info_file = "comment_info.csv"


'''
根据筛选条件组合并返回请求搜索页面的page_url和会返回职位信息json的ajax_url
'''


def combine_page_and_ajax_url(job_type: str, **condition):
    page_url = 'https://www.lagou.com/jobs/list_' + job_type
    ajax_url = r'https://www.lagou.com/jobs/positionAjax.json'

    # 工作城市
    city = '全国'
    if 'city' in condition:
        city = condition['city']
    page_url = page_url + '?city=' + city
    ajax_url = ajax_url + '?city=' + city

    # 排序方式
    px = 'default'
    if 'px' in condition:
        px = condition['px']
    page_url = page_url + '&px=' + px
    ajax_url = ajax_url + '&px=' + px

    # 公司规模 1 少于15人, 2 15-50人以此类推若多选,如1,2都选,就是_1_2
    if 'gm' in condition:
        page_url = page_url + '&gm=' + condition['gm']
        ajax_url = ajax_url + '&gm=' + condition['gm']

    # 融资阶段
    if 'jd' in condition:
        page_url = page_url + '&jd=' + condition['jd']
        ajax_url = ajax_url + '&jd=' + condition['jd']

    # 工作性质:全职,兼职,实习
    if 'gx' in condition:
        page_url = page_url + '&gx=' + condition['gx']
        ajax_url = ajax_url + '&gx=' + condition['gx']

    # 工作经验 isSchoolJob 1 应届
    if 'gj' in condition:
        page_url = page_url + 'gj=' + condition['gj']
        ajax_url = ajax_url + 'gj=' + condition['gj']

    # 学历
    if 'xl' in condition:
        page_url = page_url + '&xl=' + condition['xl']
        ajax_url = ajax_url + '&xl=' + condition['xl']

    # 行业
    if 'hy' in condition:
        page_url = page_url + '&hy=' + condition['hy']
        ajax_url = ajax_url + '&hy=' + condition['hy']

    page_url = page_url + '#filterBox'
    ajax_url = ajax_url + '&needAddtionalResult=false&isSchoolJob=1'

    # 解决中文乱码
    page_url = page_url.encode("utf-8")
    ajax_url = ajax_url.encode("utf-8")

    print(page_url)
    print(ajax_url)
    return page_url, ajax_url


'''
获取符合筛选条件的工作页面url和公司页面url
'''


def get_job_and_company_urls(page_url, ajax_url, job_type):
    # 需要获取的工作和公司数据
    jobs_id = []
    companies_id = []
    jobs_name = []
    jobs_advantage = []
    jobs_salary = []
    jobs_publish_time = []
    companies_name = []
    companies_labels = []
    companies_size = []

    show_id = None
    JOBS_COUNT_ONE_PAGE = 15

    remain_page_count = -1
    page_number = 1
    first = 'true'

    user_agent = get_user_agent()

    session = requests.session()

    r,session=request_page_result(page_url,session,user_agent)

    while remain_page_count != 0:
        # 请求的数据
        data = {
            'first': first,
            'pn': page_number,
            'kd': job_type
        }

        ajax_result=request_ajax_result(ajax_url,page_url,session,user_agent,data)

        result_json = ajax_result.json()
        position_result = result_json['content']['positionResult']

        # 第一次进入循环,获取到json中的总工作数totalCount
        if remain_page_count == -1:
            show_id = result_json['content']['showId']
            print('showId ', show_id)
            print("type of result", type(position_result))
            total_count = position_result['totalCount']

            # 没有符合条件的工作,直接返回
            if total_count == 0:
                return
            remain_page_count = math.ceil(total_count / JOBS_COUNT_ONE_PAGE)

        result = position_result['result']
        for item in result:
            position_id = item['positionId']
            job_name = item['positionName']
            job_advantage = item['positionAdvantage']
            job_salary = item['salary']
            publish_time = item['createTime']

            company_id = item['companyId']
            company_name = item['companyFullName']
            company_labels = item['companyLabelList']
            company_size = item['companySize']

            jobs_id.append(position_id)
            jobs_name.append(job_name)
            jobs_advantage.append(job_advantage)
            jobs_salary.append(job_salary)
            jobs_publish_time.append(publish_time)

            companies_name.append(company_name)
            companies_id.append(company_id)
            companies_labels.append(company_labels)
            companies_size.append(company_size)

        remain_page_count = remain_page_count - 1
        page_number = page_number + 1
        first = 'false'

    # 存储基本工作信息,公司信息到csv文件中
    job_df = pd.DataFrame(
        {'job_id': jobs_id, 'job_name': jobs_name, 'job_advantage': jobs_advantage, 'salary': jobs_salary,
         'publish_time': jobs_publish_time, 'company_id': companies_id})
    company_df = pd.DataFrame({'company_id': companies_id, 'company_name': companies_name, 'labels': companies_labels,
                               'size': companies_size})

    job_df.to_csv(job_info_file, mode='w', header=True, index=False)
    company_df.to_csv(company_info_file, mode='w', header=True, index=False)

    return show_id


'''
根据得到的job_id访问工作页面并存储工作信息到csv文件中
'''


def get_and_store_job_info(show_id: str):
    jobs_detail = []
    PAGE_SIZE = 500
    comments_content = []
    comments_time = []
    users_id = []
    company_scores = []
    interviewer_scores = []
    describe_scores = []
    comprehensive_scores = []
    useful_counts = []
    tags = []

    # 从csv文件读取job_id并与show_id组合成工作页面url
    df = pd.read_csv(job_info_file)
    jobs_id = df['job_id']

    for job_id in jobs_id:
        user_agent = get_user_agent()
        session = requests.session()
        job_page_url = 'https://www.lagou.com/jobs/' + str(job_id) + '.html?show=' + show_id

        # 访问工作页面获取职位描述和面试评价
        r = request_page_result(url=job_page_url, session=session, user_agent=user_agent)
        doc = pq(r.text)
        job_detail = doc('#job_detail > dd.job_bt > div').text()
        print("job_detail", job_detail)
        jobs_detail.append(job_detail)

        # 获取面试评价
        review_ajax_url = 'https://www.lagou.com/interview/experience/byPosition.json'

        data = {
            'positionId': job_id,
            'pageSize': PAGE_SIZE,
        }

        response = request_ajax_result(review_ajax_url, job_page_url, session, user_agent, data)
        response_json = response.json()
        print("response json", response_json)

        if response_json['content']['data']['data']['totalCount'] != 0:
            result = response_json['content']['data']['data']['result']
            for item in result:
                comment_content = item['content']
                comment_time = item['createTime']
                user_id = item['userId']
                company_score = item['companyScore']
                interviewer_score = item['interviewerScore']
                describe_score = item['describeScore']
                comprehensive_score = item['comprehensiveScore']
                useful_count = item['usefulCount']
                tag = item['tags']
                print("content",comment_content)

                comments_content.append(comment_content)
                comments_time.append(comment_time)
                users_id.append(user_id)
                company_scores.append(company_score)
                interviewer_scores.append(interviewer_score)
                describe_scores.append(describe_score)
                comprehensive_scores.append(comprehensive_score)
                useful_counts.append(useful_count)
                tags.append(tag)

            j_ids=[]
            j_ids.extend(job_id for i in range(len(comments_content)))

            comment_df = pd.DataFrame({'job_id': j_ids,  'content': comments_content})
            comment_df.to_csv(comment_info_file, mode='a', header=False, index=False)

    # 将获取到的职位描述和面试评价存储到csv文件中
    df['job_detail'] = jobs_detail
    df.to_csv(job_info_file)


def request_page_result(url, session, user_agent):
    try:
        r = session.get(url, headers={
            'User-Agent': user_agent
        })
    except MaxRetryError as e:
        print(e)
        user_agent = get_user_agent()
        r = session.get(url, headers={
            'User-Agent': user_agent
        }, proxies=get_proxy())

    return r,session


def request_ajax_result(ajax_url, page_url, session, user_agent, data):

    try:
        result = session.post(ajax_url, headers=get_ajax_header(page_url, user_agent), data=data,
                              allow_redirects=False)
    except MaxRetryError as e:
        print(e)
        user_agent = get_user_agent()
        result = session.post(ajax_url, headers=get_ajax_header(page_url, user_agent),
                              proxies=get_proxy(),
                              data=data, allow_redirects=False)
    return result

'''
访问拉勾页面所使用的header
'''
def get_page_header():
    page_header = {
        'User-Agent': get_user_agent()
    }
    return page_header

'''
访问拉勾ajax json所使用的header
'''
def get_ajax_header(url, user_agent):
    ajax_header = {
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Accept-Encoding': 'gzip, deflate, br',
        'Accept-Language': 'zh-CN,zh;q=0.9',
        'Connection': 'keep-alive',
        'Content-Length': '25',
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'Host': 'www.lagou.com',
        'Origin': 'https://www.lagou.com',
        'Referer': url,
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'same-origin',
        'User-Agent': user_agent,
        'X-Anit-Forge-Code': '0',
        'X-Anit-Forge-Token': 'None',
        'X-Requested-With': 'XMLHttpRequest'
    }
    return ajax_header


def get_user_agent():
    user_agent = [
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"

        "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 "
        "Safari/536.11",

        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6"

    ]
    return random.choice(user_agent)


def get_proxy():
    response = requests.get(PROXY_POOL_URL)
    if response.status_code == 200:
        print(response.text)
        return response.text



if __name__ == '__main__':
    page_url, ajax_url = combine_page_and_ajax_url(job_type='java', city='杭州', gx='全职', xl='本科', hy='移动互联网')
    showId = get_job_and_company_urls(page_url, ajax_url, 'java')
    get_and_store_job_info(showId)


  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值