简介:这份资源面向具备Python基础、希望入门网络数据采集的开发者与数据分析学习者,围绕知乎问答社区场景,讲解如何抓取回答中的文本与图片。内容涉及HTTP请求发送、HTML解析、文件读写与延时策略等环节,帮助读者理解从问题ID定位页面到提取回答正文、图片链接及作者信息的完整思路。压缩包共3个文件,包含2个py脚本与1个txt依赖清单,分别承担数据抓取、结果保存和运行环境说明的职责,整体约4KB,结构精简,便于快速阅读与二次修改。目前已有1144人学习下载,适合作为爬虫练手项目参考。读者可从中获得请求构造、解析提取、图片下载与本地存储的代码范例,并了解通过time.sleep控制请求频率、设置计数器限制下载数量等防封禁思路,为后续数据分析和信息挖掘打下实践基础。
1. 从一份 Python 爬虫源码说起:知乎回答文本与图片怎么落地
知乎回答页面的内容结构,是很多做数据采集的开发者绕不开的一道坎。正文里混着富文本、代码块、外链图片,图片还分「点开大图」和「缩略图」两套地址,直接拿 requests 抓 HTML 往往只能捞到一堆懒加载占位符。这份 Python 爬虫源码包,针对的就是「把某个问题下所有回答的文本和图片完整扒下来」这个具体诉求,输出结构化的文本文件和本地图片目录。它适合做舆情分析、内容归档、数据集构建的从业者,也适合刚学完 requests 和 BeautifulSoup、想找一个真实站点练手的开发者。源码里对请求头、分页、图片下载做了封装,但知乎的反爬策略这几年一直在变,直接跑之前得先搞清楚它的边界在哪。
2. 拆解爬取链路:从请求构造到图片落盘
2.1 为什么不能只靠 requests + BeautifulSoup
知乎回答列表页在未登录状态下只返回前几条回答,往下滚动触发的是 XHR 接口,返回的是 JSON 结构,而不是完整 HTML。这意味着纯静态解析的路子走不通。源码里采用的是「先拿页面里的初始数据,再走接口翻页」的混合策略。初始页面里嵌了一个js-initialData的 script 标签,里面是 JSON 格式的初始状态,包含前几个回答的完整内容;后续回答则通过api/v4/questions/{id}/answers这个接口按limit和offset分页拉取。
这个接口返回的 JSON 里,data数组每一项的content字段是 HTML 字符串,excerpt是纯文本摘要。图片信息藏在content的<img>标签里,>import requests import json import re import os from urllib.parse import urlparse # 基础请求头,Cookie 需要替换成自己浏览器里抓到的有效值 HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36", "Referer": "https://www.zhihu.com/", "Cookie": "你的_cookie_字符串", # 关键字段,缺失会导致分页受限 } def fetch_answers(question_id, offset=0, limit=20): """按偏移量拉取回答列表,返回 JSON 中的 data 数组""" api = f"https://www.zhihu.com/api/v4/questions/{question_id}/answers" params = { "include": "data[*].content,excerpt,author,created_time", "limit": limit, "offset": offset, "sort_by": "default", } resp = requests.get(api, headers=HEADERS, params=params, timeout=15) if resp.status_code != 200: print(f"请求失败,状态码:{resp.status_code}") return [] return resp.json().get("data", [])
这段代码里include参数决定了返回字段的丰富程度,不传的话content可能不返回。limit单次最大一般不超过 20,设太大容易被截断。offset按 20 递增,直到返回的data为空或paging.is_end为 true 为止。实际跑的时候建议在每次请求之间加 1 到 2 秒延时,源码里用的是time.sleep(random.uniform(1, 2)),这个习惯能省掉很多封禁麻烦。
2.3 文本清洗与图片地址提取
拿到content字段后,它是一段 HTML。文本提取不能直接上正则去标签,因为代码块里的<>会被误伤。源码里用的是 BeautifulSoup 的get_text()配合分隔符,把块级元素之间补上换行。
from bs4 import BeautifulSoup def extract_text_and_images(html_content): """从回答 HTML 中提取纯文本和原图地址列表""" soup = BeautifulSoup(html_content, "html.parser") # 把 <br> 和 <p> 替换成换行,避免文本粘连 for tag in soup.find_all(["br", "p", "div"]): tag.append("\n") text = soup.get_text(separator="\n", strip=True) image_urls = [] for img in soup.find_all("img"): # 优先取>import hashlib import os def download_images(image_urls, save_dir): """下载图片到指定目录,返回成功数量""" os.makedirs(save_dir, exist_ok=True) success = 0 for url in image_urls: try: resp = requests.get(url, headers=HEADERS, timeout=20, stream=True) if resp.status_code == 200: # 用 URL 的 MD5 做文件名,避免重名 ext = os.path.splitext(urlparse(url).path)[1] or ".jpg" fname = hashlib.md5(url.encode()).hexdigest() + ext fpath = os.path.join(save_dir, fname) with open(fpath, "wb") as f: for chunk in resp.iter_content(1024): f.write(chunk) success += 1 except Exception as e: print(f"下载失败 {url}:{e}") return successstream=True配合iter_content是为了避免大图一次性读进内存。超时设 20 秒,比文本请求宽松一些,因为图片服务器响应偶尔会慢。如果某张图反复失败,源码里不会中断整个流程,只是记录日志继续下一张,这个设计在批量任务里很实用。
3. 避坑与排查:那些跑起来才会遇到的问题
3.1 现象:接口返回 403 或空数据
原因通常是 Cookie 过期或请求头里缺少Referer。知乎对接口的校验不只看 Cookie,还会检查来源页。解决方法是重新从浏览器开发者工具里复制最新 Cookie,并确保Referer指向https://www.zhihu.com/。如果换了 IP 或短时间内请求过多,也可能触发风控,这时候需要降低频率,把延时拉到 3 秒以上。
3.2 现象:图片下载下来是 1KB 左右的空白图
这是典型的懒加载占位图。原因在于提取时取了src而不是>import yaml import os def load_config(path="config.yaml"): with open(path, "r", encoding="utf-8") as f: cfg = yaml.safe_load(f) # 环境变量优先级高于配置文件 cfg["cookie"] = os.getenv("ZHIHU_COOKIE", cfg.get("cookie", "")) return cfg
4.2 断点续爬与日志记录
批量任务最怕跑到一半中断,重新跑又得从头来。源码里用了一个简单的状态文件记录已完成的 offset 和已下载的图片 URL 集合。每次启动先读状态,跳过已完成部分。日志用logging模块输出到文件,格式包含时间戳、级别和消息,方便事后排查哪一页出了问题。
| 参数 | 建议值 | 说明 |
|---|---|---|
| 请求间隔 | 1.5~3 秒 | 太短触发风控,太长效率低 |
| 单次 limit | 20 | 超过 20 容易被截断 |
| 图片超时 | 20 秒 | 大图下载留足时间 |
| 重试次数 | 2 次 | 失败后间隔 3 秒重试 |
| 状态保存频率 | 每页一次 | 避免频繁写盘 |
4.3 验证抓取完整性的方法
跑完之后怎么确认没漏?可以对比接口返回的paging.totals字段和实际保存的回答数量。如果差距超过 5%,说明中间有失败页,需要检查日志里的 403 或超时记录。图片方面,统计每个回答目录下的文件数,和提取阶段的 URL 数量做对比,差值就是下载失败的图片。我一般会在脚本最后打印一份汇总:总回答数、成功文本数、图片总数、下载成功数、失败列表。这份汇总比任何日志都直观。
从那以后我每次写采集脚本,都强制先跑一个「只抓一页、只下三张图」的冒烟测试,确认链路通了再放开批量。这个习惯帮我省下了不少因为 Cookie 过期或接口变更导致的整夜空跑。希望这份源码的拆解能帮到你,拿到手之后先改配置、再跑冒烟、最后开批量,顺序别乱。
本文还有配套的精品资源,点击获取