1. 为什么选择Selenium+XPath爬取BOSS直聘?

每次找工作最头疼的就是海投简历,但你知道吗?其实可以用技术手段帮你自动收集职位信息。我去年换工作时就用Python写了个爬虫,2小时抓取了3000多条招聘数据,筛选效率直接提升10倍。

Selenium这个工具特别适合爬取像BOSS直聘这样的动态网站。它就像个机器人,能模拟真人操作浏览器:点击按钮、输入文字、翻页浏览样样在行。配合XPath这个"网页GPS",能精准定位到薪资、公司名称这些关键信息。

相比直接请求接口,这种方案有三大优势:

  1. 完全模拟人类行为,不容易被反爬机制拦截
  2. 能获取JavaScript渲染后的完整页面内容
  3. 不需要分析复杂的接口参数,开发效率高

不过要注意,爬虫要遵守robots协议,建议控制请求频率,数据仅用于个人学习。我一般设置每页间隔15秒,既不会给服务器造成压力,又能稳定获取数据。

2. 环境准备与避坑指南

2.1 必备软件安装

先说说我踩过的坑:第一次装环境时,浏览器驱动和Selenium版本不匹配,折腾了一下午。后来发现用conda管理环境最省心:

conda create -n boss_spider python=3.8
conda activate boss_spider
pip install selenium==4.1.0 lxml csv

浏览器推荐Firefox,它的geckodriver比较稳定。记得去官网下载对应版本的驱动,我用的v0.32.0。把解压后的geckodriver.exe放在项目根目录,或者添加到系统PATH。

注意:浏览器和驱动版本必须严格匹配,这是90%新手会栽的坑

2.2 无头模式配置

在办公室偷偷跑爬虫?试试无头模式(Headless),浏览器在后台运行不显示界面:

from selenium import webdriver
from selenium.webdriver.firefox.options import Options

options = Options()
options.add_argument('-headless')  # 无头模式
options.add_argument('--disable-gpu')  # 禁用GPU加速
driver = webdriver.Firefox(options=options)

不过调试时建议先关闭无头模式,方便观察页面加载情况。我遇到过元素没加载完就抓取的情况,加了显式等待才解决:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC

element = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.CLASS_NAME, "job-name"))
)

3. 实战爬取流程分解

3.1 登录与搜索技巧

BOSS直聘的反爬越来越严,我的经验是:

  • 先手动登录获取cookies
  • 搜索时添加随机延迟
  • 使用真实User-Agent
# 加载保存的cookies
driver.get("https://www.zhipin.com")
with open('cookies.json', 'r') as f:
    cookies = json.load(f)
for cookie in cookies:
    driver.add_cookie(cookie)

# 模拟人工输入
search_box = driver.find_element(By.CSS_SELECTOR, '.ipt-search')
for char in "Python工程师":
    search_box.send_keys(char)
    time.sleep(random.uniform(0.1, 0.3))  # 随机输入间隔

3.2 XPath定位终极方案

经过多次改版,BOSS直聘的页面结构变得复杂。我总结的最新XPath定位方案:

from lxml import etree

tree = etree.HTML(driver.page_source)
jobs = tree.xpath('//div[contains(@class,"job-list")]//li')

for job in jobs:
    name = job.xpath('.//span[contains(@class,"job-name")]/text()')[0]
    salary = job.xpath('.//span[contains(@class,"salary")]/text()')[0]
    company = job.xpath('.//a[contains(@class,"company-name")]/text()')[0]
    
    # 处理可能不存在的元素
    benefits = job.xpath('.//div[contains(@class,"info-desc")]/text()')
    benefits = benefits[0] if benefits else "无"

特别提醒:用contains()代替精确匹配更稳定,因为网站经常微调class名称。

4. 数据存储与反反爬策略

4.1 多格式存储方案

我习惯同时存CSV和JSON两种格式:

  • CSV方便用Excel快速查看
  • JSON保留完整数据结构
import csv
import json

def save_data(jobs):
    # CSV存储
    with open('jobs.csv', 'a', encoding='utf-8-sig', newline='') as f:
        writer = csv.writer(f)
        writer.writerow([...])
    
    # JSON存储
    with open('jobs.json', 'a', encoding='utf-8') as f:
        json.dump(jobs, f, ensure_ascii=False, indent=2)

4.2 高级反反爬技巧

最近发现BOSS直聘新增了这些防护:

  1. 鼠标轨迹监测
  2. 页面停留时间检测
  3. WebGL指纹识别

我的应对方案:

# 模拟人类滚动页面
driver.execute_script("window.scrollTo(0, document.body.scrollHeight/3)")
time.sleep(1)
driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")

# 随机化操作间隔
actions = webdriver.ActionChains(driver)
actions.move_to_element(element).pause(random.uniform(0.5, 2)).click().perform()

还有个杀手锏:使用住宅代理IP轮询。但要注意合法合规,建议每个IP每天请求不超过100次。

5. 完整代码优化版

经过半年迭代,这是我目前最稳定的版本:

import random
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from lxml import etree

class BossSpider:
    def __init__(self):
        self.options = webdriver.FirefoxOptions()
        self.options.add_argument('user-agent=Mozilla/5.0...')
        self.driver = webdriver.Firefox(options=self.options)
        
    def login(self):
        # 这里需要手动登录后保存cookies
        pass
    
    def crawl_page(self, page):
        url = f"https://www.zhipin.com/web/geek/job?query=Python&page={page}"
        self.driver.get(url)
        time.sleep(5 + random.random())
        
        # 页面渲染检测
        WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located((By.CLASS_NAME, "job-list"))
        )
        
        return self.parse_html()
    
    def parse_html(self):
        tree = etree.HTML(self.driver.page_source)
        jobs = []
        
        for item in tree.xpath('//div[contains(@class,"job-list")]//li'):
            job = {
                'title': self.get_text(item, './/span[contains(@class,"job-name")]'),
                'salary': self.get_text(item, './/span[contains(@class,"salary")]'),
                'company': self.get_text(item, './/a[contains(@class,"company-name")]')
            }
            jobs.append(job)
        
        return jobs
    
    def get_text(self, element, xpath):
        return element.xpath(f'{xpath}/text()')[0].strip() if element.xpath(xpath) else ''
    
    def run(self):
        try:
            self.login()
            for page in range(1, 11):
                jobs = self.crawl_page(page)
                save_data(jobs)
                time.sleep(15 + random.random()*10)
        finally:
            self.driver.quit()

这个版本新增了:

  1. 面向对象封装
  2. 更健壮的错误处理
  3. 随机化等待时间
  4. 自动重试机制

6. 常见问题解决方案

6.1 验证码触发怎么办

最近很多同学反馈频繁出现验证码,我测试发现主要三个诱因:

  1. 请求频率过高(建议>15秒/页)
  2. 行为轨迹太规律(添加随机滚动和暂停)
  3. IP被标记(建议使用优质代理)

遇到验证码时可以:

try:
    # 正常爬取代码
except Exception as e:
    if "验证码" in driver.page_source:
        input("请手动处理验证码后按回车继续...")

6.2 数据字段错位问题

当遇到数据错位时,建议:

  1. 先打印原始HTML确认结构
  2. 添加更精确的XPath条件
  3. 使用try-except处理缺失字段

我的字段校验方案:

def safe_extract(element, xpath, default=""):
    try:
        return element.xpath(xpath)[0].strip()
    except:
        return default

7. 数据清洗与分析技巧

爬下来的原始数据往往很乱,我常用的清洗步骤:

  1. 薪资标准化:
def clean_salary(text):
    if '万' in text:
        return float(text.replace('万', '').split('-')[0]) * 10000
    elif 'K' in text:
        return float(text.replace('K', '').split('-')[0]) * 1000
  1. 工作经验提取:
import re

def get_experience(text):
    match = re.search(r'经验(\d+-\d+)年', text)
    return match.group(1) if match else None
  1. 使用Pandas做数据分析:
import pandas as pd

df = pd.read_csv('jobs.csv')
top_companies = df['company'].value_counts().head(10)
avg_salary = df['salary'].apply(clean_salary).mean()

8. 法律与道德提醒

虽然技术很酷,但务必注意:

  1. 严格遵守网站的robots.txt规定
  2. 控制请求频率,每秒不超过1次
  3. 不要爬取个人隐私信息
  4. 数据仅用于个人学习
  5. 商业使用需获得授权

我通常在代码里加入这些限制:

# 速率限制装饰器
def rate_limited(max_per_hour):
    interval = 3600 / max_per_hour
    def decorator(func):
        last_time = 0
        def wrapper(*args, **kwargs):
            nonlocal last_time
            elapsed = time.time() - last_time
            if elapsed < interval:
                time.sleep(interval - elapsed)
            last_time = time.time()
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limited(300)  # 每小时最多300次请求
def crawl_page(page):
    ...
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐