--- title: "02-爬虫系统建设" created: 2026-04-03 tags: - 项目 aliases: - 爬虫系统建设 --- # 爬虫系统建设 > **—— 从零搭建一套可维护、可扩展、可自愈的爬虫** ## **一、先建立正确认知:爬虫不是"抓网页",而是"建立数据生产线"** 很多人学爬虫,停留在: - `requests` 请求网页 - `BeautifulSoup` / `XPath` 抽内容 - 存 CSV 这只能算**"脚本"**。 真正能长期跑、能维护、能适应网站变化的爬虫,应该是一条**完整的数据生产线**: ```text 目标发现 → 站点分析 → 采集策略选择 → 页面获取 → 数据解析 → 清洗标准化 → 去重校验 → 存储落盘 → 上传入库 → 监控报警 → 规则迭代 → 自动修复 ``` ![[image-d7bfacd6.png]] > “配置驱动 + 多策略抓取 + 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`**。** 应该先判断目标站属于哪种类型,然后选抓取策略。 ![[image-061e0185.png]] ### **场景 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` - `