本项目预期结果:

alt text

使用DrissionPage爬取Boss直聘

之前库的缺点和局限性

requests库

requests库效率很高,但有明显缺点:

  1. 需要手动处理heads和cookies
  2. 难以应对各个网站的反爬机制
  3. 获取网页元素的难度高

selenium库

selenium库实现难度低,能较好的处理登录等情况,缺点:

  1. 代码效率低
  2. 难以处理json形式数据

举例代码


from selenium import webdriver
from selenium.webdriver.common.by import By
from openpyxl import Workbook
import time

browser = webdriver.Edge()
browser.maximize_window()
workbook = Workbook()
sheet = workbook.active
data_row=['岗位名称','薪资','岗位要求','单位名称']
sheet.append(data_row)

browser.get("https://www.zhipin.com/web/geek/job?query=%E6%95%B0%E6%8D%AE&city=101020100&degree=202")
browser.implicitly_wait(10)

for i in range(1,31):
    try:
        title_e = browser.find_element(By.XPATH, '//*[@id="wrap"]/div[2]/div[2]/div/div[1]/div[2]/ul/li[' + str(i) + ']/div[1]/a/div[1]/span[1]')
        title_e.click()
        browser.implicitly_wait(5)

        handles = browser.window_handles
        browser.switch_to.window(handles[-1])
        job_title = browser.find_element(By.XPATH, '//*[@id="main"]/div[1]/div/div/div[1]/div[2]/h1')
        print("岗位名称:"+job_title.text)
        salary = browser.find_element(By.XPATH, '//*[@id="main"]/div[1]/div/div/div[1]/div[2]/span')
        print("薪资:"+salary.text)
        requirements = browser.find_element(By.XPATH, '//*[@id="main"]/div[3]/div/div[2]/div[1]/div[2]')
        print("岗位要求:"+requirements.text)
        conmpany = browser.find_element(By.XPATH, '//*[@id="main"]/div[3]/div/div[1]/div[2]/div/a[2]')
        print("单位名称:"+conmpany.text)
        data_row=[job_title.text,salary.text,requirements.text,conmpany.text]
        sheet.append(data_row)
        browser.implicitly_wait(4)

        # 关闭当前标签页
        handles = browser.window_handles
        browser.close()
        browser.implicitly_wait(4)

        browser.switch_to.window(handles[0])
        time.sleep(2)
    except:
        print("cuo")
workbook.save(filename="example.xlsx")
print("完毕")
time.sleep(200)

DrissionPage库

DrissionPage 是一个基于 python 的网页自动化工具。
它既能控制浏览器,也能收发数据包,还能把两者合而为一。
可兼顾浏览器自动化的便利性和 requests 的高效率。
它功能强大,内置无数人性化设计和便捷功能。它的语法简洁而优雅,代码量少,对新手友好。

Drission 是作者自创的单词,为 Driver 和 Session 的合体,因此 Drission 读作“拽神”。

官方库:https://www.drissionpage.cn/

DrissionPage库,带有三个模式SessionPageChromiumPageWebPage
SessionPage类似requests
ChromiumPage类似selenium

WebPage 整合模式,后续操作一般使用WebPage模式。

安装DrissionPage库

请使用 pip 安装 DrissionPage:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple DrissionPage

WebPage是功能最全面的页面类,既可控制浏览器,也可收发数据包。

from DrissionPage import WebPage

启动浏览器

默认状态下,程序会自动在系统内查找 Chrome 路径。

执行以下代码,浏览器启动并且访问了项目文档,说明可直接使用,跳过后面的步骤即可

from DrissionPage import WebPage
page = WebPage()
page.get('https://www.bilibili.com/')

如果要使用Edge浏览器,打开Edge浏览器,在地址栏输入输入edge://version,回车。
alt text
如图所示,红框中就是要获取的路径。

指定路径浏览器,方法:
新建一个临时 py 文件,并输入以下代码,填入您电脑里的 Edge 浏览器可执行文件路径,然后运行。

from DrissionPage import ChromiumOptions

path = r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
ChromiumOptions().set_browser_path(path).save()

这段代码会把浏览器路径记录到配置文件,今后启动浏览器皆以新路径为准。

监听链接

找到页面中相关信息的json链接。

alt text

在代码中指定监听链接

wp.listen.start('zpgeek/search/joblist.json')

打开网页,监听内容并输出

wp=WebPage()
wp.listen.start('zpgeek/search/joblist.json')
wp.get("https://www.zhipin.com/web/geek/job?query=%E6%95%B0%E6%8D%AE&city=101020100&degree=202")

packet=wp.listen.wait()
print(packet.response.body)

当前已经成功爬取了一页的信息

找到下一页按钮

alt text
实现点击

wp.ele(".ui-icon-arrow-right").click()

实现循环

for i in range(1,11):
    packet=wp.listen.wait()
    print("第"+str(i))
    try:
        wp.ele(".ui-icon-arrow-right").click()
        time.sleep(2)
    except Exception as e :
        print(e)
        break

实现对json的解析

job_list=packet.response.body['zpData']['jobList']
for job in job_list:
        jobName=job['jobName']
        salaryDesc=job['salaryDesc']
        areaDistrict=job['areaDistrict']
        businessDistrict=job['businessDistrict']
        brandName=job['brandName']
        welfareList=job['welfareList']
        jobLabels=job['jobLabels']
        skills=job['skills']
        brandIndustry=job['brandIndustry']
        data_row=[jobName,salaryDesc,areaDistrict,businessDistrict,brandName,welfareList,jobLabels,skills,brandIndustry]
        print(data_row)

将信息存入csv文件中供后续分析

采集信息包括了
‘岗位名称’,‘薪资’,‘区域’,‘地点’,‘公司名称’,‘福利待遇’,‘经历要求’,‘技能要求’,‘行业’
先将信息存入DataFrame,后续直接将DataFrame转换成csv

import pandas as pd
column_names=['岗位名称','薪资','区域','地点','公司名称','福利待遇','经历要求','技能要求','行业']
df = pd.DataFrame(columns=column_names)
# ...............
# 在循环中,将每次循环数据加入df
        data_row=[jobName,salaryDesc,areaDistrict,businessDistrict,brandName,welfareList,jobLabels,skills,brandIndustry]
        df.loc[len(df)] = data_row

程序最后,将df转换成csv文件

df.to_csv('example.csv', index=False)
print("完毕")

结果如下:
alt text

对薪资数据进行处理

薪资
6-8K
10-15K·13薪
13-20K·14薪
150-200元/天

为统一计量单位,统一转换为年薪。
月薪以平均数计算。日薪 * 250 = 年薪

def parse_salary_range(salary_range):
    # 去除额外字符(如'·13薪'和'元/天'),同时判断是否按天计算
    if '·13薪' in salary_range:
        salary_range = salary_range.replace('·13薪', '')
        annual_bonus_months = 13
    elif '·14薪' in salary_range:
        salary_range = salary_range.replace('·14薪', '')
        annual_bonus_months = 14
    elif '·15薪' in salary_range:
        salary_range = salary_range.replace('·15薪', '')
        annual_bonus_months = 15
    elif '·16薪' in salary_range:
        salary_range = salary_range.replace('·16薪', '')
        annual_bonus_months = 16
    else:
        annual_bonus_months = 12  # 默认按照12个月计算

    if '元/天' in salary_range:
        salary_range = salary_range.replace('元/天', '')
        is_daily_salary = True
    else:
        is_daily_salary = False
    
    if is_daily_salary == False:
        salary_range = salary_range.replace('K', '')
        if '-' in salary_range:
            min_salary, max_salary = map(int, salary_range.split('-'))
        else:
            min_salary = max_salary = int(salary_range)
        average_yearly_salary = ((min_salary + max_salary) / 2) * (annual_bonus_months )*1000
        return average_yearly_salary
    if is_daily_salary == True:
        if '-' in salary_range:
            min_salary, max_salary = map(int, salary_range.split('-'))
        else:
            min_salary = max_salary = int(salary_range)
        average_yearly_salary = ((min_salary + max_salary) / 2) * 250
        return average_yearly_salary

用以上函数,实现对薪资的处理。输出为岗位平均年薪数据

完整代码

以下代码实现了对特定岗位基本信息的爬取。
下一步,我们将实现:

  1. 数据基础分析
  2. 对技能要求的分析和可视化处理
from DrissionPage import WebPage
from DrissionPage import ChromiumOptions
import time

import pandas as pd

def parse_salary_range(salary_range):
    # 去除额外字符(如'·13薪'和'元/天'),同时判断是否按天计算
    if '·13薪' in salary_range:
        salary_range = salary_range.replace('·13薪', '')
        annual_bonus_months = 13
    elif '·14薪' in salary_range:
        salary_range = salary_range.replace('·14薪', '')
        annual_bonus_months = 14
    elif '·15薪' in salary_range:
        salary_range = salary_range.replace('·15薪', '')
        annual_bonus_months = 15
    elif '·16薪' in salary_range:
        salary_range = salary_range.replace('·16薪', '')
        annual_bonus_months = 16
    else:
        annual_bonus_months = 12  # 默认按照12个月计算

    if '元/天' in salary_range:
        salary_range = salary_range.replace('元/天', '')
        is_daily_salary = True
    else:
        is_daily_salary = False
    
    if is_daily_salary == False:
        salary_range = salary_range.replace('K', '')
        if '-' in salary_range:
            min_salary, max_salary = map(int, salary_range.split('-'))
        else:
            min_salary = max_salary = int(salary_range)
        average_yearly_salary = ((min_salary + max_salary) / 2) * (annual_bonus_months )*1000
        return average_yearly_salary
    if is_daily_salary == True:
        if '-' in salary_range:
            min_salary, max_salary = map(int, salary_range.split('-'))
        else:
            min_salary = max_salary = int(salary_range)
        average_yearly_salary = ((min_salary + max_salary) / 2) * 250
        return average_yearly_salary


column_names=['岗位名称','薪资','区域','地点','公司名称','福利待遇','经历要求','技能要求','行业']
df = pd.DataFrame(columns=column_names)

path = r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
# ChromiumOptions().set_browser_path(path)
ChromiumOptions().set_browser_path(path).save()
# co = ChromiumOptions().auto_port()
wp=WebPage()
wp.listen.start('zpgeek/search/joblist.json')
wp.get("https://www.zhipin.com/web/geek/job?query=%E6%95%B0%E6%8D%AE&city=101020100&degree=202")
for i in range(1,11):
    packet=wp.listen.wait()
    # wp.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    print("第"+str(i))
    job_list=packet.response.body['zpData']['jobList']
    for job in job_list:
        
        jobName=job['jobName']
        # salaryDesc=job['salaryDesc']
        
        salaryDesc=job['salaryDesc']
        salaryDesc=parse_salary_range(salaryDesc)
        areaDistrict=job['areaDistrict']
        businessDistrict=job['businessDistrict']
        brandName=job['brandName']
        welfareList=job['welfareList']
        jobLabels=job['jobLabels']
        skills=job['skills']
        brandIndustry=job['brandIndustry']
        data_row=[jobName,salaryDesc,areaDistrict,businessDistrict,brandName,welfareList,jobLabels,skills,brandIndustry]
        df.loc[len(df)] = data_row
    
    try:
        wp.ele(".ui-icon-arrow-right").click()
        time.sleep(2)
    except Exception as e :
        print(e)
        break
df.to_csv('example.csv', index=False)
print("完毕")