Python爬虫:异步数据抓取并保存到Excel

标签(空格分隔): Python 爬虫 异步


环境:Python 3.6Pycharm 2017.2.3Chrome 61.0.3163.100


======> 2018年3月3号 更新整理了一下代码 <======

  • 修改了post请求中的form-data没有生效的错误
  • 修改为可直接从程序外部传入参数,可获得任意职位,任意地区的职位信息,并以职位-城市命名文件

前言

刚接触了一下异步数据的抓取,所以试一下如何实现异步数据的抓取。
这次需要实现的是,在招聘网站拉勾网【网站链接】中爬出Python相关职位的一些基本信息,并且将基本信息保存至Excel表格中。


需要的一些知识点

  • AJAXAsynchronous JavaScript and XML(异步的 JavaScriptXML)。它不是新的编程语言,而是一种使用现有标准的新方法。它采用的是AJAX异步请求。通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。因此就可以在不重新加载整个网页的情况下,对网页的某部分进行更新,从而实现数据的动态载入。
  • XHRXMLHttpRequest 对象用于和服务器交换数据。

开始动手吧

分析网页

打开拉勾网主页之后,我们在搜索框中输入关键字Python,以用来查找和Python相关的职位。在搜索结果的页面中,我们按照以下顺序操作可以发现。

–> 右键检查
–> 打开审查元素后默认打开的是Elements
–> 我们切换到Network标签,刷新一下网页会出现各种条目的请求
–> 因为该网站是异步请求,所以打开Network中的XHR,针对JSON中的数据进行分析。

python 抓取网页提交表单 python 爬虫抓取网页数据导出excel_ajax

–> 在审查元素中我们可以发现以下信息:

# 该页面的请求url 
Request URL:https://www.lagou.com/jobs/positionAjax.json?city=%E5%B9%BF%E5%B7%9E&needAddtionalResult=false&isSchoolJob=0
# 该页面的请求方法,需要提交表单数据(Form-data)
Request Method:POST
Form-data:  first: true
            pn: 1
            kd: python
--------------------------------
# Request Headers中的相关信息
包括Cookie、Referer、User-Agent等我们之后会用到的信息

代码

代码片段1-请求该网页
import requests     # 导入请求模块

url = 'https://www.lagou.com/jobs/positionAjax.json?px=default&city=%E6%88%90%E9%83%BD&needAddtionalResult=false&isSchoolJob=0'
form_data = {'first': 'true',
             'pn': '1',
             'kd': 'python'}

def getJobList():
    res = requests.post(
        # 请求url
        url=url,
        # 填写表单数据
        data = form_data
    )
    result = res.json()     # 获取res中的json信息
    print(result)

getJobList()

运行结果:

{'success': False, 'msg': '您操作太频繁,请稍后再访问', 'clientIp': '113.14.1.254'}

出现这样他的结果,应该是拉钩网的反爬机制起了作用,所以此时就需要伪装成用户去访问,需要加入headers信息。

代码片段2-加入headers信息
import requests

url = 'https://www.lagou.com/jobs/positionAjax.json?px=default&city=%E6%88%90%E9%83%BD&needAddtionalResult=false&isSchoolJob=0'
form_data = {'first': 'true',
             'pn': '1',
             'kd': 'python'}
HEADERS = {
    # User-Agent(UA) 服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。也就是说伪装成浏览器进行访问
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
    # 用于告诉服务器我是从哪个页面链接过来的,服务器基此可以获得一些信息用于处理。如果不加入,服务器可能依旧会判断为非法请求
    'Referer': 'https://www.lagou.com/jobs/list_Python?px=default&gx=&isSchoolJob=1&city=%E5%B9%BF%E5%B7%9E'
}

def getJobs():
    res = requests.post(url=url, headers=HEADERS, data=form_data)
    result = res.json()
    print(result)

getJobs()

运行结果:

{'success': True, 'requestId': None, 'resubmitToken': None, 'msg': None, 'content': {'pageNo': 1, 'pageSize': 15, 'hrInfoMap': {'4067747': {'userId': 1944019, 'phone': None, 'positionName': 'HR', 'receiveEmail': None, 'realName': 'zp', 'portrait': None, 'canTalk': True, 'userLevel': 'G1'}, '4170140': {'userId': 7646331, 'phone': None, 'positionName': None, 'receiveEmail': None, 'realName': 'liuyaou', 'portrait': None, 'canTalk': True, 'userLevel': 'G1'}, '3887376': {'userId': 8149206, 'phone': None, 'positionName': '技术主管', 'receiveEmail': None, 'realName': '张敏', 'portrait': 'i/image2/M00/24/B7/CgoB5lobmu-AbIU0AAr-Af5S-eo650.jpg', 'canTalk': True, 'userLevel': 'G1'}, '4029468': {'userId': 3070268, 'phone': None, 'positionName': 'HR', 'receiveEmail': None, 'realName': 'hr', 'portrait': None, 'canTalk': True, 'userLevel': 'G1'}, '3490584': {'userId': 7309781, 'phone': None, 'positionName': 'HR', 'receiveEmail': None, 'realName': 'lydia', 'portrait': 'i/image2/M00/36/F2/CgoB5lpK4CGAPYqmAAGFpNMN_LQ448.jpg', 'canTalk': True, 'userLevel': 'G1'}, ......}

这样就获得了响应结果中的json信息,它和Preview中的数据信息是一致的(dict类型顺序可能不一致)。

python 抓取网页提交表单 python 爬虫抓取网页数据导出excel_异步_02

代码片段3-获取当前页面职位信息

可以发现需要的职位信息在content –> positionResult –> result下,其中包含了工作地点、公司名、职位等信息。

python 抓取网页提交表单 python 爬虫抓取网页数据导出excel_python_03

import requests

url = 'https://www.lagou.com/jobs/positionAjax.json?px=default&city=%E6%88%90%E9%83%BD&needAddtionalResult=false&isSchoolJob=0'

HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3346.9 Safari/537.36',
    'Referer': 'https://www.lagou.com/jobs/list_python?px=default&city=%E6%88%90%E9%83%BD'
}

form_data = {'first': 'true',
             'pn': '1',
             'kd': 'python'}


def getJobs():
    res = requests.post(url=url, headers=HEADERS, data=form_data)
    result = res.json()
    jobs = result['content']['positionResult']['result']
    print(type(jobs))
    for job in jobs:
        print(job)


getJobs()

运行结果:

<class 'list'>
{'companyId': 38879, 'positionId': 4154756, 'industryField': '企业服务,金融', 'education': '本科', 'workYear': '5-10年', 'city': '成都', 'positionAdvantage': '五险一金 周末双休 带薪年假', 'createTime': '2018-03-01 16:37:31', 'salary': '10k-18k', 'positionName': 'Python', 'companySize': '500-2000人', 'companyShortName': '广州碧软', 'companyLogo': 'i/image/M00/95/8F/CgqKkVibxr2AKXQpAAAVkFx6QdU265.jpg', 'financeStage': '不需要融资', 'jobNature': '全职', 'approve': 1, 'companyLabelList': ['技能培训', '节日礼物', '专项奖金', '带薪年假'], 'publisherId': 4356018, 'score': 0, 'district': '高新区', 'positionLables': ['Java', 'MySQL'], 'industryLables': [], 'businessZones': ['华阳'], 'hitags': None, 'resumeProcessRate': 63, 'resumeProcessDay': 1, 'adWord': 0, 'longitude': '104.05558', 'latitude': '30.53895', 'imState': 'disabled', 'lastLogin': 1519908146000, 'explain': None, 'plus': None, 'pcShow': 0, 'appShow': 0, 'deliver': 0, 'gradeDescription': None, 'promotionScoreExplain': None, 'firstType': '开发/测试/运维类', 'secondType': '后端开发', 'isSchoolJob': 0, 'subwayline': '1号线', 'stationname': '天府五街', 'linestaion': '1号线_天府五街', 'companyFullName': '广州碧软信息科技有限公司', 'formatCreateTime': '1天前发布'}
{'companyId': 67804, 'positionId': 4066429, 'industryField': '移动互联网,硬件', 'education': '本科', 'workYear': '3-5年', 'city': '成都', 'positionAdvantage': '百万级产品,高薪资,六险一金,氛围轻松', 'createTime': '2018-03-02 15:59:37', 'salary': '12k-15k', 'positionName': 'python开发工程师', 'companySize': '50-150人', 'companyShortName': '俊云科技', 'companyLogo': 'i/image/M00/58/0F/Cgp3O1fSFEuAQJnSAAATSLVt79k366.jpg', 'financeStage': '不需要融资', 'jobNature': '全职', 'approve': 1, 'companyLabelList': ['技能培训', '股票期权', '绩效奖金', '岗位晋升'], 'publisherId': 8597794, 'score': 0, 'district': '高新区', 'positionLables': ['旅游', '游戏', '文化娱乐', '游戏开发', 'php'], 'industryLables': ['旅游', '游戏', '文化娱乐', '游戏开发', 'php'], 'businessZones': None, 'hitags': None, 'resumeProcessRate': 100, 'resumeProcessDay': 1, 'adWord': 0, 'longitude': '0.0', 'latitude': '0.0', 'imState': 'today', 'lastLogin': 1519982057000, 'explain': None, 'plus': None, 'pcShow': 0, 'appShow': 0, 'deliver': 0, 'gradeDescription': None, 'promotionScoreExplain': None, 'firstType': '开发/测试/运维类', 'secondType': '后端开发', 'isSchoolJob': 0, 'subwayline': None, 'stationname': None, 'linestaion': None, 'companyFullName': '昆明俊云科技有限公司', 'formatCreateTime': '15:59发布'}
{'companyId': 155487, 'positionId': 3490584, 'industryField': '移动互联网', 'education': '本科', 'workYear': '1-3年', 'city': '成都', 'positionAdvantage': '绩效奖金,带薪年假', 'createTime': '2018-03-01 10:12:46', 'salary': '8k-12k', 'positionName': 'Python', 'companySize': '15-50人', 'companyShortName': '捷云智慧', 'companyLogo': 'i/image/M00/6A/FF/CgqKkVgZsBGAaIu2AABxzOn4w3A040.png', 'financeStage': '不需要融资', 'jobNature': '全职', 'approve': 1, 'companyLabelList': [], 'publisherId': 7309781, 'score': 0, 'district': '高新区', 'positionLables': ['云计算', 'node.js', 'django'], 'industryLables': ['云计算', 'node.js', 'django'], 'businessZones': ['中和'], 'hitags': None, 'resumeProcessRate': 45, 'resumeProcessDay': 1, 'adWord': 0, 'longitude': '104.066259', 'latitude': '30.546215', 'imState': 'today', 'lastLogin': 1519954055000, 'explain': None, 'plus': None, 'pcShow': 0, 'appShow': 0, 'deliver': 0, 'gradeDescription': None, 'promotionScoreExplain': None, 'firstType': '开发/测试/运维类', 'secondType': '后端开发', 'isSchoolJob': 0, 'subwayline': '1号线', 'stationname': '世纪城', 'linestaion': '1号线_天府五街;1号线_天府三街;1号线_世纪城', 'companyFullName': '成都捷云智慧科技有限公司', 'formatCreateTime': '1天前发布'}
......

根据运行结果发现,jobsInfo中的数据类型为列表,并且包含了整个页面的所显示的所有职位的信息。所以可以采用遍历的方法,整理出每一个职位的相关信息,并提取出需要的数据,如公司名字、职位优势等。

import requests

url = 'https://www.lagou.com/jobs/positionAjax.json?px=default&city=%E6%88%90%E9%83%BD&needAddtionalResult=false&isSchoolJob=0'

HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3346.9 Safari/537.36',
    'Referer': 'https://www.lagou.com/jobs/list_python?px=default&city=%E6%88%90%E9%83%BD'
}

form_data = {'first': 'true',
             'pn': '1',
             'kd': 'python'}


def getJobs():
    res = requests.post(url=url, headers=HEADERS, data=form_data)
    result = res.json()
    jobs = result['content']['positionResult']['result']
    return jobs

for job in getJobs():
    # 这里演示,提取出公司名以及公司职位优势
    print(job['companyFullName'] + ':' + job['positionAdvantage'])

运行结果,其余的相关信息可以自己根据key进行获取:

广州碧软信息科技有限公司:五险一金 周末双休 带薪年假
昆明俊云科技有限公司:百万级产品,高薪资,六险一金,氛围轻松
成都捷云智慧科技有限公司:绩效奖金,带薪年假
四川长虹电器股份有限公司:五险一金,年终奖,项目奖
四川海德智和科技有限公司:发展前景良好
北京链商电子商务有限公司:带薪年假,零食无限,福利多多,美女如云
网信集团有限公司:六险一金,弹性工作制,股票期权,饭补
..................

最终代码-写入Excel,修改代码传入参数使其具有通用性

关于Python操作Excel的模板读写主要有xlrdxlwtxlutilsopenpyxlxlsxwriter几种。这里使用的是XlsxWriter,可以参考我之前一篇博客【链接】

import requests
import xlsxwriter
from bs4 import BeautifulSoup
import time
import sys
import logging

# 请求职位列表的url
job_url = 'https://www.lagou.com/jobs/positionAjax.json?px=default&city={city}&needAddtionalResult=false&isSchoolJob={school}'

# 根据传入的关键字获取搜索结果中的第一页页面
first_page_url = 'https://www.lagou.com/jobs/list_{job}?isSchoolJob={school}&city={city}'

HEADERS = {
    'Host': 'www.lagou.com',
    'Origin': 'https://www.lagou.com',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3346.9 Safari/537.36',
    'Referer': 'https://www.lagou.com/jobs/list_python'
}


def getJobs(job, city, school):
    # 获取最大页数
    # logging.info('Home Page:' + first_page_url.format(job=job, school=school, city=city))  # format格式填写请求url
    print('Home Page ==> ' + first_page_url.format(job=job, school=school, city=city))
    first_page = requests.get(url=first_page_url.format(job=job, school=school, city=city), headers=HEADERS)
    html = first_page.text
    obj = BeautifulSoup(html, 'lxml')
    try:
        max_page = int(obj.find('span', {'class': 'span totalNum'}).get_text())  # 提取页面中的总页码
    except:
        max_page = 0
    jobs_info_list = list()
    if max_page > 0:
        for page_num in range(1, max_page + 1):
            form_data = {'pn': page_num, 'kd': job}
            res = requests.post(url=job_url.format(city=city, school=school), data=form_data, headers=HEADERS)
            now_cookies = res.cookies
            HEADERS.update(now_cookies)  # 更新HEADERS信息
            result = res.json()
            if result['success']:
                jobs = result['content']['positionResult']['result']
                time.sleep(1)  # 获取正常的情况下延时1s请求一次
            else:
                jobs = []
                time.sleep(10)  # 出现异常时,间隔10s后再获取
            print('Total:' + str(max_page) + '  Completed:' + str(page_num))
            jobs_info_list.extend(jobs)
    return jobs_info_list


# 将数据写入工作表中
def writeExcel(row=0, positionName='', area='', education='', salary='', companyFullName='', positionAdvantage='',
               type=''):
    if row == 0:
        worksheet.write(row, 0, '公司名')
        worksheet.write(row, 1, '薪资')
        worksheet.write(row, 2, '地区')
        worksheet.write(row, 3, '教育程度')
        worksheet.write(row, 4, '公司全名')
        worksheet.write(row, 5, '职位优势')
        worksheet.write(row, 6, '工作类型')
    else:
        worksheet.write(row, 0, positionName)
        worksheet.write(row, 1, salary)
        worksheet.write(row, 2, area)
        worksheet.write(row, 3, education)
        worksheet.write(row, 4, companyFullName)
        worksheet.write(row, 5, positionAdvantage)
        worksheet.write(row, 6, type)


def start(job, city, school):
    cursor = 0
    job_list = getJobs(job=job, city=city, school=school)
    writeExcel(row=cursor)
    for each_job in job_list:
        # 需要写入Excel的职位信息条目
        if each_job['positionName']:
            cursor += 1
            writeExcel(row=cursor, positionName=each_job['positionName'],
                       companyFullName=each_job['companyFullName'], area=each_job['city'] + each_job['district'],
                       education=each_job['education'], salary=each_job['salary'],
                       positionAdvantage=each_job['positionAdvantage'],
                       type=each_job['firstType'] + '-' + each_job['secondType'])


if __name__ == '__main__':
    job_city_school = sys.argv[1:]  # 接受程序外部传入的参数
    try:
        if job_city_school:
            job = job_city_school[0]
            city = job_city_school[1]
            school = job_city_school[2]
            # 创建一个excel表格
            workbook = xlsxwriter.Workbook(job + '-' + city + '.xlsx')
            # 为创建的excel表格添加一个工作表
            worksheet = workbook.add_worksheet()
            start(job, city, school)
            workbook.close()
            print('所有条目写入完成..')
    except ValueError:
        print('输入文本有误,请按照顺序 => 职位 城市 校招 ')

运行程序

> python lagouJobs.py python 成都 0     # 职位 城市 校招 作为参数直接传给py程序

运行结果:

Total:5  Completed:1
Total:5  Completed:2
Total:5  Completed:3
Total:5  Completed:4
Total:5  Completed:5
所有条目写入完成..

这样我们就已经把我们所需要的招聘信息爬取下来了

python 抓取网页提交表单 python 爬虫抓取网页数据导出excel_异步_04