Python爬虫 抓取拉勾招聘信息
我想用这个爬虫做什么
作为一个即将升入大四的学生,找工作这事不得不提上日程。所以我就想着我能不能编写一个爬虫来爬取相关的招聘信息,分析出目标岗位的普遍要求,整体的薪资待遇状况等,从而帮助我找到一个好的工作。
其实根据不要重复造轮子原则,我在网上也搜索了招聘爬虫方面别人已经写好的东西。可是基于3个原因我还是决定自己写。首先别人写好的与我想要的不同,比如选择的招聘网站,爬取的侧重点不同。其次是我也想锻炼自己这方面的技术。最后也是最重要的,我需要这个作为我第一篇博客的素材
首先从我需要爬取的信息方面来讲,主要分为两个方面(不过暂时我不会爬取公司相关信息)。
一方面是岗位相关信息。如岗位名,薪资,岗位要求,职位诱惑等。
另一方面是公司相关信息。如公司名,面试评价,公司产品,公司人数,公司类型标签等等
然后我需要将我爬取下来的数据进行处理分析。
比如可以分析岗位平均工资,工资中位数等统计信息,对某一岗位需要的最突出的能力等等
爬取
可以分为两个阶段,第一阶段是对页面进行分析,即url的规律,数据存储在哪里,json数据的结构等等;第二阶段就是具体的代码实现了。
页面分析
需要爬取数据的页面一共有3个:搜索页面,工作页面和公司页面
其中工作页面和公司页面比较简单,在返回的html页面中就有需要的数据
而搜索页面稍微复杂一点,数据是通过Ajax返回的
- 搜索页面
在这个页面我的主要目标是分析搜索页面,破解反爬措施,获得job_info信息并存储在csv文件中
- 分析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
- 分析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
- 分析json
- 工作页面
首先分析工作页面的urlhttps://www.lagou.com/jobs/3970021.html?show=4cae259e7a4343e6a71d93141c78c53b
从这个url和之前的ajax 获取的json对比很容易得到https://www.lagou.com/jobs/{position_id}.html?show={showId}
爬取中遇到的问题以及解决方案
- 在游览器中访问搜索页面的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
运行之后发现确实可行,仔细分析后发现,想要获取ajax json文件需要满足两个条件
- 需要通过post 特定数据
数据有3个参数。first
表示是否为第一页,pn
表示当前页数,kd
表示职位名称 - 需要用户的Session(Cookie)信息。
- 然而当我把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一下,只要不报错就不管了。既不思考为什么会报错也不思考为什么能解决
- 在解决了编码问题之后,很自然碰到了
Max retries exceeded with url
出现这种错误大概有4种可能:
- http连接太多没有关闭导致的。
- 机器的内存不够了。
- 还有一种可能就是:由于请求频率过快,被目标网站封IP了
- 请求的 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}
- 但是这并没有完,拉勾在多次访问之后会将爬虫重定向到这个网页
<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&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)