资讯动态

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

发布时间:2026/10/23 13:02:47 来源:尧图企业网站定制
简介这份资源面向具备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 fhttps://www.zhihu.com/api/v4/questions/{QID}/answers HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: fhttps://www.zhihu.com/question/{QID}, Accept: application/json, text/plain, */*, } def fetch_page(offset0, limit20): 拉取一页回答返回 JSON 字典 params { include: data[*].content,excerpt,author.name,created_time,voteup_count, limit: limit, offset: offset, } resp requests.get(API, headersHEADERS, paramsparams, timeout10) # 状态码非 200 直接抛错方便定位 resp.raise_for_status() return resp.json() def iter_answers(max_page5): 按页迭代回答最多抓 max_page 页 for page in range(max_page): data fetch_page(offsetpage * 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_rootzhihu_images): 把一组图片下载到 save_root/answer_id/ 下 folder os.path.join(save_root, str(answer_id)) os.makedirs(folder, exist_okTrue) for idx, url in enumerate(img_urls): try: r requests.get(url, headersHEADERS, timeout15) 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_okTrue)保证重复跑不会报目录已存在。url.split(?)[0]去掉查询参数再取扩展名因为知乎图片 URL 后面常带?sourcexxx之类。每张图单独 try一张失败不影响后续。下载间隔如果被限流可以在循环里加time.sleep(0.5)。3. 翻页、去重与存储让抓取结果可复用3.1 翻页边界与停止条件API 返回的 JSON 里通常有paging字段包含is_end和next。用is_end判断是否还有下一页比固定页数更稳。下面改造iter_answers让它自动停。def iter_answers_auto(max_page50): 自动翻页遇到 is_end 或达到 max_page 停止 offset 0 for _ in range(max_page): data fetch_page(offsetoffset) 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 ansans[id]是回答的唯一标识比用作者名或时间戳去重更可靠。seen集合放在模块级整个抓取周期内有效。3.3 文本落盘按回答存 txt 还是汇总成 csv如果后续要做文本分析汇总成 CSV 更方便如果只是存档按回答存 txt 更直观。下面给一个 CSV 方案字段包括回答 ID、作者、赞同数、正文、图片数量。import csv def save_to_csv(answers, pathanswers.csv): with open(path, w, newline, encodingutf-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), ])encodingutf-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(pathzhihu.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(rootzhihu_images, outimage_index.csv): with open(out, w, newline, encodingutf-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。从那以后我每次写爬虫都先把间隔和重试逻辑写好再跑第一行请求省得后面返工。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑