数据获取:爬虫获取招聘网站产品经理职位信息

一、前言

洞悉岗位要求最直接有效的方式就是分析各公司发布的招聘需求,本文使用python爬取了拉勾网近期发布的产品经理岗位的相关信息,内容侧重于网页的解析,能够让没有python基础的小伙伴直接套用代码获取想要的数据,也让自己更加明确未来要准备的方向,同时希望能给有兴趣从事产品经理岗位的小伙伴一些参考信息。

严正声明:本次爬取的信息不包含任何个人隐私数据,未对访问网站造成大批量的请求。代码仅供学习使用,不得用于不合规的用途。

代码链接
如果代码页面加载不出来,可以打开
这个网页,再粘贴地址https://nbviewer.jupyter.org/github/zyyssr/spider_pm_from_lagou/blob/main/spider_pm_from_lagou.ipynb

二、爬虫基础知识

当我们在浏览器中输入一个url后回车,后台会发生什么?

这段过程发生了以下四个事件:

(1)查找域名对应的IP地址。

(2)向IP对应的服务器发送请求。

(3)网站服务器响应请求,返回对应的响应。

(4)浏览器解析网页内容。

网络爬虫要做的,简单来说,就是实现浏览器的功能。通过指定url,直接返回用户所需的数据,不需要一步步手动操作浏览器获取。

一句话概括:爬虫就是获取网页并提取和保存信息的自动化程序。

用户获取网络数据的方式:

浏览器提交请求->下载网页代码->解析成页面

爬虫获取网络数据的方式:

模拟浏览器发送请求(获取网页代码)->提取有用的数据->存放于数据库或文件中

HTTP 请求方法

我们平常遇到的绝大部分请求都是 GET 或 POST 请求,另外还有一些请求方法,如 HEAD、PUT、DELETE、OPTIONS、CONNECT、TRACE 等,感兴趣参考:HTTP 请求方法 | 菜鸟教程

本项目中,职位简介页面是通过POST方式,而职位详情页面是通过GET方式。

Json基础结构

所用工具

Python版本: Python3.8

IDE: Jupyter notebook

浏览器:Chrome

爬虫软件包:为了复习使用,采用了两个不同的包

  • urllib:内置的http请求库,一般要先构建get或者post请求,然后再发起请求

  • requests:是对urllib的再次封装,可以直接发起请求

三、职位简介爬取

3.1 网页解析

和很多网站一样,拉勾网也采用的是Ajax(异步加载)的技术,并且使用Json来传输网站数据。

我们通过Chrome打开拉勾网主页,搜索栏搜索”产品经理“,并勾选自己想要的筛选条件。点击下一页,可以发现搜索栏的网址并没有改变。

在搜索结果的页面中,我们按照以下顺序操作:

(1)右键单击检查

(2)默认打开的是Elements页面

(3)切换到Network标签,输入json,Ctrl + R刷新网页,可以找到positionAjax.json

(4)因为该网站是异步请求,所以打开Network中的XHR,针对JSON中的数据进行分析

结果如下图所示:

(1)General

首先是 General 部分,Request URL 为请求的 URL,Request Method 为请求的方法,Status Code 为响应状态码,Remote Address 为远程服务器的地址和端口,Referrer Policy 为 Referrer 判别策略。

(2)Response Headers

Response Headers 就是响应的一部分,例如其中包含了服务器的类型、文档类型、日期等信息,浏览器接收到响应后,会解析响应内容,进而呈现网页内容。

(3)Request Headers

下面是我们需要构造的请求头信息,如果这里没有构造好的话,容易被网站识别为爬虫,被拒绝访问。



红框部分是我们构造请求头时选择的重要信息。

Authority:表示服务器的域名以及服务器所监听的端口号。如果所请求的端口是对应的服务的标准端口(80),则端口号可以省略。

Cookie:也作复数Cookies。它的主要功能是维持当前访问会话。当我们输入用户名和密码成功登录某个网站后,服务器会用会话保存登录状态信息,后面我们每次刷新或请求该站点的其他页面时,会发现都是登录状态,这就是 Cookies 的功劳。

Referer:此内容用来标识这个请求是从哪个页面发过来的,通常在访问链接时,都要带上Referer字段,服务器会进行来源验证,后台通常会用此字段作为防盗链的依据。

User-Agent:后台通常会通过此字段判断用户设备类型、系统以及浏览器的型号版本。如果不加此字段,很可能会被识别出为爬虫。

(4)Form Data

下面是我们发送POST请求时需要包含的表单信息Form Data。将请求的参数构造成一个字典,传给requests.post()的data参数即可。

3.2 爬取策略

  • 构建请求头、表单信息,使用request库请求
  • 爬取第一页信息,分析数据结构,构建列表存储自己想要的数据
  • 爬取所有页面的信息,将数据存储到csv文件中

3.3 代码部分

下图是爬取单个页面信息的函数:

def get_json(url, page_num):
    '''
    从网页获取JSON,使用POST请求,加上头部信息请求的header
    '''
    
    # 搜索栏的网址,调用requests对象的cookies属性获得登录的cookies,并赋值给变量cookies,最后带着cookies去请求
    url1 = 'https://www.lagou.com/jobs/list_%E4%BA%A7%E5%93%81%E7%BB%8F%E7%90%86/p-city_0?px=default&gx= \
            %E5%85%A8%E8%81%8CC&gj=%E5%9C%A8%E6%A0%A1/%E5%BA%94%E5%B1%8A,3%E5%B9%B4%E5%8F%8A%E4%BB%A5%E4%B8%8B&xl= \
            %E6%9C%AC%E7%A7%91,%E7%A1%95%E5%A3%AB#filterBox'
    # 构造请求头信息
    header = {
        'authority': 'www.lagou.com',
        'referer': 'https://www.lagou.com/jobs/list_%E4%BA%A7%E5%93%81%E7%BB%8F%E7%90%86/p-city_0? \
                    px=default&gx=%E5%85%A8%E8%81%8C&gj=%E5%9C%A8%E6%A0%A1/%E5%BA%94%E5%B1%8A,\
                    3%E5%B9%B4%E5%8F%8A%E4%BB%A5%E4%B8%8B&xl=%E6%9C%AC%E7%A7%91,%E7%A1%95%E5%A3%AB',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) \
                       Chrome/66.0.3359.139 Safari/537.36',
    }
    # 构造表单信息
    form_data = {
        'first': 'true',
        'pn': page_num,
        'kd': '产品经理',
    }
    
    session = requests.Session() 
    cookie = session.get(url = url1, headers = header, timeout = 5).cookies
    res = requests.post(url, headers = header, data = form_data, cookies = cookie, timeout = 5)
    res.encoding = 'utf-8'
    page_data = res.json()
    return page_data

在网页解析阶段,点击”preview”, 发现无法预览源代码,也就无从得知数据结构,所以我们先调用get_json函数,分析搜索结果第一页的数据组成。


分析可得,我们想要的数据都存储在[‘content’]里。[‘positionResult’][‘pageSize’]告诉我们每页最多15条职位信息,产品经理岗位的总体数量存储在totalCount,[‘positionResult’][‘result’]里面是每个岗位有哪些信息,可将其都打印出来,以便选择自己想要的内容。

url = 'https://www.lagou.com/jobs/positionAjax.json? \ 
gj=%E5%9C%A8%E6%A0%A1%2F%E5%BA%94%E5%B1%8A%2C3%E5%B9%B4%E5%8F%8A%E4%BB%A5%E4%B8%8B \ 
&xl=%E6%9C%AC%E7%A7%91%2C%E7%A1%95%E5%A3%AB&px=default&gx=%E5%85%A8%E8%81%8C&needAddtionalResult=false'
first_page = get_json(url,1)
print(first_page)
page_size = first_page['content']['pageSize']
total_count = first_page['content']['positionResult']['totalCount']
print("每页职位数: {}, 总数为: {}".format(page_size, total_count))
position = first_page['content']['positionResult']['result']
df = pd.DataFrame(position)
df.info()

接下来设置列表以存储自己想要的数据。

def get_page_info(jobs_list):
    '''
    获取每一页的职位信息
    '''
    page_info_list = []
    for i in jobs_list:  
        job_info = []
        job_info.append(i['positionId'])
        job_info.append(i['positionName'])        
        job_info.append(i['companyFullName'])
        job_info.append(i['companyShortName'])
        job_info.append(i['companySize'])
        job_info.append(i['industryField'])
        job_info.append(i['financeStage'])
        job_info.append(i['companyLabelList'])
        job_info.append(i['firstType'])
        job_info.append(i['secondType'])
        job_info.append(i['skillLables'])
        job_info.append(i['positionLables'])    
        job_info.append(i['createTime'])    
        job_info.append(i['city'])        
        job_info.append(i['district'])
        job_info.append(i['salary'])        
        job_info.append(i['salaryMonth'])        
        job_info.append(i['workYear'])
        job_info.append(i['education'])        
        job_info.append(i['positionAdvantage'])
        job_info.append(i['resumeProcessRate'])
        page_info_list.append(job_info)
    return page_info_list

同时,我们发现拉勾网最多展示30页搜索结果。

def get_page_num(count):
    '''
    确定要抓取的页数,通过观察,可以发现拉勾网最多显示30页结果
    '''
    page_num = math.ceil(count / 15)
    if page_num > 30:
        return 30
    else:
        return page_num

然后,我们爬取所有页面信息,存储到csv文件中。

def main():
    url = 'https://www.lagou.com/jobs/positionAjax.json? \
           gj=%E5%9C%A8%E6%A0%A1%2F%E5%BA%94%E5%B1%8A%2C3%E5%B9%B4%E5%8F%8A%E4%BB%A5%E4%B8%8B \
           &xl=%E6%9C%AC%E7%A7%91%2C%E7%A1%95%E5%A3%AB&px=default&gx=%E5%85%A8%E8%81%8C&needAddtionalResult=false'
    first_page = get_json(url,1)
    page_size = first_page['content']['pageSize']
    total_count = first_page['content']['positionResult']['totalCount']
    print("每页职位数: {}, 总数为: {}".format(page_size, total_count))

    num = get_page_num(total_count)
    total_info = []
    for num in range(1, num + 1):
        # 获取每一页的职位相关的信息
        page_data = get_json(url, num)  
        jobs_list = page_data['content']['positionResult']['result']  
        page_info = get_page_info(jobs_list)
        print('第{}页搜索结果:{}'.format(num, page_info))
        total_info += page_info
        print('当前职位总数为:', len(total_info))
        time.sleep(25) # 爬取一次休息一段时间,以免被识别
        
        # 将数据转化为data frame,写入到csv文件中
        df = pd.DataFrame(data=total_info,
                          columns=['职位编号', '职位名称', '公司全称', '公司简称', '公司规模', '所属领域', '融资阶段', '公司标签', 
                                   '职位类型', '第二职位类型', '技能标签', '职位标签', '发布时间',  '城市', '区域', '薪资', '薪酬月数',
                                   '工作经验', '学历要求', '职位福利', '简历处理速度'])
        df.to_csv('pm_position_data.csv', index=False)
        
if __name__ == '__main__':
    main()

四、职位详情描述爬取

4.1 网页解析

通过观察可以发现,拉勾网的职位页面详情是由 http://m.lagou.com/jobs/ + ***** (PositionId).html 组成,而PositionId可以在上一阶段获得。同时,要注意到此页面请求方式为Get方法。

4.2 爬取策略

  • 根据上一阶段爬取的PositionId构造url,使用urllib库发起请求
  • 爬取一个样例,分析数据结构
  • 通过正则化方法解析每个职位对应的职位描述信息

4.3 代码实现

def get_content(url):
    '''
    获取职位描述信息
    '''
    headers = {
        'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) \
                       Chrome/85.0.4183.121 Mobile Safari/537.36',
    }
    req = request.Request(url, headers=headers)
    page = request.urlopen(req).read()
    content = page.decode('utf-8')
    return content

预览一个样例,可以发现我们想要的内容在<div class="content">部分。


正则化解析内容

def get_jd(content):
    '''
    正则化解析页面
    '''
    soup = BS(content, 'lxml')
    job_description = soup.select('div[class="content"]')
    job_description = re.sub(r'</p><p>', '\n', str(job_description[0]))
    result = re.sub(r'<[^>]+>', '', job_description)
    result = result.strip()
    return result

得到以下结果:


最后,爬取所有职位描述数据,与上一阶段的数据合并,存入csv文件。

data = pd.read_csv("pm_position_data.csv") #导入上一阶段爬取的数据
data = data.drop_duplicates() # 去重
position_ids = data['职位编号'].tolist()

jd_dict = {}
for i in position_ids:
    url = 'https://m.lagou.com/jobs/' + str(i) + '.html'
    jd = get_jd(get_content(url))
    jd_dict[i] = jd
    time.sleep(15)

data_jd = pd.DataFrame([jd_dict]).T.reset_index().rename(columns={'index':'职位编号',0:'职位描述'}) 
final_data = pd.merge(data, data_jd, on = '职位编号', how = 'left') # 将两组数据合并
final_data.to_csv('pm_position.csv', index=False, encoding = 'utf-8-sig') # 导出爬取最终数据

五、数据可视化呈现

由于时间关系,本次爬取的数据量较少,如需获取更多的职位信息,可以分类爬取,例如循环公司规模这个类别,观察url可寻得相应类别的规律,这里不多赘述。拉勾网专注的是互联网垂直领域的招聘,所以还需要从其它全类招聘网站上获取更多的数据才能进行分析。也因个人需求设置了相应的筛选条件,因此此次数据分析的结果并不具代表性,仅简单说明爬取结果。

经过去重处理后,共有410条产品经理岗位招聘数据,岗位信息来自279家规模各异的公司,其中招聘需求最大的是字节跳动公司。根据本次搜索结果,有21座城市提供了产品经理的岗位,其中北深上广杭需求较大。

在岗位数量排名前7的城市里,薪酬中位数似乎也呈现阶梯式排名。北京已超25k/月,深圳、上海和杭州月薪水平相当,在20k/月左右,不管从岗位需求量还是薪酬水平方面看,成都表现得都不错,这也和近几年不少大公司都在成都设立了业务部门有关。

全面了解产品经理这个岗位的要求是本次爬取网页的主要目的,从这个词云看,产品经理必须具备产品设计规划能力、产品运营推广能力和多部门之间的协调沟通能力。产品经理必须掌握的软件和技能分为以下几类:原型设计——Axure、Sketch,思维导图——Xmind、MindManager,流程图制作——Visio,用户需求调研——CRM,项目管理——Project,以及三大文档——PRD、MRD、BRD。还可以看到几个比较醒目的英文单词:Web、PC、AI、SaaS(Software-as-a-Service)、ERP(Enterprise Resource Planning),产品的发展趋势一直在变化,要求产品经理不断深耕和变革,这也是产品经理这个岗位最吸引我的地方。

六、结语

转行路上,切实体会到了“选择比努力更重要”这句话的分量。愿大家都能快乐工作,快乐生活!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342