---
title: "16-爬虫系统建设实战"
created: 2026-04-03
tags:
- 博客
aliases:
- 爬虫系统建设实战
---
# 爬虫系统建设实战
## 爬虫系统建设实战文档
**—— 从零搭建一套可维护、可扩展、可自愈的招聘信息爬虫**
---
## 一、先建立正确认知:爬虫不是"抓网页",而是"建立数据生产线"
很多人学爬虫,停留在:
- `requests` 请求网页
- `BeautifulSoup` / `XPath` 抽内容
- 存 CSV
这只能算**"脚本"**。
真正能长期跑、能维护、能适应网站变化的爬虫,应该是一条**完整的数据生产线**:
```text
目标发现
→ 站点分析
→ 采集策略选择
→ 页面获取
→ 数据解析
→ 清洗标准化
→ 去重校验
→ 存储落盘
→ 上传入库
→ 监控报警
→ 规则迭代
→ 自动修复
```
```mermaid
flowchart LR
A[目标发现] --> B[站点分析]
B --> C[采集策略选择]
C --> D[页面获取]
D --> E[数据解析]
E --> F[清洗标准化]
F --> G[去重校验]
G --> H[存储落盘]
H --> I[上传入库]
I --> J[监控报警]
J --> K[规则迭代]
K --> L[自动修复]
L -.-> C
```
你给的这套系统可取之处就在于:它已经不是单点爬虫,而是**"配置驱动 + 多策略抓取 + AI解析 + 自动修复 + 数据闭环"**。
所以如果你未来要自己做爬虫,应该按下面这些步骤来建设。
---
## 二、做一套爬虫,建议分成 10 个阶段
### 阶段 1:先定义目标,而不是先写代码
#### 你先要回答 6 个核心问题
| 序号 | 问题 | 典型选项 |
| --- | --- | --- |
| ❶ | **我要抓什么** | 招聘职位?招聘公告?商品价格?新闻正文?评论? |
| ❷ | **数据最终要长什么样** | 标题、发布时间、地点、岗位名称、招聘类型、正文、图片、附件、投递链接 |
| ❸ | **数据源是什么类型** | 静态网页、动态渲染网页、接口 JSON、小程序接口、图片海报、PDF、视频字幕 |
| ❹ | **抓一次就行,还是要长期增量更新** | 一次性采集、每日更新、每小时更新、实时监控 |
| ❺ | **后续怎么用** | 搜索、展示、推荐、数据分析、自动通知 |
| ❻ | **允许多大维护成本** | 只抓几个站可以写死;上百站点必须配置化;长期运行必须自动监控和自动修复 |
#### 这套代码给你的经验
它不是"抓几个公司页面",而是一开始就明确了:
| 维度 | 设计 |
| --- | --- |
| 目标 | 招聘公告 / 职位 |
| 输出 | 结构化 JSON |
| 场景 | 公司官网、学校就业网、图片海报、接口数据 |
| 运行方式 | 定时任务 + 分片 |
| 后续 | 上传正式环境数据库 |
> **核心经验:你以后做任何爬虫,第一步都要先设计统一数据模型。不要先抓,抓完再想怎么存——那样后面一定会返工。**
#### 统一数据模型设计示例
```html
{
"announcement": {
"title": "2026年春季校园招聘公告",
"company_name": "某科技有限公司",
"publish_time": "2026-03-15",
"source_url": "https://example.com/jobs/123",
"content_html": "
...
",
"content_text": "纯文本正文...",
"category": "校招",
"graduation_year": "2026届",
"apply_method": "在线投递",
"images": ["https://obs.xxx/img1.jpg"],
"attachments": ["https://obs.xxx/岗位表.pdf"]
},
"positions": [
{
"job_name": "Java开发工程师",
"city": "北京",
"diploma": "本科及以上",
"salary": "15k-25k",
"headcount": 5,
"deadline": "2026-04-30",
"apply_link": "https://example.com/apply/456"
}
]
}
```
---
## 三、阶段 2:站点分析——决定用什么抓法
这是最关键的一步。**不要一上来就 `requests` + `XPath`。**
你应该先判断目标站属于哪种类型,然后选抓取策略。
```mermaid
flowchart TD
START[目标站点] --> CHECK1{view-source 里有数据?}
CHECK1 -->|是| A[场景A:静态HTML]
CHECK1 -->|否| CHECK2{Network 面板有 JSON 接口?}
CHECK2 -->|是| C[场景C:前后端分离/API]
CHECK2 -->|否| CHECK3{数据藏在 JS 文件/变量里?}
CHECK3 -->|是| D[场景D:JS嵌入数据]
CHECK3 -->|否| CHECK4{需要滚动/点击/等待?}
CHECK4 -->|是| B[场景B:动态渲染SPA]
CHECK4 -->|否| CHECK5{主要内容在图片/PDF里?}
CHECK5 -->|是| E[场景E:图片/PDF]
CHECK5 -->|否| F[回到场景B或组合方案]
A --> MA[requests + lxml/BS4]
B --> MB[Playwright/Selenium]
C --> MC[直调API 或 拦截响应]
D --> MD[请求JS文件 + 正则抽JSON]
E --> ME[OCR + PDF解析]
```
### 场景 A:页面源码里就有数据
**特征:**
- `view-source:` 里能看到职位名称、正文、链接
- 页面刷新后内容不变
- 没有复杂 JS 动态请求
**方案:**
- `requests` + `lxml` + `BeautifulSoup` + 正则辅助
**优点:**
- 快、成本低、稳定、并发高
**风险:**
- 网站稍微改 DOM 就失效
**适合:**
- 新闻站、公告页、简单企业官网
**代码示例:**
```python
import requests
from lxml import etree
def fetch_static_page(url, selectors):
"""静态页面抓取标准流程"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
resp = requests.get(url, headers=headers, timeout=30)
resp.encoding = resp.apparent_encoding # 自动检测编码
tree = etree.HTML(resp.text)
items = []
for node in tree.xpath(selectors['list_xpath']):
item = {
'title': ''.join(node.xpath(selectors['title_xpath'])).strip(),
'url': ''.join(node.xpath(selectors['url_xpath'])).strip(),
'publish_time': ''.join(node.xpath(selectors['time_xpath'])).strip(),
}
# 补全相对链接
if item['url'] and not item['url'].startswith('http'):
from urllib.parse import urljoin
item['url'] = urljoin(url, item['url'])
items.append(item)
return items
```
---
### 场景 B:页面是 JS 动态渲染
**特征:**
- `requests` 抓源码只有框架,没有正文
- F12 的 Elements 面板有内容,但 page source 里没有
- 需要滚动、点击、切换 tab 才出现内容
**方案:**
- Playwright / Selenium 模拟浏览器
- 等待 DOM 渲染、处理滚动加载、点击 tab、翻页
**这套系统怎么做的:**
它在 `spider_com.py` 里用 Playwright 打开列表页和详情页,并且支持:
- tab 点击
- 分页点击
- 当前页操作
- 页面等待
- 动态内容加载
**代码示例:**
```python
from playwright.sync_api import sync_playwright
def fetch_dynamic_page(url, wait_selector=None, scroll=False, click_selectors=None):
"""动态页面抓取标准流程"""
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
)
page = context.new_page()
try:
page.goto(url, timeout=60000, wait_until='networkidle')
# 等待关键元素出现
if wait_selector:
page.wait_for_selector(wait_selector, timeout=15000)
# 执行点击操作(如切换tab)
if click_selectors:
for selector in click_selectors:
try:
page.click(selector, timeout=5000)
page.wait_for_timeout(2000) # 等待内容加载
except Exception:
pass
# 处理滚动加载
if scroll:
for _ in range(5):
page.evaluate('window.scrollBy(0, window.innerHeight)')
page.wait_for_timeout(1500)
html_content = page.content()
return html_content
finally:
browser.close()
```
> **经验:能不用浏览器就不用,因为浏览器贵。但遇到动态站点,不要硬抠 `requests`,直接上 Playwright。**
---
### 场景 C:前后端分离,网页本身只是壳,真实数据在接口里
**特征:**
- 页面上的数据是 Ajax 请求回来的
- F12 Network 里能看到 JSON
- HTML 本身没数据
**方案优先级:**
1. 直接调用接口
2. 不行再用浏览器拦截响应
3. 最后才解析 HTML
**这套系统的两种成熟做法:**
**做法 1:API 直连**
比如百度、京东、软通、金蝶这类,直接请求接口拿 JSON。
```python
import requests
def fetch_api_direct(api_url, params=None, headers=None):
"""API直连抓取"""
default_headers = {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json',
'Referer': 'https://example.com/careers'
}
if headers:
default_headers.update(headers)
resp = requests.get(api_url, params=params, headers=default_headers, timeout=30)
data = resp.json()
# 标准化处理
items = []
for job in data.get('data', {}).get('list', []):
items.append({
'title': job.get('positionName', ''),
'city': job.get('cityName', ''),
'publish_time': job.get('publishDate', ''),
'url': f"https://example.com/job/{job.get('id', '')}",
})
return items
```
**做法 2:拦截浏览器响应**
比如兴业银行,用 `page.on("response", handler)` 监听浏览器收到的 Ajax 响应,然后直接解析 JSON。
```python
from playwright.sync_api import sync_playwright
import json
def fetch_by_intercept(page_url, api_pattern):
"""浏览器响应拦截抓取"""
captured_data = []
def handle_response(response):
if api_pattern in response.url:
try:
body = response.json()
captured_data.append(body)
except Exception:
pass
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.on("response", handle_response)
page.goto(page_url, wait_until='networkidle')
page.wait_for_timeout(5000)
browser.close()
return captured_data
```
> **为什么这很值得学:因为很多人明明可以拿 JSON,还在那里费劲解析 HTML。这是低效的。**
**你以后应该怎么判断:**
```text
1. 先看接口能不能直接调
→ 2. 不行看能不能在浏览器里拦截响应
→ 3. 再不行才解析页面 DOM
```
---
### 场景 D:数据藏在 JS 文件或 script 变量里
**特征:**
- 页面加载一个 `jobs.js`
- `