--- title: "03-爬虫的基础积木——每个动作能得到什么效果" created: 2026-04-24 tags: - 项目 aliases: - 第 3 讲:爬虫的'基础积木'——每个动作能得到什么效果 --- # 爬虫的基础积木——每个动作能得到什么效果 前两讲我们解决了"爬虫是什么"和"有哪些采集通道"的问题。 这一讲开始进入实战的**第一步**:把爬虫拆成一个个最小的"动作积木"。 只要理解每块积木"输入什么、输出什么、解决什么问题",后面再看真实项目里那些上千行的脚本,就会发现——它们不过是这些积木的反复组合和封装而已。 讲按照"**发请求 → 解析内容 → 操作浏览器 → 监听接口 → 落盘存储 → 控制节奏 → 处理异常**"这个顺序展开,因为这正是数据从网络流到硬盘上的真实路径。 --- ## **一、最底层的积木:发一个 HTTP 请求** 爬虫的本质就是"程序代替人去发请求"。所以第一块积木,就是"发请求"。 ### **1.1** `requests.get(url)` **—— 拿到网页源码或接口数据** ```python import requests resp = requests.get("https://example.com") print(resp.status_code) # 200 表示成功 print(resp.text) # 网页 HTML 源码(字符串) print(resp.json()) # 如果是 API,直接解析成 Python 字典 ``` **能得到什么:** - 如果 URL 是网页,`resp.text` 就是 HTML 字符串,后面交给解析器处理。 - 如果 URL 是 API 接口,`resp.json()` 直接得到结构化数据(字典/列表),不需要任何解析。 **对应到项目里:** 通用结构化系统中"API 直连通道"(`auto_api/baidu_data_proc_api.py`)用的就是这种方式——百度、京东、金蝶等公司的招聘接口直接返回 JSON,根本不需要打开浏览器,效率最高、最稳定。 ### **1.2 给请求加"伪装" —— headers 和 cookies** 光会 `get` 还不够。很多网站一看你是个"裸请求"(没有 User-Agent、没有 Referer),直接拒绝你。 ```java headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", "Referer": "https://www.liepin.com/", } resp = requests.get(url, headers=headers, cookies={"sessionid": "xxx"}) ``` **能得到什么:** 让服务器以为"这是个正常用户从浏览器点过来的",大幅提高成功率。 **这是反爬的第一道门槛**,也是项目里所有 HTTP 请求都会带上 headers 的原因。 ### **1.3 加代理 —— 让请求换个"出口 IP"** ```java proxies = {"http": "http://1.2.3.4:8080", "https": "http://1.2.3.4:8080"} resp = requests.get(url, proxies=proxies) ``` **能得到什么:** 同一个 IP 抓太多次会被封,代理能让你"换张脸"继续抓。 **对应到项目里:** `spider.py` 基类里的 `getProxy()` / `setProxyRecord()` 就是把代理做成了一个"资源池"——不是一次性用,而是"领用 → 上报使用情况 → 触达上限就换",这是工程化爬虫和玩具爬虫最大的区别之一。 --- ## **二、解析积木:从 HTML / JSON 里把数据"挑"出来** 请求拿回来的东西要么是 HTML(一坨标签),要么是 JSON(一坨嵌套字典)。我们要把"职位标题、薪资、公司名"这些字段从里面挑出来。 #### **2.1 BeautifulSoup —— 从 HTML 里提取内容** ```python from bs4 import BeautifulSoup soup = BeautifulSoup(resp.text, "lxml") title = soup.select_one("h1.job-title").get_text(strip=True) links = [a["href"] for a in soup.select("a.job-link")] ``` **能得到什么:** 用类似 CSS 选择器的语法,把 HTML 里的标题、链接、文本批量提出来。 **适用场景:** 静态页面、详情页(项目里猎聘抓职位描述、地址、标签就是用这种方式)。 #### **2.2** `json.loads()` **—— 解析接口返回** ```python import json data = json.loads(resp.text) job_list = data["data"]["jobCardList"] for job in job_list: print(job["title"], job["salary"]) ``` **能得到什么:** 接口返回的数据天然就是结构化的,直接按 key 取就行,不用费力解析 HTML。 **对应到项目里:** 猎聘的 `onResponse()` 拿到 `pc-search-job` 接口返回后,直接读 `ret['data']['data']['jobCardList']`——这就是为什么它"列表页靠接口"比"列表页靠 DOM"稳定得多。 #### **2.3 正则表达式 —— 从乱糟糟的文本里抠特定模式** ```python import re salary = re.search(r"(\d+)k-(\d+)k", text) ``` **能得到什么:** 当数据混在长文本里(比如职位描述里夹着"3-5年经验"),用正则一抓一个准。项目里"届次推断"、"URL 归一化"都用正则。 --- ## **三、浏览器操作积木:让 Playwright 像人一样动** 当网站用 JavaScript 动态渲染、需要登录、需要点击展开时,`requests` 就不够用了——你必须真的"开一个浏览器"。 ### **3.1 启动浏览器并打开页面** ```python from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) # False 表示能看到浏览器窗口 page = browser.new_page() page.goto("https://www.liepin.com/zhaopin/") page.wait_for_load_state("networkidle") # 等所有网络请求结束 html = page.content() # 拿到完整渲染后的 HTML ``` **能得到什么:** 拿到的是 **JS 执行完毕后**的最终 HTML,而不是原始的"骨架"HTML。这是抓动态网站的关键。 ### **3.2 模拟用户操作** ```text page.click("button.search-btn") # 点击按钮 page.fill("input.keyword", "Python 工程师") # 输入文字 page.select_option("select.city", "北京") # 下拉选择 page.mouse.wheel(0, 3000) # 滚动 3000 像素 ``` **能得到什么:** 触发筛选、翻页、加载更多——就像真人在用网站。 **对应到项目里:** 猎聘 `__selectItem()` 通过点击控件选公司性质、规模、经验,鱼泡爬虫还要"模拟滑块拖动"过验证——本质都是这几个 API 的组合。 ### **3.3 反自动化伪装** ```text page.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})") ``` **能得到什么:** 隐藏掉"我是个自动化工具"的标志,让网站不容易识破。 ### **3.4 拦截资源,加速 + 省流量** ```python def block_resource(route): if route.request.resource_type in ("image", "font", "media"): route.abort() else: route.continue_() context.route("**/*", block_resource) ``` **能得到什么:** 不下载图片和字体,页面打开速度翻倍,代理流量也省一大半。猎聘和鱼泡爬虫全都用了这招。 --- ## **四、监听积木:**`page.on("response")` **—— 偷听浏览器和服务器的对话** 这是一个非常巧妙的技术,也是前面提到的"第四种采集通道"的核心。 ```python def on_response(response): if "pc-search-job" in response.url: data = response.json() print("拦截到职位列表数据:", data) page.on("response", on_response) page.goto("https://www.liepin.com/zhaopin/") ``` **能得到什么:** 我们让浏览器**正常**打开页面、正常和服务器对话,但我们在旁边"偷听",把浏览器收到的接口数据**截下来**自己用。 **为什么这招好用?** - 不用自己研究接口签名(很多接口有复杂的 token、加密参数,自己构造很麻烦) - 不用解析复杂的 HTML - 拿到的就是干净的 JSON **对应到项目里:** 猎聘所有列表数据都是这么拿的——这就是"页面操作驱动 + 接口监听获取"混合方案的精髓。 --- ## **五、存储积木:把抓下来的东西存好** 工程化爬虫一定不是"抓完直接入库",而是"先落盘,再入库"。 #### **5.1 写 JSON 文件** ```python import json with open("job_123.json", "w", encoding="utf-8") as f: json.dump(job_data, f, ensure_ascii=False, indent=2) ``` **能得到什么:** 抓到的原始数据先存成文件,**任何一步出错都能从文件重跑**,不用重新抓。 #### **5.2 文件状态机:.ok / .err / .expired** ```python import os os.rename("job_123.json", "success/job_123.json.ok") # 入库成功 os.rename("job_123.json", "failed/job_123.json.err") # 入库失败 ``` **能得到什么:** 一眼看出每个任务的状态,失败的可以批量重试,成功的可以归档。 **对应到项目里:** 爬虫逻辑分析里反复强调的"先文件、后接口"——`com/`、`job/`、`success/`、`failed/`、`progress/`、`clean/` 这套目录就是一个手工版的"任务状态机"。 #### **5.3 进度文件 / 状态文件:断点续跑的灵魂** ```properties # 每抓完一批就写一次 with open("progress.txt", "w") as f: f.write(str(last_id)) # 启动时读 last_id = int(open("progress.txt").read()) if os.path.exists("progress.txt") else 0 ``` **能得到什么:** 程序中断了再启动,从上次的位置继续,不用从头来一遍。 **对应到项目里:** 猎聘公司版的 `spider_status_.json` 就是这个的豪华版——它不仅记录"抓到哪了",还记录"哪些条件组合已经抓完了",甚至做了**临时文件 + 原子重命名 + 备份恢复**这种数据库级别的可靠性保证。 --- ### **六、节奏控制积木:别让网站发现你是机器** #### **6.1 随机休眠** ```python import random, time time.sleep(random.uniform(1.5, 4.0)) ``` **能得到什么:** 请求之间有随机间隔,看起来更像人类。猎聘、鱼泡都有 `randomSleep()`。 #### **6.2 去重判断** ```text if job_id in already_spider_today: continue # 今天抓过,跳过 ``` **能得到什么:** 不重复抓,省时间省代理额度。项目里有四层去重:**今日是否抓过 → 公司维度是否抓过 → 数据库是否已存在 → 是否在黑名单**。 --- ### **七、异常处理积木:网络是不可靠的** #### **7.1 try / except + 重试** ```text for i in range(3): try: resp = requests.get(url, timeout=10) break except Exception as e: print(f"第 {i+1} 次失败: {e}") time.sleep(2 ** i) # 指数退避:1秒、2秒、4秒 ``` **能得到什么:** 网络抖动、临时被封、超时——都不会让程序崩掉,而是"等等再来"。 **对应到项目里:** `spider_yupao_clean.py` 用的就是"指数退避 + 多选择器兼容"组合,这是工程化爬虫的标配。 #### **7.2 风控识别** ```python if page.title() == "猎聘": # 正常页应该是"职位搜索 - 猎聘" print("可能被拦截") change_proxy() ``` **能得到什么:** 主动识别"我被发现了",立刻换代理或休眠,避免被永久封禁。 --- ### **把这些积木拼起来,就是一个最小爬虫** 我们可以现在就把上面所有积木串成一个 30 行的小例子,看看一个"麻雀虽小五脏俱全"的爬虫长什么样: ```python import requests, json, time, random, os from bs4 import BeautifulSoup headers = {"User-Agent": "Mozilla/5.0 ..."} visited = set() def fetch_list(page_no): url = f"https://example.com/jobs?page={page_no}" resp = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "lxml") return [(a.get_text(), a["href"]) for a in soup.select("a.job-link")] def fetch_detail(url): resp = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "lxml") return { "title": soup.select_one("h1").get_text(strip=True), "desc": soup.select_one(".job-desc").get_text(strip=True), } def save(job_id, data): os.makedirs("jobs", exist_ok=True) with open(f"jobs/{job_id}.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) for page_no in range(1, 6): for title, link in fetch_list(page_no): job_id = link.split("/")[-1] if job_id in visited: continue try: detail = fetch_detail(link) save(job_id, {"title": title, "link": link, **detail}) visited.add(job_id) except Exception as e: print(f"出错: {e}") time.sleep(random.uniform(1, 3)) ``` 这就是一个**完整的、能跑的、有去重有异常处理的**最小爬虫。它包含了:发请求 → 解析 → 翻页 → 详情页 → 落盘 → 去重 → 节奏控制 → 异常处理 这 8 块积木。 真实项目(比如猎聘那 1000 多行的 `spider_liepin_company.py`)做的事情其实**没有本质上的不同**,只是把每块积木**做强、做大、做工程化**了: - "发请求"被升级成了 Playwright + 代理池 + 反自动化 - "解析"分成了"接口监听拿列表 + DOM 拿详情" - "去重"变成了四层去重 - "落盘"变成了完整的状态机目录 - "进度记录"变成了带备份恢复的状态文件系统 - "异常处理"变成了风控识别 + 自动换代理 + 跨日恢复 --- **组内导航**:⬅️ [[02-四重采集通道|四重采集通道]] | 🏠 [[00-爬虫课程六讲|00-爬虫课程六讲]] | ➡️ [[04-用积木拼一个最小职位爬虫|用积木拼一个最小职位爬虫]]