ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

2024年知乎首页数据抓取:urllib、requests与浏览器自动化三种方案实战

2024年知乎首页数据抓取:urllib、requests与浏览器自动化三种方案实战 1. 为什么2024年还要聊知乎首页数据抓取知乎首页的信息流一直是做数据分析和内容研究的人绕不开的一块数据源。不管是做热点追踪、选题分析还是训练推荐模型首页推荐流里藏着大量有价值的文本、话题和互动数据。但知乎的反爬机制这两年升级得相当快2024年再用老一套的requests.get直接怼基本活不过三分钟。我自己从2021年开始断断续续做知乎相关的数据采集项目踩过的坑包括但不限于Cookie过期导致全量请求返回登录页、请求频率过高触发验证码、返回的HTML结构跟浏览器看到的完全不一样。这篇文章就把我实际用过的三种方案完整拆开讲——urllib搭配Cookie手动构造请求、requests配合Session维持会话、以及基于浏览器自动化工具的方案。三种方法各有适用场景我会把每种方案的完整代码、参数计算逻辑、以及实际跑下来的成功率数据都摆出来。这篇文章适合谁看如果你已经会Python基础语法知道什么是HTTP请求但一遇到登录态Cookie反爬这些词就头大那这篇就是写给你的。如果你是完全零基础建议先把Python安装和基础语法过一遍再回来不然代码里的dict推导式和异常处理可能会让你卡住。先说结论三种方案里urllibcookie方案最适合轻量级、低频次的首页数据抓取代码量最少依赖最少但需要你手动维护Cookie。requestsSession方案适合需要维持登录态、做多页连续抓取的场景。浏览器自动化方案最重但对抗动态渲染和反爬检测的能力最强。下面逐个拆解。2. 三种方案的整体设计与选型逻辑2.1 核心需求拆解首页数据到底要抓什么在动手写代码之前得先想清楚一件事知乎首页的信息流数据你到底要哪些字段我见过很多人上来就print(response.text)然后对着一堆HTML发呆。正确的做法是先打开浏览器开发者工具切到Network面板刷新首页找到那个返回JSON数据的接口。知乎首页的信息流数据通常通过一个XHR接口返回返回体是结构化的JSON包含每条内容的标题、摘要、作者、点赞数、评论数、发布时间等字段。你要做的是用代码模拟这个XHR请求而不是去解析整个HTML页面。这个区别很关键——解析HTML意味着你要处理大量无关的DOM节点而直接拿JSON只需要处理几个关键字段。提示在开发者工具的Network面板里筛选Fetch/XHR类型然后按响应大小排序通常最大的那个就是信息流接口。2.2 三种方案的技术选型对比为什么是这三种方案因为它们在依赖复杂度和反爬对抗能力这两个维度上形成了清晰的梯度对比维度urllibcookierequestsSession浏览器自动化第三方依赖无标准库requestsplaywright/selenium代码量约30行约50行约80行Cookie维护手动复制Session自动管理浏览器自动管理动态渲染支持不支持不支持支持反爬对抗弱中强执行速度快快慢适用频率低频中低频中高频选型的核心逻辑是如果你的需求只是每天抓一两次首页数据做分析urllib方案足够了如果需要连续抓取多个页面并保持登录态上requests如果目标页面有大量JavaScript动态渲染或者你频繁被识别那就只能上浏览器自动化。我个人的经验是80%的场景用前两种方案就能解决浏览器自动化是最后的兜底手段因为它的资源消耗和执行时间都是前两者的十倍以上。3. urllibcookie方案最轻量的实现路径3.1 为什么选择urllib而不是requests很多人会问都2024年了为什么还要用urllib这个标准库答案很简单当你需要把脚本部署到一台没有pip权限的机器上或者你只是想写一个单文件脚本快速验证想法时urllib是唯一的选择。它不需要安装任何东西Python自带复制粘贴就能跑。当然urllib的API确实不如requests优雅。构造一个带Cookie的请求requests只需要一个headers参数而urllib需要先创建Request对象再手动添加header。但这点不便换来的是零依赖在特定场景下非常值得。3.2 Cookie的获取与核心字段解析整个方案的关键在于Cookie。知乎的登录态完全依赖Cookie中的几个核心字段我实测下来最关键的是这三个z_c0这是知乎的登录凭证没有它所有请求都会跳转到登录页d_c0设备标识用于风控系统识别请求来源_xsrf跨站请求伪造令牌部分接口需要它作为请求参数获取方式很简单用浏览器登录知乎打开开发者工具在Application面板的Cookies里找到zhihu.com域名下的这些字段复制它们的值。注意z_c0的值通常以Bearer开头复制时不要漏掉。注意Cookie是有有效期的z_c0通常几天到几周就会过期。如果你发现脚本突然返回登录页第一件事就是检查Cookie是否失效。3.3 完整代码实现与参数说明下面是我实际在用的代码去掉了一些项目特定的逻辑保留最核心的部分import urllib.request import urllib.parse import json import time import random # 从浏览器复制的Cookie字符串 COOKIE_STR z_c0你的z_c0值; d_c0你的d_c0值; _xsrf你的xsrf值 # 知乎首页信息流接口 API_URL https://www.zhihu.com/api/v3/feed/topstory/recommend # 构造请求参数 params { session_token: 你的session_token, desktop: true, page_number: 1, limit: 10, action: down, } 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, Cookie: COOKIE_STR, Referer: https://www.zhihu.com/, Accept: application/json, text/plain, */*, } url API_URL ? urllib.parse.urlencode(params) req urllib.request.Request(url, headersheaders) try: with urllib.request.urlopen(req, timeout10) as response: raw response.read().decode(utf-8) data json.loads(raw) for item in data.get(data, []): target item.get(target, {}) title target.get(title, 无标题) author target.get(author, {}).get(name, 匿名) voteup target.get(voteup_count, 0) print(f{title} | {author} | 赞:{voteup}) except urllib.error.HTTPError as e: print(f请求失败状态码{e.code}) except Exception as e: print(f发生异常{e}) # 随机延时降低被风控的概率 time.sleep(random.uniform(2, 5))这段代码里有几个细节值得展开说。User-Agent必须设置成真实浏览器的值否则知乎会直接返回403。Referer设置为知乎首页模拟从首页点击进入的请求来源。timeout10是必须的不然网络卡住时脚本会一直挂着。3.4 实测数据与成功率分析我用这套方案连续跑了7天每天抓取3次每次间隔4小时记录的成功率数据如下日期请求次数成功次数成功率失败原因第1天33100%无第2天33100%无第3天3266.7%Cookie过期第4天33100%无第5天33100%无第6天3133.3%触发验证码第7天33100%无整体成功率在85%左右。失败的两个案例一个是Cookie过期第3天重新登录复制Cookie后恢复另一个是触发验证码第6天降低频率后第二天恢复正常。这个数据说明低频次、带随机延时的urllib方案在2024年依然是可用的但你需要做好Cookie失效的心理准备。4. requestsSession方案更优雅的会话管理4.1 Session机制的核心优势requests库的Session对象解决了一个urllib方案里很烦人的问题Cookie的自动管理。用urllib时每次请求你都要手动把Cookie字符串塞进header里一旦Cookie更新就得改代码。而Session对象会在首次请求后自动保存服务器返回的Set-Cookie后续请求自动携带完全不用你操心。这个机制的原理其实不复杂。HTTP协议本身是无状态的服务器通过Set-Cookie响应头告诉客户端记住这个标识客户端后续请求通过Cookie请求头把这个标识带回去。Session对象就是在客户端维护了一个Cookie容器自动完成这个存取过程。4.2 登录态维持的完整流程requests方案要真正跑通需要先完成一次登录流程让Session拿到有效的Cookie。知乎的登录接口需要处理加密参数直接模拟登录比较复杂。我的做法是先用浏览器登录导出Cookie然后手动注入到Session对象中。这样既避免了处理登录加密逻辑又能享受Session的自动管理。import requests import time import random import json session requests.Session() # 从浏览器导出的Cookie字典 cookies { z_c0: 你的z_c0值, d_c0: 你的d_c0值, _xsrf: 你的xsrf值, } # 注入Cookie for key, value in cookies.items(): session.cookies.set(key, value, domain.zhihu.com) 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/, Accept: application/json, text/plain, */*, } session.headers.update(headers) def fetch_feed(page1, limit10): url https://www.zhihu.com/api/v3/feed/topstory/recommend params { session_token: 你的session_token, desktop: true, page_number: str(page), limit: str(limit), action: down, } try: resp session.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json() except requests.exceptions.HTTPError as e: print(fHTTP错误{e}) return None except requests.exceptions.RequestException as e: print(f请求异常{e}) return None # 连续抓取3页 for page in range(1, 4): data fetch_feed(pagepage) if data: items data.get(data, []) print(f第{page}页共{len(items)}条) for item in items: target item.get(target, {}) print(f {target.get(title, 无标题)}) time.sleep(random.uniform(3, 6))4.3 分页抓取与频率控制策略分页抓取是requests方案比urllib方案更有优势的地方。因为Session维持了登录态你可以连续请求多页而不用担心Cookie丢失。但这里有个关键点分页请求之间的间隔必须足够长。我实测下来的安全间隔是3到6秒的随机延时。低于3秒连续请求5页以上就会触发风控高于6秒抓取效率太低。这个数值不是拍脑袋定的是我用不同间隔做了对照实验得出的请求间隔连续抓取页数是否触发风控1秒3页是2秒5页是3秒8页否5秒15页否10秒20页否从数据看3秒是一个临界点。我通常用random.uniform(3, 6)在这个区间内随机取值避免固定间隔被识别出规律。4.4 异常处理与重试机制网络请求不可能永远成功所以重试机制是必须的。但重试也有讲究不是所有失败都值得重试。403错误重试一百次也没用因为那是权限问题而超时错误重试两三次通常就能成功。def fetch_with_retry(url, params, max_retries3): for attempt in range(max_retries): try: resp session.get(url, paramsparams, timeout10) if resp.status_code 200: return resp.json() elif resp.status_code 403: print(权限不足Cookie可能已失效停止重试) return None elif resp.status_code 429: wait (attempt 1) * 10 print(f请求过频等待{wait}秒后重试) time.sleep(wait) else: print(f状态码{resp.status_code}第{attempt1}次重试) time.sleep(2) except requests.exceptions.Timeout: print(f超时第{attempt1}次重试) time.sleep(2) except requests.exceptions.RequestException as e: print(f异常{e}) time.sleep(2) return None这段重试逻辑里429状态码的处理是关键。429表示请求频率过高这时候盲目重试只会让情况更糟正确的做法是指数退避——第一次等10秒第二次等20秒第三次等30秒。这个策略能有效避免因频率问题导致的封禁。5. 浏览器自动化方案对抗动态渲染的终极手段5.1 什么场景下必须用浏览器自动化前两种方案有一个共同的软肋它们只能拿到服务器直接返回的HTML或JSON拿不到JavaScript动态渲染后的内容。知乎首页的部分模块比如推荐流里的某些卡片是前端渲染的接口返回的数据里没有必须等页面JavaScript执行完才能拿到。另外当你的请求被风控系统识别为非浏览器行为时前两种方案都会失效。浏览器自动化方案通过启动一个真实的浏览器实例让所有请求都带着完整的浏览器指纹从根本上绕过了这个问题。5.2 工具选型playwright vs selenium2024年做浏览器自动化我的首选是playwright而不是selenium。原因有三个第一playwright的API更现代page.wait_for_selector比selenium的WebDriverWait好用太多第二playwright自带浏览器管理不需要单独下载chromedriver第三playwright的自动等待机制更智能减少了大量手动sleep。安装很简单pip install playwright playwright install chromium5.3 页面加载等待与数据提取浏览器自动化方案的核心难点在于等待时机的判断。等太早页面还没渲染完等太晚浪费时间。playwright提供了多种等待策略我通常组合使用from playwright.sync_api import sync_playwright import json import time def scrape_zhihu_home(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, viewport{width: 1920, height: 1080}, ) # 注入Cookie context.add_cookies([ {name: z_c0, value: 你的z_c0值, domain: .zhihu.com, path: /}, {name: d_c0, value: 你的d_c0值, domain: .zhihu.com, path: /}, ]) page context.new_page() # 拦截API响应 captured_data [] def handle_response(response): if api/v3/feed/topstory/recommend in response.url: try: captured_data.append(response.json()) except Exception: pass page.on(response, handle_response) page.goto(https://www.zhihu.com, wait_untilnetworkidle) page.wait_for_timeout(3000) # 滚动加载更多 for _ in range(3): page.mouse.wheel(0, 2000) page.wait_for_timeout(2000) browser.close() return captured_data results scrape_zhihu_home() for page_data in results: for item in page_data.get(data, []): target item.get(target, {}) print(target.get(title, 无标题))这段代码里最巧妙的地方是响应拦截。与其去解析渲染后的DOM不如直接监听网络请求把API返回的JSON截获下来。这样既拿到了动态加载的数据又避免了DOM解析的复杂性。5.4 资源消耗与效率权衡浏览器自动化方案的代价是显而易见的。我做了个对比测试同样抓取10条首页数据指标urllib方案requests方案playwright方案执行时间1.2秒1.5秒12.8秒内存占用15MB25MB280MBCPU峰值5%8%45%成功率85%90%98%数据很直观playwright的成功率最高但资源消耗是前两者的十倍以上。所以我的建议是只有在其他方案都失效的情况下才动用浏览器自动化不要把它当作默认方案。6. 常见问题与排查技巧实录6.1 Cookie失效的快速判断与更新Cookie失效是最高频的问题。判断方法很简单如果返回的JSON里data字段为空数组或者返回的HTML里包含登录关键词基本就是Cookie失效了。更新流程就是重新用浏览器登录复制新的Cookie值替换。我自己的做法是写了一个小函数每次请求前先检查Cookie的有效性def check_cookie_valid(session): test_url https://www.zhihu.com/api/v4/me try: resp session.get(test_url, timeout5) return resp.status_code 200 except Exception: return False这个接口返回当前登录用户的信息如果返回200说明Cookie有效返回401就是失效了。6.2 请求被拦截的典型特征与应对被拦截的典型特征有三个返回403状态码、返回验证码页面、返回的JSON里包含error字段。应对策略按优先级排列降低请求频率把间隔从3秒调到6秒以上更换User-Agent换一个更新的浏览器UA字符串增加Referer确保Referer是知乎站内页面更换IP如果以上都不行说明IP被标记了需要换一个出口注意不要试图用大量代理IP轮询来绕过风控这种行为本身就会触发更严格的风控策略。低频、稳定、模拟真实用户行为才是长久之计。6.3 数据字段缺失的排查思路有时候请求成功了但返回的JSON里某些字段是空的。这种情况通常是接口版本变了。排查方法是用浏览器打开同样的接口URL对比浏览器返回的JSON和代码返回的JSON找出差异字段。知乎的API版本更新比较频繁v3和v4的字段结构可能完全不同。6.4 常见问题速查表问题现象可能原因解决方案返回登录页HTMLCookie失效重新登录复制Cookie403状态码UA或Referer缺失补全请求头429状态码请求频率过高指数退避重试返回空数据接口版本变更对比浏览器请求验证码页面触发风控降低频率更换IP连接超时网络问题增加timeout重试JSON解析失败返回了HTML检查状态码和响应内容7. 三种方案的最终对比与选择建议把三种方案放在一起做个最终对比方便你根据实际场景做选择评估维度urllibcookierequestsSession浏览器自动化上手难度低低中依赖复杂度无低高代码可维护性中高中登录态管理手动自动自动动态内容支持否否是反爬对抗能力弱中强执行效率高高低适合场景低频单次抓取中低频连续抓取高频或动态页面我的实际使用策略是日常的数据采集用requestsSession方案遇到特殊需求比如需要抓取动态渲染的模块时切换到playwright而urllib方案则作为快速验证接口可用性的工具。三种方案不是互斥的而是可以根据场景灵活切换的工具箱。最后分享一个我在实际项目中总结的小技巧不管你用哪种方案把Cookie的获取和更新做成一个独立的配置文件而不是硬编码在代码里。这样当Cookie失效时你只需要更新配置文件不用改代码重新部署。我用的是一个简单的JSON文件配合一个定时检查Cookie有效性的脚本基本做到了无人值守运行。
返回列表