Python爬虫实战:Boss直聘职位数据采集实战 - Playwright + 结构化解析完整方案(附CSV导出 + SQLite持久化存储)!

㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~

㊙️本期爬虫难度指数:⭐⭐⭐

🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。

💕订阅后更新会优先推送,按目录学习更高效💯~

📌 摘要(Abstract)

本文将带你从零实现一个 Boss直聘职位爬虫,使用 Playwright 处理动态渲染页面,通过 CSS Selector 进行结构化字段抽取,最终输出包含职位名称、薪资范围、公司信息、技能要求等字段的结构化数据集。

读完你将获得:

  • 掌握 Playwright 自动化浏览器的核心用法(等待策略、元素定位、异常处理)
  • 学会设计可维护的选择器管理体系(字段映射表 + 容错机制)
  • 理解动态站点的反爬特征及应对策略(UA轮换、行为模拟、频率控制)

🎯 背景与需求(Why)

为什么要爬 Boss直聘?

在技术招聘市场分析、求职信息聚合、薪资水平调研等场景下,我们常需要批量获取职位数据。Boss直聘作为主流招聘平台,其数据具有:

  • 时效性强:职位更新频繁,反映市场实时需求
  • 字段丰富:薪资、技能栈、福利待遇、公司规模等维度完整
  • 覆盖面广:从初级到高级岗位,从大厂到创业公司

然而,Boss直聘采用了 React 框架渲染,页面内容通过 JavaScript 动态生成,传统的 requests + BeautifulSoup 方案会抓到空壳 HTML,必须使用浏览器自动化工具。

目标字段清单

字段名 说明 示例值
job_title 职位名称 Python后端工程师
salary_range 薪资范围 20-35K·14薪
experience 经验要求 3-5年
education 学历要求 本科
company_name 公司名称 字节跳动
company_scale 公司规模 10000人以上
company_stage 融资阶段 已上市
job_tags 技能标签 ['Django', 'MySQL', 'Redis']
welfare_tags 福利标签 ['五险一金', '带薪年假']
job_url 详情链接 https://...

⚖️ 合规与注意事项(必读)

robots.txt 说明

Boss直聘的 robots.txt 文件并未完全禁止爬取,但这不代表可以肆意采集。我们需要:

  • ✅ 尊重网站带宽资源,设置合理请求间隔(建议 3-5 秒)
  • ✅ 仅采集公开可见的职位信息(不绕过登录墙)
  • ✅ 不采集用户隐私数据(HR 联系方式、求职者简历)
  • ✅ 数据仅用于个人学习/研究,不用于商业目的

频率控制原则

python 复制代码
# ❌ 错误示范:攻击式并发
for url in urls:
    scrape(url)  # 瞬间发起数百请求

# ✅ 正确做法:模拟人类行为
import random
time.sleep(random.uniform(3, 5))  # 随机间隔 3-5 秒

技术边界

本文展示的技术方案仅用于教学,实际使用时需注意:

  • 不应突破网站的技术防护措施(如滑块验证强制绕过)
  • 不应对服务器造成过载压力
  • 遵守《网络安全法》《数据安全法》等相关法律法规

🛠️ 技术选型与整体流程(What/How)

静态 vs 动态 vs API

Boss直聘属于典型的动态渲染站点:

  • 页面源码中职位数据为空(<div id="root"></div>
  • 内容通过 React 组件挂载后生成
  • 接口有加密参数(sign、timestamp 等),逆向成本高

因此选择 Playwright 方案:

  • ✅ 完整模拟真实浏览器行为
  • ✅ 自动处理 JavaScript 渲染
  • ✅ 支持等待策略(网络空闲、元素可见)
  • ✅ 可截图调试、录制脚本

整体流程

json 复制代码
[搜索条件] → [列表页采集] → [详情页跳转] → [字段解析] → [数据清洗] → [存储导出]
     ↓              ↓              ↓              ↓              ↓
  关键词设置      翻页控制      元素等待      选择器映射      CSV输出
  城市筛选       反爬检测      异常捕获      空值处理       去重策略

核心模块设计:

  1. Fetcher(请求层):浏览器实例管理、页面待策略
  2. Parser(解析层):选择器配置、字段抽取、容错处理
  3. Storage(存储层):数据去重、格式转换、持久化

为什么不用 Selenium/Scrapy?

工具 优势 劣势 适用场景
Scrapy 高并发、分布式 不支持 JS 渲染(需配合 Splash) 静态站点大规模爬取
Playwright 现代化、性能好、反检测能力强 学习曲线稍陡 动态站点首选

📦 环境准备与依赖安装

Python 版本要求

bash 复制代码
# 推荐  --version  # 确认版本 >= 3.9

依赖安装

bash 复制代码
# 核心依赖
pip install playwright pandas lxml --break-system-packages

# 安装浏览器驱动(首次使用必行)
playwright install chromium

# 可选:数据分析相关
pip install openpyxl sqlalchemy --break-system-packages

项目结构

json 复制代码
boss_scraper/
├── config.py           # 配置文件(选择器、URL模板)
├── fetcher.py          # 请求层(浏览器控制)
├── parser.py           # 解析层(字段抽取)
├── storage.py          # 存储层(CSV/数据库)
├── main.py             # 入口文件
├── requirements.txt    # 依赖清单
└── data/
    ├── jobs.csv        # 输出数据
    └── logs/           # 运行日志

🌐 核心实现:请求层(Fetcher)

浏览器实例管理

python 复制代码
# fetcher.py
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
import random
import time

class BossFetcher:
    def __init__(self, headless=True):
        """
        初始化浏览器实例
        :param headless: 是否无头模式(True=后台运行,False=显示浏览器窗口)
        """
        self.playwright = sync_playwright().start()
        self.browser = self.playwright.chromium.launch(
            headless=headless,
            args=[
                '--disable-blink-features=AutomationControlled',  # 隐藏自动化特征
                '--window
            ]
        )
        self.context = self.browser.new_context(
            viewport={'width': 1920, 'height': 1080},
            user_agent=self._get_random_ua()
        )
        self.page = self.context.new_page()
        
        # 注入反检测脚本
        self.page.add_init_script("""
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined
            });
        """)
    
    def _get_random_ua(self):
        """UA 池轮换(模拟不同设备)"""
        ua_list = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
        ]
        return random.choice(ua_list)
    
    def fetch_page(self, url, wait_selector=None, timeout=30000):
        """
        访问页面并等待指定元素
        :param url: 目标 URL
        :param wait_selector: 等待的 CSS 选择器(如 '.job-list')
        :param timeout: 超时时间(毫秒)
        :return: 页面对象
        """
        try:
            # 访问页面
            self.page.goto(url, wait_until='domcontentloaded', timeout=timeout)
            
            # 等待关键元素出现
            if wait_selector:
                self.page.wait_for_selector(wait_selector, timeout=timeout)
            
            # 模拟人类行为:随机滚动
            self._random_scroll()
            
            # 随机等待(避免被识别为机器)
            time.sleep(random.uniform(2, 4))
            
            return self.page
        
        except PlaywrightTimeout:
            print(f"⚠️ 页面加载超时: {url}")
            return None
        except Exception as e:
            print(f"❌ 请求失败: {e}")
            return None
    
    def _random_scroll(self):
        """模拟人类滚动行为"""
        scroll_times = random.randint(2, 4)
        for _ in range(scroll_times):
            self.page.mouse.wheel(0, random.randint(300, 600))
            time.sleep(random.uniform(0.5, 1.5))
    
    def close(self):
        """关闭浏览器"""
        self.context.close()
        self.browser.close()
        self.playwright.stop()
python 复制代码
# 在 BossFetcher 类中添加
def set_cookies(self, cookies):
    """
    设置 Cookie(如需登录态)
    :param cookies: Cookie 字典列表
    """
    self.context.add_cookies(cookies)

# 使用示例
fetcher = BossFetcher()
fetcher.set_cookies([
    {'name': 'sessionId', 'value': 'xxx', 'domain': '.zhipin.com', 'path': '/'}
])

失败重试机制

python 复制代码
def fetch_with_retry(self, url, max_retries=3):
    """
    带重试的请求方法
    :param url: 目标 URL
    :param max_retries: 最大重试次数
    :return: 页面对象或 None
    """
    for attempt in range(max_retries):
        page = self.fetch_page(url, wait_selector='.job-list-box')
        if page:
            return page
        
        print(f"🔄 第 {attempt + 1} 次重试...")
        time.sleep(2 ** attempt)  # 指数退避:2秒 → 4秒 → 8秒
    
    print(f"❌ 重试失败,放弃该页面: {url}")
    return None

🔍 核心实现:解析层(Parser)

选择器配置管理

python 复制代码
# config.py
class SelectorConfig:
    """字段选择器映射表(集中管理,便于维护)"""
    
    # 列表页选择器
    JOB_CARD = '.job-card-wrapper'  # 职位卡片容器
    JOB_TITLE = '.job-name'
    SALARY = '.salary'
    COMPANY_NAME = '.company-name'
    JOB_TAGS = '.tag-list li'
    
    # 详情页选择器
    DETAIL_SALARY = '.job-detail .salary'
    EXPERIENCE = '.job-detail .experience'
    EDUCATION = '.job-detail .education'
    COMPANY_SCALE = '.company-info .scale'
    WELFARE_TAGS = '.welfare-list .tag'
    JOB_DESC = '.job-detail .detail-content'

字段抽取实现

python 复制代码
# parser.py
from lxml import etree
from config import SelectorConfig as SC

class BossParser:
    def __init__(self, page):
        """
        初始化解析器
        :param page: Playwright 页面对象
        """
        self.page = page
        self.html = page.content()
        self.tree = etree.HTML(self.html)
    
    def parse_job_list(self):
        """
        解析列表页职位卡片
        :return: 职位列表 [{'title': '', 'salary': '', ...}, ...]
        """
        jobs = []
        
        # 定位所有职位卡片
        cards = self.page.query_selector_all(SC.JOB_CARD)
        
        for card in cards:
            try:
                job = {
                    'title': self._safe_extract(card, SC.JOB_TITLE),
                    'salary': self._safe_extract(card, SC.SALARY),
                    'company': self._safe_extract(card, SC.COMPANY_NAME),
                    'tags': self._extract_tags(card, SC.JOB_TAGS),
                    'url': self._extract_url(card)
                }
                jobs.append(job)
            except Exception as e:
                print(f"⚠️ 解析单条职位失败: {e}")
                continue
        
        return jobs
    
    def _safe_extract(self, element, selector, default=''):
        """
        安全提取文本(容错处理)
        :param element: 父元素
        :param selector: CSS 选择器
        :param default: 默认值
        :return: 提取的文本
        """
        try:
            target = element.query_selector(selector)
            return target.inner_text().strip() if target else default
        except:
            return default
    
    def _extract_tags(self, element, selector):
        """
        提取标签列表
        :param element: 父元素
        :param selector: 标签选择器
        :return: 标签数组
        """
        try:
            tags = element.query_selector_all(selector)
            return [tag.inner_text().strip() for tag in tags]
        except:
            return []
    
    def _extract_url(self, card):
        """
        提取详情页链接
        :param card: 职位卡片元素
        :return: 完整 URL
        """
        try:
            link = card.query_selector('a.job-card-left')
            href = link.get_attribute('href')
            return f"https://www.zhipin.com{href}" if href else ''
        except:
            return ''
    
    def parse_job_detail(self):
        """
        解析详情页字段
        :return: 详细信息字典
        """
        detail = {
            'experience': self._safe_extract(self.page, SC.EXPERIENCE),
            'education': self._safe_extract(self.page, SC.EDUCATION),
            'company_scale': self._safe_extract(self.page, SC.COMPANY_SCALE),
            'welfare': self._extract_tags(self.page, SC.WELFARE_TAGS),
            'description': self._safe_extract(self.page, SC.JOB_DESC)
        }
        return detail

容错策略说明

python 复制代码
# 1. 字段缺失处理
def _safe_extract(self, element, selector, default=''):
    # 当选择器找不到元素时,返回默认值而非抛出异常
    
# 2. 选择器变化应对
# 方案一:备用选择器链
SALARY_SELECTORS = ['.salary', '.job-salary', '[class*="salary"]']

def _extract_with_fallback(self, element, selectors):
    for selector in selectors:
        result = self._safe_extract(element, selector)
        if result:
            return result
    return ''

# 方案二:模糊匹配
# 使用 XPath 的 contains() 函数
self.tree.xpath('//span[contains(@class, "salary")]//text()')

💾 数据存储与导出(Storage)

字段映射表

python 复制代码
# storage.py
import pandas as pd
from datetime import datetime

class JobStorage:
    def __init__(self, output_path='data/jobs.csv'):
        self.output_path = output_path
        self.data = []
        
        # 字段映射:数据库字段名 → 展示名称 → 数据类型
        self.field_mapping = {
            'title': ('职位名称', str),
            'salary': ('薪资范围', str),
            'experience': ('经验要求', str),
            'education': ('学历要求', str),
            'company': ('公司名称', str),
            'company_scale': ('公司规模', str),
            'tags': ('技能标签', list),
            'welfare': ('福利', list),
            'url': ('详情链接', str),
            'crawl_time': ('采集时间', str)
        }
    
    def add_job(self, job_dict):
        """
        添加单条职位数据
        :param job_dict: 职位字典
        """
        # 数据清洗
        cleaned = self._clean_data(job_dict)
        
        # 添加时间戳
        cleaned['crawl_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        
        self.data.append(cleaned)
    
    def _clean_data(self, job):
        """
        数据清洗与标准化
        """
        cleaned = {}
        
        # 薪资格式化(20-35K·14薪 → 20-35K)
        if 'salary' in job:
            cleaned['salary'] = job['salary'].split('·')[0]
        
        # 标签列表转字符串(便于 CSV 存储)
        if 'tags' in job and isinstance(job['tags'], list):
            cleaned['tags'] = ','.join(job['tags'])
        
        if 'welfare' in job and isinstance(job['welfare'], list):
            cleaned['welfare'] = ','.join(job['welfare'])
        
        # 复制其他字段
        for key in ['title', 'experience', 'education', 'company', 'company_scale', 'url']:
            cleaned[key] = job.get(key, '')
        
        return cleaned
    
    def save_to_csv(self):
        """导出为 CSV 文件"""
        df = pd.DataFrame(self.data)
        
        # 去重(基于 URL)
        df.drop_duplicates(subset=['url'], inplace=True)
        
        # 按字段映射排序列
        column_order = list(self.field_mapping.keys())
        df = df.reindex(columns=column_order)
        
        # 保存
        df.to_csv(self.output_path, index=False, encoding='utf-8-sig')
        print(f"✅ 数据已保存至: {self.output_path}")
        print(f"📊 共采集 {len(df)} 条职位数据")

去重策略

python 复制代码
# 方案一:URL 唯一性去重(推荐)
df.drop_duplicates(subset=['url'], keep='first', inplace=True)

# 方案二:内容 Hash 去重(适用于无 URL 场景)
import hashlib

def content_hash(job):
    """计算职位内容哈希值"""
    content = f"{job['title']}{job['company']}{job['salary']}"
    return hashlib.md5(content.encode()).hexdigest()

df['content_hash'] = df.apply(content_hash, axis=1)
df.drop_duplicates(subset=['content_hash'], inplace=True)

数据库存储(可选)

python 复制代码
# storage.py 新增方法
def save_to_mysql(self, db_config):
    """
    保存到 MySQL 数据库
    :param db_config: {'host': '', 'user': '', 'password': '', 'database': ''}
    """
    from sqlalchemy import create_engine
    
    engine = create_engine(
        f"mysql+pymysql://{db_config['user']}:{db_config['password']}"
        f"@{db_config['host']}/{db_config['database']}?charset=utf8mb4"
    )
    
    df = pd.DataFrame(self.data)
    df.to_sql('jobs', con=engine, if_exists='append', index=False)
    print("✅ 数据已入库")

🚀 运行方式与结果展示

入口文件

python 复制代码
# main.py
from fetcher import BossFetcher
from parser import BossParser
from storage import JobStorage
import time

def main():
    # 配置参数
    keyword = 'Python工程师'
    city = '101010100'  # 北京城市代码
    page_count = 3  # 爬取页数
    
    # 初始化组件
    fetcher = BossFetcher(headless=False)  # headless=True 为后台运行
    storage = JobStorage()
    
    try:
        for page in range(1, page_count + 1):
            print(f"\n{'='*50}")
            print(f"正在采集第 {page} 页...")
            print(f"{'='*50}")
            
            # 构造 URL
            url = f"https://www.zhipin.com/web/geek/job?query={keyword}&city={city}&page={page}"
            
            # 请求页面
            page_obj = fetcher.fetch_with_retry(url)
            if not page_obj:
                continue
            
            # 解析列表页
            parser = BossParser(page_obj)
            jobs = parser.parse_job_list()
            
            print(f"✅ 本页解析到 {len(jobs)} 条职位")
            
            # 存储数据
            for job in jobs:
                storage.add_job(job)
            
            # 翻页间隔(重要!)
            time.sleep(random.uniform(4, 6))
        
        # 导出数据
        storage.save_to_csv()
    
    finally:
        fetcher.close()

if __name__ == '__main__':
    main()

启动命令

bash 复制代码
# 方式一:直接运行
python main.py

# 方式二:指定参数(可扩展为命令行参数)
python main.py --keyword "数据分析师" --city "101020100" --pages 5

输出示例

终端输出:

json 复制代码
==================================================
正在采集第 1 页...
==================================================
✅ 本页解析到 15 条职位
==================================================
正在采集第 2 页...
==================================================
✅ 本页解析到 15 条职位
✅ 数据已保存至: data/jobs.csv
📊 共采集 28 条职位数据(去重后)

CSV 文件内容(jobs.csv):

职位名称 薪资范围 经验要求 学历要求 公司名称 技能标签
Python后端工程师 25-40K 3-5年 本科 字节跳动 Django,MySQL,Redis
数据分析师 18-30K 1-3年 本科 美团 Python,SQL,Tableau
爬虫工程师 20-35K 3-5年 本科 阿里巴巴 Scrapy,分布式,反爬

🛠️ 常见问题与排错

问题1:403/429 错误(频率限制)

现象:

json 复制代码
playwright._impl._errors.TimeoutError: Timeout 30000ms exceeded.

原因:

  • 请求频率过高触发反爬
  • UA 被识别为机器人
  • IP 被临时封禁

解决方案:

python 复制代码
# 1. 增加请求间隔
time.sleep(random.uniform(5, 8))  # 改为 5-8 秒

# 2. 更换 User-Agent
ua_list = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...',
    # 添加更多真实 UA
]

# 3. 使用代理池(如被封 IP)
self.context = self.browser.new_context(
    proxy={'server': 'http://proxy.example.com:8080'}
)

问题2:页面抓到空壳(动态加载未完成)

现象:

python 复制代码
jobs = parser.parse_job_list()  # 返回空列表

原因:

  • 等待策略不当(DOM 已加载但数据未渲染)
  • 选择器错误

排查步骤:

python 复制代码
# 1. 增加调试截图
page.screenshot(path='debug.png')

# 2. 打印页面源码
print(page.content()[:500])  # 查看前 500 字符

# 3. 改进等待策略
page.wait_for_selector('.job-list-box', state='visible')  # 等待可见
page.wait_for_load_state('networkidle')  # 等待网络空闲

问题3:选择器失效(页面改版)

现象:

json 复制代码
⚠️ 解析单条职位失败: 'NoneType' object has no attribute 'inner_text'

应对方案:

python 复制代码
# 1. 使用开发者工具重新定位选择器
# Chrome DevTools → Elements → 右键目标元素 → Copy → Copy selector

# 2. 采用更稳定的选择器策略
# ❌ 避免:依赖动态 class 名
'.css-1x9z3k7'  # 构建后生成的随机类名

# ✅ 推荐:使用语义化属性
'[data-jobid]'  # 自定义属性
'.job-title'    # 语义化类名

# 3. 定期维护选择器配置
# 在 config.py 中集中管理,便于批量更新

问题4:中文乱码

现象:

json 复制代码
职位名称,薪资范围
ç ´å  å ·ç¨ ,20-35K

解决:

python 复制代码
# 保存 CSV 时指定编码
df.to_csv('jobs.csv', encoding='utf-8-sig')  # BOM 头,Excel 兼容

# 读取时也需指定
df = pd.read_csv('jobs.csv', encoding='utf-8-sig')

🚀 进阶优化

1. 异步并发(提升效率)

python 复制代码
# async_fetcher.py
from playwright.async_api import async_playwright
import asyncio

class AsyncBossFetcher:
    async def fetch_multiple_pages(self, urls):
        """
        异步并发请求多个页面
        :param urls: URL 列表
        :return: 页面对象列表
        """
        async with async_playwright() as p:
            browser = await p.chromium.launch()
            
            # 创建多个页面并发请求
            tasks = []
            for url in urls:
                context = await browser.new_context()
                page = await context.new_page()
                tasks.append(self._fetch_single(page, url))
            
            results = await asyncio.gather(*tasks)
            await browser.close()
            return results
    
    async def _fetch_single(self, page, url):
        await page.goto(url)
        await page.wait_for_selector('.job-list-box')
        return page.content()

# 使用示例
async def main():
    fetcher = AsyncBossFetcher()
    urls = [f"https://www.zhipin.com/...?page={i}" for i in range(1, 6)]
    results = await fetcher.fetch_multiple_pages(urls)

asyncio.run(main())

2. 断点续爬

python 复制代码
# storage.py 新增方法
class JobStorage:
    def __init__(self, checkpoint_file='data/checkpoint.txt'):
        self.checkpoint_file = checkpoint_file
        self.crawled_urls = self._load_checkpoint()
    
    def _load_checkpoint(self):
        """加载已爬取的 URL"""
        try:
            with open(self.checkpoint_file, 'r') as f:
                return set(f.read().splitlines())
        except FileNotFoundError:
            return set()
    
    def is_crawled(self, url):
        """检查 URL 是否已爬取"""
        return url in self.crawled_urls
    
    def mark_crawled(self, url):
        """标记 URL 为已爬取"""
        self.crawled_urls.add(url)
        with open(self.checkpoint_file, 'a') as f:
            f.write(f"{url}\n")

# main.py 中使用
for job in jobs:
    if storage.is_crawled(job['url']):
        continue
    storage.add_job(job)
    storage.mark_crawled(job['url'])

3. 日志与监控

python 复制代码
# logger.py
import logging
from datetime import datetime

def setup_logger():
    logger = logging.getLogger('BossScraper')
    logger.setLevel(logging.INFO)
    
    # 文件处理器
    fh = logging.FileHandler(
        f"data/logs/scraper_{datetime.now().strftime('%Y%m%d')}.log"
    )
    fh.setFormatter(logging.Formatter(
        '%(asctime)s - %(levelname)s - %(message)s'
    ))
    
    logger.addHandler(fh)
    return logger

# main.py 中使用
logger = setup_logger()
logger.info(f"开始爬取第 {page} 页")
logger.warning(f"请求失败: {url}")
logger.error(f"解析异常: {e}")

# 统计指标
stats = {
    'total_requests': 0,
    'success_count': 0,
    'fail_count': 0,
    'success_rate': 0.0
}

4. 定时任务

bash 复制代码
# 使用 crontab(Linux/Mac)
# 每天凌晨 2 点执行
0 2 * * * cd /path/to/boss_scraper && python main.py >> data/logs/cron.log 2>&1

# 使用 APScheduler(Python 方案)
from apscheduler.schedulers.blocking import BlockingScheduler

scheduler = BlockingScheduler()

@scheduler.scheduled_job('cron', hour=2, minute=0)
def scheduled_job():
    main()

scheduler.start()

📚 总结与延伸阅读

本文完成了什么?

通过这个实战项目,我们构建了一个生产级别的动态网站爬虫

✅ 使用 Playwright 处理 JavaScript 渲染页面

✅ 设计可维护的选择器管理体系

✅ 实现完整的数据采集→解析→存储流程

✅ 掌握反爬应对策略(UA 轮换、行为模拟、频率控制)

✅ 建立容错机制(重试、异常捕获、字段缺失处理)

下一步可以做什么?

技术深化:

  1. 分布式爬虫:使用 Scrapy-Redis 实现多机协同
  2. 智能反爬:研究验证码识别(OCR、打码平台)、IP 代理池管理
  3. 数据分析:对采集的职位数据进行薪资分析、技能热度统计、地域分布可视化

工程化升级:

  1. 容器化部署:使用 Docker 封装爬虫环境
  2. 调度系统:接入 Airflow 实现任务编排
  3. 监控告警:集成 Prometheus + Grafana 监控爬虫健康度

学习资源:

⚠️ 最终提醒

本文所有技术方案仅供学习交流,实际应用时务必:

  • 遵守网站 robots.txt 协议
  • 尊重数据版权,不用于商业目的
  • 控制爬取频率,避免对服务器造成压力
  • 遵守《网络安全法》等相关法律法规

技术本身没有对错,关键在于如何使用。希望这篇文章能帮你理解现代爬虫技术的核心原理,并在合规的前提下解决实际问题。如果有任何疑问,欢迎交流探讨!🤝

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。技术无罪,责任在人!!!

相关推荐
夜瞬1 小时前
【Flask 框架学习】02:核心基本概念全解析
python·flask·web
ding_zhikai1 小时前
【Web应用开发笔记】Django笔记2:一个 Hello World 网页
笔记·后端·python·django
kyle~1 小时前
Python---webbrowser库 跨平台打开浏览器的控制接口
开发语言·python·web
一尘之中2 小时前
量子力学数学基础入门:从态矢到内积外积(附Python演示)
python·ai写作·量子计算
七夜zippoe2 小时前
性能测试实战:Locust负载测试框架深度指南
分布式·python·性能测试·locust·性能基准
有点心急10212 小时前
SQL 执行 MCP 工具开发(一)
人工智能·python·aigc
belldeep2 小时前
python:Flask 3, mistune 2, 实现在线编辑 Markdown 文档的 Web 服务程序
python·flask·markdown·mistune·pygments
apcipot_rain2 小时前
python 多进程多线程 学习笔记
笔记·python·学习
Albert Edison8 小时前
【Python】学生管理系统
开发语言·数据库·python