--- 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` - `