㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期爬虫难度指数:⭐⭐⭐
🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [📌 摘要(Abstract)](#📌 摘要(Abstract))
- [🎯 背景与需求(Why)](#🎯 背景与需求(Why))
-
- [为什么要爬 Boss直聘?](#为什么要爬 Boss直聘?)
- 目标字段清单
- [⚖️ 合规与注意事项(必读)](#⚖️ 合规与注意事项(必读))
- [🛠️ 技术选型与整体流程(What/How)](#🛠️ 技术选型与整体流程(What/How))
-
- [静态 vs 动态 vs API](#静态 vs 动态 vs API)
- 整体流程
- [为什么不用 Selenium/Scrapy?](#为什么不用 Selenium/Scrapy?)
- [📦 环境准备与依赖安装](#📦 环境准备与依赖安装)
- [🌐 核心实现:请求层(Fetcher)](#🌐 核心实现:请求层(Fetcher))
- [🔍 核心实现:解析层(Parser)](#🔍 核心实现:解析层(Parser))
- [💾 数据存储与导出(Storage)](#💾 数据存储与导出(Storage))
- [🚀 运行方式与结果展示](#🚀 运行方式与结果展示)
- [🛠️ 常见问题与排错](#🛠️ 常见问题与排错)
-
- [问题1:403/429 错误(频率限制)](#问题1:403/429 错误(频率限制))
- 问题2:页面抓到空壳(动态加载未完成)
- 问题3:选择器失效(页面改版)
- 问题4:中文乱码
- [🚀 进阶优化](#🚀 进阶优化)
-
- [1. 异步并发(提升效率)](#1. 异步并发(提升效率))
- [2. 断点续爬](#2. 断点续爬)
- [3. 日志与监控](#3. 日志与监控)
- [4. 定时任务](#4. 定时任务)
- [📚 总结与延伸阅读](#📚 总结与延伸阅读)
- [⚠️ 最终提醒](#⚠️ 最终提醒)
- [🌟 文末](#🌟 文末)
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: 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输出
城市筛选 反爬检测 异常捕获 空值处理 去重策略
核心模块设计:
- Fetcher(请求层):浏览器实例管理、页面待策略
- Parser(解析层):选择器配置、字段抽取、容错处理
- 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()
Headers 与 Cookie 管理
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 轮换、行为模拟、频率控制)
✅ 建立容错机制(重试、异常捕获、字段缺失处理)
下一步可以做什么?
技术深化:
- 分布式爬虫:使用 Scrapy-Redis 实现多机协同
- 智能反爬:研究验证码识别(OCR、打码平台)、IP 代理池管理
- 数据分析:对采集的职位数据进行薪资分析、技能热度统计、地域分布可视化
工程化升级:
- 容器化部署:使用 Docker 封装爬虫环境
- 调度系统:接入 Airflow 实现任务编排
- 监控告警:集成 Prometheus + Grafana 监控爬虫健康度
学习资源:
- Playwright 官方文档:https://playwright.dev/python/
- Scrapy 最佳实践:https://docs.scrapy.org/
- 《Python 网络三版)
- 反爬虫对抗技术研究论文:搜索 "Web Scraping Detection"
⚠️ 最终提醒
本文所有技术方案仅供学习交流,实际应用时务必:
- 遵守网站 robots.txt 协议
- 尊重数据版权,不用于商业目的
- 控制爬取频率,避免对服务器造成压力
- 遵守《网络安全法》等相关法律法规
技术本身没有对错,关键在于如何使用。希望这篇文章能帮你理解现代爬虫技术的核心原理,并在合规的前提下解决实际问题。如果有任何疑问,欢迎交流探讨!🤝
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)

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