news 2026/10/23 13:02:32

Python爬取知乎回答文本与图片:动态页面解析与本地语料库构建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬取知乎回答文本与图片:动态页面解析与本地语料库构建

简介:这份资源面向具备Python基础、希望入门网络数据采集的开发者与数据分析学习者,围绕知乎问答社区这一典型场景,讲解如何抓取回答中的正文文本与配图。内容覆盖HTTP请求发送、HTML解析、文本与图片链接提取、二进制图片保存、用户信息抓取,以及time.sleep()延时防封与下载计数控制等实用策略,帮助读者建立从请求到落盘的完整爬虫思路。压缩包共3个文件,以2个py脚本和1个txt依赖清单为主,脚本分别承担数据获取与保存职责,txt用于声明requests、BeautifulSoup、lxml等依赖版本,整体约4KB,结构精简、便于快速阅读与二次修改。目前已有1144人学习下载,适合作为爬虫练手项目,理解请求、解析、存储与限速的配合方式,并注意遵守robots.txt协议与知识产权规范。

1. 从零拆解知乎回答爬取:文本与图片双通道落地的实战思路

很多人第一次接触爬虫,都是从“把某个页面上的内容搬下来”开始的。知乎回答页恰好是一个典型样本:正文是动态渲染的富文本,图片藏在懒加载属性里,分页靠滚动触发,直接拿 requests 怼上去大概率只能拿到一具空壳。这份资源围绕“Python 爬取知乎回答中的文本及图片”展开,核心解决三件事:把回答正文完整提取成可读文本、把配图按回答维度归档到本地、把翻页逻辑跑通不至于只抓第一屏。适合已经会写基础 requests 和 BeautifulSoup、但一遇到动态页面就卡住的开发者,也适合想拿真实站点练手数据清洗的在校同学。下面按“先跑通再优化”的顺序拆。

2. 请求层与解析层:把回答正文和图片地址同时拿到手

2.1 为什么直接 requests.get 拿不到完整回答

知乎回答页的正文并不是一次性塞在初始 HTML 里的。服务端返回的文档里,回答内容往往以 JSON 片段形式嵌在 script 标签中,或者由前端脚本异步填充。你如果只做一次requests.get(url).text,再用 BeautifulSoup 找div.RichContent,结果经常是空的。常见做法是两条路:一是直接请求知乎的 API 接口,返回结构化 JSON;二是用浏览器自动化工具渲染后再取 DOM。前者轻量但需要处理签名参数,后者重但稳定。我一般先用 API 路线,因为文本和图片地址在 JSON 里是分开字段,清洗起来更干净。

2.2 用 API 接口拿回答列表的请求构造

知乎的回答列表接口通常形如https://www.zhihu.com/api/v4/questions/{qid}/answers,带include、limit、offset参数。下面这段代码演示如何构造请求并翻页,注意 headers 里必须带User-Agent和Referer,否则容易被拒。

import requests import time import json # 问题 ID,从回答页 URL 里提取 QID = "12345678" # 接口地址,include 字段决定返回哪些数据 API = f"https://www.zhihu.com/api/v4/questions/{QID}/answers" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": f"https://www.zhihu.com/question/{QID}", "Accept": "application/json, text/plain, */*", } def fetch_page(offset=0, limit=20): """拉取一页回答,返回 JSON 字典""" params = { "include": "data[*].content,excerpt,author.name,created_time,voteup_count", "limit": limit, "offset": offset, } resp = requests.get(API, headers=HEADERS, params=params, timeout=10) # 状态码非 200 直接抛错,方便定位 resp.raise_for_status() return resp.json() def iter_answers(max_page=5): """按页迭代回答,最多抓 max_page 页""" for page in range(max_page): data = fetch_page(offset=page * 20) for item in data.get("data", []): yield item # 翻页间隔,避免请求过密 time.sleep(1.5) if __name__ == "__main__": for ans in iter_answers(): print(ans.get("author", {}).get("name"), ans.get("voteup_count"))

这段代码里,include参数是关键,它告诉接口把content(正文 HTML)、author.name(作者名)、voteup_count(赞同数)一起返回。offset按页递增,limit一般设 20。time.sleep(1.5)是给服务端留缓冲,实际跑的时候如果发现返回 403,先把间隔调到 3 秒以上再试。raise_for_status()能让你在状态码异常时立刻知道,而不是拿着空 JSON 往下走。

2.3 从 content 字段里分离文本和图片地址

接口返回的content是一段 HTML 字符串,里面既有<p>文本,也有<img>标签。图片地址通常写在>from bs4 import BeautifulSoup def parse_content(html): """输入回答的 content HTML,返回 (纯文本, 图片URL列表)""" soup = BeautifulSoup(html, "html.parser") # 提取所有图片,优先取>import os def download_images(img_urls, answer_id, save_root="zhihu_images"): """把一组图片下载到 save_root/answer_id/ 下""" folder = os.path.join(save_root, str(answer_id)) os.makedirs(folder, exist_ok=True) for idx, url in enumerate(img_urls): try: r = requests.get(url, headers=HEADERS, timeout=15) r.raise_for_status() # 从 URL 里取扩展名,取不到就默认 jpg ext = os.path.splitext(url.split("?")[0])[-1] or ".jpg" path = os.path.join(folder, f"{idx}{ext}") with open(path, "wb") as f: f.write(r.content) except Exception as e: print(f"下载失败 {url} -> {e}")

os.makedirs(..., exist_ok=True)保证重复跑不会报目录已存在。url.split("?")[0]去掉查询参数再取扩展名,因为知乎图片 URL 后面常带?source=xxx之类。每张图单独 try,一张失败不影响后续。下载间隔如果被限流,可以在循环里加time.sleep(0.5)。

3. 翻页、去重与存储:让抓取结果可复用

3.1 翻页边界与停止条件

API 返回的 JSON 里通常有paging字段,包含is_end和next。用is_end判断是否还有下一页,比固定页数更稳。下面改造iter_answers,让它自动停。

def iter_answers_auto(max_page=50): """自动翻页,遇到 is_end 或达到 max_page 停止""" offset = 0 for _ in range(max_page): data = fetch_page(offset=offset) paging = data.get("paging", {}) for item in data.get("data", []): yield item if paging.get("is_end"): break offset += 20 time.sleep(1.5)

paging.is_end为 True 时说明没有更多数据。max_page是保险丝,防止接口异常导致死循环。每次 offset 加 20 要和limit保持一致,否则会漏数据或重复。

3.2 用 answer_id 做去重

翻页过程中如果接口不稳定,可能重复返回同一条回答。用集合记录已处理的id,重复的直接跳过。

seen = set() def iter_unique_answers(): for ans in iter_answers_auto(): aid = ans.get("id") if aid in seen: continue seen.add(aid) yield ans

ans["id"]是回答的唯一标识,比用作者名或时间戳去重更可靠。seen集合放在模块级,整个抓取周期内有效。

3.3 文本落盘:按回答存 txt 还是汇总成 csv

如果后续要做文本分析,汇总成 CSV 更方便;如果只是存档,按回答存 txt 更直观。下面给一个 CSV 方案,字段包括回答 ID、作者、赞同数、正文、图片数量。

import csv def save_to_csv(answers, path="answers.csv"): with open(path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["answer_id", "author", "voteup", "text", "img_count"]) for ans in answers: text, imgs = parse_content(ans.get("content", "")) writer.writerow([ ans.get("id"), ans.get("author", {}).get("name"), ans.get("voteup_count"), text, len(imgs), ])

encoding="utf-8-sig"让 Excel 打开不乱码。newline=""避免 Windows 下多空行。正文里的换行会被 csv 模块自动处理成引号包裹,不会破坏结构。

3.4 图片去重与命名冲突

同一张图可能在多个回答里出现,按回答分文件夹已经隔离了大部分冲突。如果同一回答内 URL 重复,可以在下载前用集合过滤。

def download_images_unique(img_urls, answer_id): seen_url = set() unique = [] for u in img_urls: if u not in seen_url: seen_url.add(u) unique.append(u) download_images(unique, answer_id)

这样同一回答内不会出现两张一样的图。跨回答的重复如果也想去掉,可以把seen_url提到全局,但一般没必要,因为不同回答的图分开存更利于溯源。

4. 避坑与排查:请求被拒、图片 403、正文乱码怎么破

4.1 返回 403 或 401

现象:requests.get直接返回 403,或者 API 返回{"error": ...}。原因通常是 headers 里缺User-Agent或Referer,或者请求频率过高被临时限制。解决:补全 headers,把time.sleep间隔调到 3 秒以上,必要时换一个User-Agent字符串再试。如果仍然 403,检查是否漏了Accept头。

4.2 图片下载下来是空白或 1x1 像素

现象:图片文件只有几百字节,打开是空白。原因是你抓的是src里的占位图,不是>import sqlite3 def init_db(path="zhihu.db"): conn = sqlite3.connect(path) conn.execute(""" CREATE TABLE IF NOT EXISTS answers ( answer_id TEXT PRIMARY KEY, question_id TEXT, author TEXT, voteup INTEGER, text TEXT, img_count INTEGER, crawled_at TEXT ) """) conn.commit() return conn def save_to_db(conn, answers, qid): from datetime import datetime for ans in answers: text, imgs = parse_content(ans.get("content", "")) conn.execute( "INSERT OR REPLACE INTO answers VALUES (?,?,?,?,?,?,?)", ( ans.get("id"), qid, ans.get("author", {}).get("name"), ans.get("voteup_count"), text, len(imgs), datetime.now().isoformat(), ), ) conn.commit()

INSERT OR REPLACE保证重复跑不会报主键冲突,同时更新抓取时间。crawled_at用 ISO 格式,方便后续按时间筛选。建完库之后,你可以直接用 SQL 查“赞同数大于 100 且包含某个关键词的回答”,比翻 CSV 快得多。

图片那边,我习惯在下载完成后生成一份index.csv,记录每张图的回答 ID、本地路径、原始 URL 和文件大小。这样即使以后原图链接失效,你也能知道本地文件对应的是哪条回答。生成索引的代码很短:

import os def build_image_index(root="zhihu_images", out="image_index.csv"): with open(out, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["answer_id", "local_path", "size_bytes"]) for answer_id in os.listdir(root): folder = os.path.join(root, answer_id) if not os.path.isdir(folder): continue for fname in os.listdir(folder): fpath = os.path.join(folder, fname) writer.writerow([answer_id, fpath, os.path.getsize(fpath)])

跑完这一步,你手里就有了一份带文本库和图片索引的本地数据集。后续想做词频统计、图片分类或者只是离线翻阅,都不用再碰网络。最后提醒一句:抓取频率一定要克制,time.sleep不要省,遇到 403 先降速而不是换 IP。从那以后我每次写爬虫都先把间隔和重试逻辑写好再跑第一行请求,省得后面返工。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 12:50:54

【水下】声呐同步时钟可靠性评估和节点数量优化的MATLAB仿真

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;擅长数学建模、数据处理、算法改进、程序设计科研仿真。 &#x1f34e; 往期回顾关注个人主页&#xff1a;完整代码获取 定制创新 论文复现私信 &#x1f34a;个人信条&#xff1a;做科研&#xff0c;博学之、审…

作者头像 李华
网站建设 2026/10/23 12:45:51

基于Django与Celery的Web运维平台:SSH批量执行与部署实践

简介&#xff1a;这份资源是一套基于Django框架搭建的运维平台源码&#xff0c;面向具备Python基础、希望了解Web运维系统开发的IT从业者与学习者。平台采用前后端不分离架构&#xff0c;将主机管理、服务监控、日志查看、任务调度、权限控制、资源统计、脚本执行、故障告警、审…

作者头像 李华
网站建设 2026/10/23 12:41:48

Minegrub主题定制:Minecraft风格Grub图标工程化实践

1. 这不是换个图标那么简单&#xff1a;为什么Minegrub World Selection Theme值得专门定制你有没有在启动某发行版时&#xff0c;看到那个熟悉的Grub菜单界面——黑底白字、几行选项、光标闪烁——突然觉得它和你桌面上那套精心搭配的Minecraft像素风主题格格不入&#xff1f;…

作者头像 李华
网站建设 2026/10/23 12:39:48

PiliPlus第三方客户端安装配置全攻略:从安装包获取到播放器内核调优

1. 为什么有人愿意折腾第三方客户端聊第三方客户端之前&#xff0c;得先把一个前提说清楚&#xff1a;官方客户端已经做得足够好了&#xff0c;绝大多数人没有理由换。那为什么还有一小撮人愿意花时间去找、去装、去配置一个第三方版本&#xff1f;答案通常集中在几个很具体的点…

作者头像 李华
网站建设 2026/10/23 12:38:06

MATLAB细菌菌落自动计数:鲁棒预处理与可解释图像分析流水线

简介&#xff1a;本资源是一套基于MATLAB实现的细菌菌落自动计数系统&#xff0c;面向生物医学工程、图像处理初学者及科研实验人员&#xff0c;旨在解决传统平板琼脂培养中人工计数耗时、主观性强、重复性差等痛点。项目完整覆盖图像预处理、Otsu阈值分割、形态学去噪、连通域…

作者头像 李华