知乎十年,沉淀了全网最优质的中文问答。与其在信息流中迷失,不如用代码把这些精华装进自己的数据库。
一、为什么要爬取知乎高赞回答?
知乎上有一个经典问题:"你见过最不求上进的人是什么样子?"下面有个高赞回答:"他们为现状焦虑,又没有毅力践行决心去改变自己,以最普通的身份埋没在人群中,却过着最煎熬的日子。"
这条回答获得了10万+点赞,评论区无数人表示"是我本人没错了"。
这就是知乎高赞回答的魅力------它们往往击中人心,蕴含着深厚的专业知识、独特的经验见解或深刻的人生感悟。把这些高赞回答系统化地收集起来,就是一座取之不尽的知识宝库。
1.1 高赞回答的价值
| 应用场景 | 价值体现 | 实例 |
|---|---|---|
| 个人学习 | 获取高质量知识,避免信息噪音 | 编程、职场、心理学等领域精华 |
| 内容创作 | 素材库、选题参考 | 写文章、做视频的灵感来源 |
| 行业研究 | 洞察用户痛点和需求 | 产品经理分析用户评论 |
| 数据分析 | 情感分析、话题演化 | 舆情监控、社会研究 |
1.2 知乎爬虫的难点
知乎的反爬虫机制,在国内知识平台中属于第一梯队:
| 难点 | 表现 | 应对策略 |
|---|---|---|
| 强制登录 | 未登录状态只能看到截断的回答 | 使用Cookie维持登录态 |
| 动态加载 | 回答通过AJAX异步加载 | 分析XHR请求或使用Selenium |
| 浏览器指纹 | 检测Canvas/WebGL等特征 | 深度伪装+行为模拟 |
| 频率限制 | 高频请求直接封IP | 随机延迟+代理池 |
本文将带你绕过层层反爬,稳定抓取知乎高赞回答,并构建结构化的知识库。
二、技术选型与准备工作
2.1 核心依赖库
bash
# 基础爬虫
pip install requests beautifulsoup4 lxml
# 动态渲染(备选)
pip install selenium webdriver-manager
# 数据处理
pip install pandas numpy
# 数据库
pip install sqlite3 # Python内置
2.2 获取Cookie(最关键的一步)
知乎大部分高质量回答(包括完整内容和真实点赞数)都需要登录状态才能获取。未登录状态下,长回答会被截断,点赞数可能显示为"0"或"100+"(实际可能是5000+)。
获取步骤:
- 用Chrome浏览器登录知乎账号
- 按F12打开开发者工具 → 切换到 Application(应用)标签
- 左侧找到 Cookies → https://www.zhihu.com
- 找到名为
z_c0的Cookie,复制其值
python
# Cookie格式示例
COOKIE = 'z_c0="2|1:0|10:1648123456|4:z_c0|92:xxxxxxxxxxxxx"; '
** 注意**:Cookie有效期通常几小时到几天,过期需要重新获取。
三、核心爬虫实现(Requests + Session)
3.1 分析页面结构
以知乎问题页面为例(https://www.zhihu.com/question/123456789),回答内容通过异步加载:
- 初始HTML只包含问题标题和部分元数据
- 回答数据通过XHR请求获取:
https://www.zhihu.com/api/v4/questions/{question_id}/answers - 返回JSON格式,包含回答列表、作者信息、点赞数、评论数等
3.2 完整爬虫代码
python
# zhihu_spider.py
import requests
import json
import time
import random
import sqlite3
from datetime import datetime
import pandas as pd
class ZhihuAnswerSpider:
"""知乎高赞回答爬虫"""
def __init__(self, cookie):
self.session = requests.Session()
self.session.headers.update({
'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',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Referer': 'https://www.zhihu.com/',
'Cookie': cookie,
'Authorization': 'oauth 2.0', # 知乎API需要的认证头
})
# 回答数据缓存
self.answers = []
def get_question_answers(self, question_id, limit=20, offset=0):
"""
获取问题下的回答列表(API方式)
:param question_id: 问题ID
:param limit: 每页数量
:param offset: 偏移量
"""
url = f"https://www.zhihu.com/api/v4/questions/{question_id}/answers"
params = {
'include': 'data[*].content,data[*].voteup_count,data[*].comment_count,data[*].author.name,data[*].author.headline',
'limit': limit,
'offset': offset,
'sort_by': 'default'
}
try:
# 随机延迟
time.sleep(random.uniform(2, 4))
response = self.session.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
return data
except requests.exceptions.RequestException as e:
print(f"API请求失败: {e}")
return None
def parse_answers(self, api_response):
"""
解析API返回的回答数据
"""
if not api_response or 'data' not in api_response:
return []
answers = []
for item in api_response['data']:
try:
answer = {
'answer_id': item.get('id'),
'question_id': item.get('question', {}).get('id'),
'question_title': item.get('question', {}).get('title'),
'author_name': item.get('author', {}).get('name'),
'author_headline': item.get('author', {}).get('headline', ''),
'content': item.get('content', ''), # HTML格式的富文本
'voteup_count': item.get('voteup_count', 0),
'comment_count': item.get('comment_count', 0),
'created_time': datetime.fromtimestamp(item.get('created_time', 0)).strftime('%Y-%m-%d %H:%M:%S'),
'updated_time': datetime.fromtimestamp(item.get('updated_time', 0)).strftime('%Y-%m-%d %H:%M:%S'),
'crawl_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
answers.append(answer)
except Exception as e:
print(f"解析单条回答出错: {e}")
continue
return answers
def fetch_all_answers(self, question_id, max_answers=100):
"""
获取某个问题的所有回答(支持翻页)
"""
print(f"开始爬取问题 {question_id} 的回答...")
all_answers = []
offset = 0
limit = 20
while offset < max_answers:
print(f" 正在抓取 offset={offset}...")
data = self.get_question_answers(question_id, limit=limit, offset=offset)
if not data:
break
answers = self.parse_answers(data)
all_answers.extend(answers)
# 检查是否还有更多数据
paging = data.get('paging', {})
is_end = paging.get('is_end', True)
if is_end:
print(" 已到达最后一页")
break
offset += limit
time.sleep(random.uniform(3, 6)) # 翻页间隔
print(f"完成!共获取 {len(all_answers)} 条回答")
self.answers = all_answers
return all_answers
def filter_by_vote(self, min_votes=1000):
"""
筛选高赞回答
"""
high_vote_answers = [a for a in self.answers if a['voteup_count'] >= min_votes]
print(f"筛选出 {len(high_vote_answers)} 条点赞数 ≥ {min_votes} 的回答")
return high_vote_answers
def save_to_sqlite(self, db_path='zhihu.db'):
"""
保存到SQLite数据库
"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 创建表
cursor.execute('''
CREATE TABLE IF NOT EXISTS answers (
answer_id INTEGER PRIMARY KEY,
question_id INTEGER,
question_title TEXT,
author_name TEXT,
author_headline TEXT,
content TEXT,
voteup_count INTEGER,
comment_count INTEGER,
created_time TEXT,
updated_time TEXT,
crawl_time TEXT
)
''')
# 插入数据
for answer in self.answers:
cursor.execute('''
INSERT OR REPLACE INTO answers
(answer_id, question_id, question_title, author_name, author_headline,
content, voteup_count, comment_count, created_time, updated_time, crawl_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
answer['answer_id'], answer['question_id'], answer['question_title'],
answer['author_name'], answer['author_headline'], answer['content'],
answer['voteup_count'], answer['comment_count'], answer['created_time'],
answer['updated_time'], answer['crawl_time']
))
conn.commit()
conn.close()
print(f"数据已保存到 {db_path}")
def save_to_csv(self, filename=None):
"""
保存到CSV文件
"""
if not filename:
filename = f"zhihu_answers_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
df = pd.DataFrame(self.answers)
df.to_csv(filename, index=False, encoding='utf-8-sig')
print(f"数据已保存到 {filename}")
return df
# 使用示例
if __name__ == '__main__':
# 替换成你自己的Cookie
MY_COOKIE = 'z_c0="2|1:0|10:1648123456|4:z_c0|92:xxxxxxxxxxxxx";'
spider = ZhihuAnswerSpider(cookie=MY_COOKIE)
# 爬取某个问题的回答(例如:Python 有哪些好的学习资料?)
question_id = "29116517" # 问题ID可从URL获取
spider.fetch_all_answers(question_id, max_answers=100)
# 筛选高赞回答
high_vote = spider.filter_by_vote(min_votes=1000)
# 保存数据
spider.save_to_sqlite()
spider.save_to_csv()
四、进阶:使用Selenium应对复杂反爬
如果API方式频繁失效,可以采用Selenium模拟真实浏览器。
4.1 Selenium配置
python
# selenium_spider.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import random
def create_stealth_driver():
"""创建深度伪装的Chrome浏览器"""
chrome_options = Options()
# 1. 禁用自动化特征
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option('useAutomationExtension', False)
chrome_options.add_argument('--disable-blink-features=AutomationControlled')
# 2. 随机User-Agent
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.36'
]
chrome_options.add_argument(f'user-agent={random.choice(user_agents)}')
# 3. 窗口大小
chrome_options.add_argument('--window-size=1920,1080')
# 4. 禁用图片加载(提升速度)
prefs = {"profile.managed_default_content_settings.images": 2}
chrome_options.add_experimental_option("prefs", prefs)
driver = webdriver.Chrome(options=chrome_options)
# 5. 注入JS修改webdriver属性
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5]
});
Object.defineProperty(navigator, 'languages', {
get: () => ['zh-CN', 'zh', 'en']
});
"""
})
return driver
def fetch_with_selenium(question_url):
"""使用Selenium获取回答内容"""
driver = create_stealth_driver()
try:
# 访问页面
driver.get(question_url)
# 等待回答加载
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".AnswerItem")))
# 模拟人类滚动,触发懒加载
for _ in range(5):
driver.execute_script("window.scrollTo(0, document.body.scrollHeight * 0.8);")
time.sleep(random.uniform(1, 3))
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(random.uniform(2, 4))
# 获取回答元素
answer_items = driver.find_elements(By.CSS_SELECTOR, ".AnswerItem")
results = []
for item in answer_items:
try:
# 提取点赞数
vote = item.find_element(By.CSS_SELECTOR, ".VoteButton").text
# 提取作者
author = item.find_element(By.CSS_SELECTOR, ".AuthorInfo-name").text
# 提取内容
content = item.find_element(By.CSS_SELECTOR, ".RichText").text
results.append({
'author': author,
'vote': vote,
'content': content[:200] + "..."
})
except:
continue
return results
finally:
driver.quit()
五、构建知识库系统
爬下来的数据只有经过结构化存储和索引,才能真正发挥价值。
5.1 数据库设计
sql
-- 问题表
CREATE TABLE questions (
question_id INTEGER PRIMARY KEY,
title TEXT,
description TEXT,
follower_count INTEGER,
view_count INTEGER,
answer_count INTEGER,
crawl_time TEXT
);
-- 回答表(已在前面的代码中创建)
CREATE TABLE answers (
answer_id INTEGER PRIMARY KEY,
question_id INTEGER,
question_title TEXT,
author_name TEXT,
author_headline TEXT,
content TEXT,
voteup_count INTEGER,
comment_count INTEGER,
created_time TEXT,
updated_time TEXT,
crawl_time TEXT,
FOREIGN KEY (question_id) REFERENCES questions(question_id)
);
-- 用户表(可选)
CREATE TABLE authors (
author_name TEXT PRIMARY KEY,
headline TEXT,
follower_count INTEGER,
answer_count INTEGER,
crawl_time TEXT
);
5.2 知识库查询工具
python
# knowledge_base.py
import sqlite3
import pandas as pd
class KnowledgeBase:
"""知识库查询工具"""
def __init__(self, db_path='zhihu.db'):
self.conn = sqlite3.connect(db_path)
def search_by_keyword(self, keyword):
"""关键词搜索回答内容"""
query = """
SELECT question_title, author_name, content, voteup_count
FROM answers
WHERE content LIKE ?
ORDER BY voteup_count DESC
LIMIT 20
"""
df = pd.read_sql_query(query, self.conn, params=[f'%{keyword}%'])
return df
def get_top_answers_by_question(self, question_id, limit=10):
"""获取某个问题下的高赞回答"""
query = """
SELECT author_name, content, voteup_count, comment_count
FROM answers
WHERE question_id = ?
ORDER BY voteup_count DESC
LIMIT ?
"""
df = pd.read_sql_query(query, self.conn, params=[question_id, limit])
return df
def get_author_stats(self, author_name):
"""获取作者的统计信息"""
query = """
SELECT
COUNT(*) as answer_count,
AVG(voteup_count) as avg_votes,
MAX(voteup_count) as max_votes,
SUM(voteup_count) as total_votes
FROM answers
WHERE author_name = ?
"""
df = pd.read_sql_query(query, self.conn, params=[author_name])
return df
def export_to_markdown(self, answer_id, output_file=None):
"""将回答导出为Markdown格式"""
query = "SELECT * FROM answers WHERE answer_id = ?"
df = pd.read_sql_query(query, self.conn, params=[answer_id])
if df.empty:
print("未找到该回答")
return
row = df.iloc[0]
md = f"""# {row['question_title']}
> 作者:{row['author_name']}
> 点赞:{row['voteup_count']} | 评论:{row['comment_count']}
> 创建时间:{row['created_time']}
---
{row['content']}
---
*本文档由知乎知识库自动生成*
"""
if output_file:
with open(output_file, 'w', encoding='utf-8') as f:
f.write(md)
print(f"已导出到 {output_file}")
else:
print(md)
def close(self):
self.conn.close()
六、数据分析与洞察
有了数据,我们可以做一些有趣的分析。
6.1 高赞回答的特征分析
python
# analysis.py
import pandas as pd
import matplotlib.pyplot as plt
import jieba
from wordcloud import WordCloud
plt.rcParams['font.sans-serif'] = ['SimHei']
def analyze_answers(csv_file):
"""分析高赞回答的特征"""
df = pd.read_csv(csv_file)
print("=== 基础统计 ===")
print(f"总回答数: {len(df)}")
print(f"平均点赞: {df['voteup_count'].mean():.0f}")
print(f"最高点赞: {df['voteup_count'].max()}")
print(f"平均评论: {df['comment_count'].mean():.0f}")
# 作者活跃度分布
plt.figure(figsize=(10, 6))
author_counts = df['author_name'].value_counts().head(20)
author_counts.plot(kind='bar')
plt.title('高产作者TOP20')
plt.xlabel('作者')
plt.ylabel('回答数量')
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig('author_distribution.png')
plt.show()
# 点赞数分布
plt.figure(figsize=(10, 6))
plt.hist(df['voteup_count'], bins=50, edgecolor='black')
plt.title('点赞数分布')
plt.xlabel('点赞数')
plt.ylabel('回答数量')
plt.tight_layout()
plt.savefig('vote_distribution.png')
plt.show()
# 生成词云
all_text = ' '.join(df['content'].astype(str).tolist())
words = jieba.lcut(all_text)
# 停用词
stop_words = {'的', '了', '是', '在', '和', '与', '这', '那', '也', '都'}
filtered = [w for w in words if len(w) > 1 and w not in stop_words]
word_freq = {}
for w in filtered:
word_freq[w] = word_freq.get(w, 0) + 1
wc = WordCloud(
font_path='simhei.ttf',
width=1000,
height=600,
background_color='white',
max_words=100
).generate_from_frequencies(word_freq)
plt.figure(figsize=(12, 8))
plt.imshow(wc, interpolation='bilinear')
plt.axis('off')
plt.title('高赞回答词云图')
plt.savefig('wordcloud.png', dpi=300)
plt.show()
七、反爬避坑指南
7.1 常见问题及解决方案
| 问题 | 现象 | 解决方案 |
|---|---|---|
| Cookie过期 | API返回401或空数据 | 重新登录获取Cookie |
| 请求频率过高 | 突然返回403 | 增加随机延迟(3-8秒),使用代理池 |
| 浏览器指纹检测 | Selenium被识别 | 注入JS修改webdriver属性 |
| 验证码弹窗 | 出现滑块验证 | 降低频率,或使用打码平台 |
7.2 代理IP配置
python
# 使用代理
proxies = {
'http': 'http://username:password@proxy.example.com:8080',
'https': 'http://username:password@proxy.example.com:8080'
}
response = requests.get(url, proxies=proxies, timeout=10)
7.3 深度伪装方案
如果常规反爬措施失效,可以参考最新的"指纹+行为"双检测伪装方案:
- 修改Canvas/WebGL指纹
- 模拟真实的鼠标轨迹
- 随机化操作间隔
- 匹配IP地域与时区
八、法律与道德规范
在享受技术便利的同时,请务必遵守以下准则:
- 遵守Robots协议:知乎的robots.txt对爬虫有限制
- 控制访问频率:设置合理延迟,不给服务器造成压力
- 仅限个人使用:爬取的数据不得用于商业用途
- 尊重用户隐私:不爬取用户个人信息、私信等
- 合法合规:遵守《个人信息保护法》《数据安全法》
九、总结:从爬虫到知识库的完整路径
通过本文,我们完成了:
| 阶段 | 成果 | 技术要点 |
|---|---|---|
| 数据获取 | 知乎高赞回答采集 | Cookie登录、API调用、反爬应对 |
| 数据存储 | SQLite知识库 | 表结构设计、批量插入 |
| 数据查询 | 关键词搜索、作者统计 | SQL查询优化 |
| 数据分析 | 特征洞察、词云图 | 数据可视化、文本挖掘 |
扩展方向:
- 增量更新:定时爬取,监控新回答
- 知识图谱:构建回答之间的引用关系
- 推荐系统:基于用户兴趣推荐高赞回答
- Web可视化:Flask+ECharts搭建知识库前端
最后的话:知乎是一座知识的富矿,但真正的价值不在于数据的多寡,而在于你如何组织和利用这些数据。当你亲手把散落在各个问题下的高赞回答汇聚成自己的知识库时,你收获的不仅是技术能力的提升,更是一份可以随时查阅的智慧结晶。