1. 项目缘起与核心价值
最近在折腾墨水屏设备,想在上面实现一个沉浸式的阅读体验,自然就想到了微信读书。但官方App在墨水屏上的适配,尤其是那个1.5.2版本,功能上总有些掣肘。比如,我想把书架同步到自己的服务器上做个备份,或者想写个小工具自动整理我的阅读笔记和划线,发现官方并没有提供现成的开放接口。这让我萌生了一个想法:能不能通过技术手段,逆向分析微信读书App的网络请求,整理出一套可用的“非官方”API,从而解锁更多个性化的玩法?
这不仅仅是“抓个包”那么简单。它涉及到对现代移动应用通信协议(如HTTP/2、Protobuf)的理解,对授权认证机制(如Token、Cookie)的维护,以及对服务端反爬策略的应对。更重要的是,通过分析这些API,我们能一窥一个千万级日活产品背后的核心数据模型和业务逻辑设计,比如书籍信息如何组织、阅读进度如何同步、社交互动如何实现。这对于开发者理解一个复杂C端应用的数据流转,有着极高的学习价值。无论你是想开发一个第三方阅读客户端,还是想构建个人知识管理系统来自动化处理微信读书的数据,亦或是单纯对大型应用架构感兴趣,这个分析过程都是一次绝佳的实战。
2. 前期准备与环境搭建
工欲善其事,必先利其器。分析移动端API,首要任务是能够捕获并解密其网络流量。由于微信读书App普遍使用了HTTPS加密,我们需要一些特定的工具来搭建中间人攻击(MitM)环境。
2.1 核心工具选型与配置
我的分析环境主要基于以下工具链,它们各自扮演着关键角色:
抓包代理工具:Charles Proxy我选择Charles而非Fiddler,主要是因为Charles对HTTP/2和JSON格式化的支持更友好,界面也更直观。关键在于安装Charles的根证书,并让移动设备信任它。在手机上配置Wi-Fi代理,将服务器指向运行Charles的电脑IP,端口通常为8888。这样,手机的所有HTTP/HTTPS流量都会先经过Charles。
注意:很多App,包括微信读书,会启用证书绑定(SSL Pinning),即App只信任自己内置的证书,拒绝系统证书。这会直接导致Charles无法解密HTTPS流量。为此,我们需要一个额外的步骤来绕过它。
SSL Pinning绕过工具:FridaFrida是一个动态代码插桩框架,它可以在App运行时,注入脚本修改其行为。我们通过Frida脚本,可以Hook住App内部校验证书的代码逻辑,使其接受Charles的证书。通常需要一台已Root的Android设备或越狱的iOS设备来运行Frida Server。对于微信读书,社区已有一些现成的反Pinning脚本可供参考或修改使用。
辅助分析与调试工具
- Postman / Insomnia:用于将捕获到的API请求导入,并重放、修改参数,进行测试。
- Chrome开发者工具:对于某些WebView或H5页面发起的请求,可以直接用电脑浏览器打开并调试。
- Wireshark:作为底层抓包补充,当遇到非HTTP协议(如自定义TCP)时使用,但本次分析中未涉及。
2.2 设备与账号准备
- 测试设备:建议使用一台备用安卓手机,并获取Root权限。这为安装Frida、使用Xposed模块等高级操作提供了便利。如果没有Root设备,可以尝试使用安卓模拟器(如夜神、MuMu),部分模拟器支持Root模式,但需注意模拟器环境可能与真机有差异,可能触发风控。
- 测试账号:强烈建议使用一个全新的、不重要的微信小号来登录微信读书进行测试。因为频繁的异常请求、登录行为可能导致账号被临时限制功能,使用主账号存在风险。
配置好Charles和Frida后,打开微信读书App,进行登录、浏览书架、阅读、划线等操作。此时,Charles的会话列表中应该会逐步出现大量来自weread.qq.com、i.weread.qq.com等域名的HTTPS请求。如果请求内容显示为乱码或“Unknown”,说明SSL Pinning绕过成功但解密可能遇到其他加密(如RequestBody使用Protobuf),这是我们下一步要攻克的重点。
3. 核心接口分析与鉴权机制
成功捕获流量后,面对满屏的请求,我们需要像侦探一样,从中梳理出核心的API脉络。微信读书的API设计遵循了典型的RESTful风格,但夹杂着一些GraphQL的影子(用于复杂数据查询)。
3.1 登录与会话维持
一切始于登录。微信读书的登录严格依赖微信的OAuth2.0授权。
- 登录流程:App启动后,会向微信发起授权请求。授权成功后,会获得一个
code。App将这个code提交给微信读书的后端(例如https://weread.qq.com/login),后端与微信服务器验证后,会返回一个关键的登录凭证。 - 核心凭证:
wr_skey与wr_rt:分析请求头,你会发现两个至关重要的Cookie:wr_skey和wr_rt(名称可能随版本迭代变化,但功能类似)。wr_skey:相当于会话Token,是后续绝大多数API请求的身份凭据。它被设置在Cookie头中,也有时出现在自定义的X-Requested-With或Authorization头里,需要仔细比对。wr_rt:刷新Token。当wr_skey过期后,可以使用wr_rt调用特定的刷新接口来获取新的wr_skey,而无需用户重新登录。
- 请求签名:为了防止重放攻击,重要的写操作(如添加笔记、发表想法)API,往往还带有签名机制。签名算法通常是将请求参数、时间戳和一个固定盐值(Salt)按特定顺序拼接后,进行MD5或SHA256运算。这个签名值会以
x-sign或类似名称的请求头发送。服务器端会以同样的算法验签,不一致则拒绝请求。
实操心得:在Postman中测试API时,首要任务就是模拟这个登录流程,或者更简单地,直接从Charles捕获的某个成功请求中,将完整的Cookie请求头复制出来,作为环境变量。这样,在测试其他API时,直接引用该环境变量即可模拟已登录状态。务必注意Cookie的过期时间。
3.2 关键业务接口解析
以下是我梳理出的部分核心接口及其用途。请注意,接口地址和参数可能随版本更新而变化,此处仅为示例。
| 功能模块 | 疑似接口地址 (示例) | 请求方法 | 核心参数/请求体 | 返回数据说明 |
|---|---|---|---|---|
| 书架与书籍 | https://i.weread.qq.com/user/books | GET | type=0(0:书架, 1:已购) | 返回用户书架列表,包含书籍ID、封面、最新阅读进度等。 |
| 书籍详情 | https://i.weread.qq.com/book/info | GET | bookId=xxx | 返回书籍的元数据:标题、作者、出版社、简介、目录等。 |
| 阅读进度 | https://i.weread.qq.com/read/readinfo | GET | bookId=xxx | 返回本书的阅读进度(百分比、最后阅读章节)、阅读时长等。 |
| 获取章节内容 | https://i.weread.qq.com/book/chapter | GET | bookId=xxx&chapterIdx=n | 返回指定章节的纯文本或带格式的HTML内容。这是核心资源接口。 |
| 笔记与划线 | https://i.weread.qq.com/web/book/bookmarklist | GET | bookId=xxx | 返回用户在该书中的所有划线(bookmark)和笔记(note)。 |
| 添加想法/笔记 | https://i.weread.qq.com/review/add | POST | JSON Body:{bookId, chapterUid, range, content...} | 发布一条想法或笔记到指定段落。需要签名。 |
| 发现与推荐 | https://weread.qq.com/web/book/list-in-booklist | GET | booklistId=xxx | 获取某个书单的详情和书籍列表。 |
接口分析要点:
- 数据格式:响应体大多是JSON,但书籍章节内容等可能采用更高效的二进制协议(如Protobuf)编码,返回的
Content-Type可能是application/protobuf。这时,你需要找到对应的.proto协议定义文件(可能通过反编译App获得),或使用工具尝试反序列化。 - 分页与参数:列表接口通常有
count和maxIdx参数来控制分页。maxIdx一般是上一批返回的最后一个项目的ID或索引。 - GraphQL端点:部分复杂查询,如同时获取书籍详情、进度、笔记,可能会请求一个统一的GraphQL端点(如
/graphql),请求体是一个包含查询语句和变量的JSON。这需要分析App源码中的查询语句。
4. 数据获取与处理实战
掌握了接口,下一步就是如何稳定、高效地获取和处理数据。这里会遇到几个典型的挑战。
4.1 应对反爬策略
微信读书的后端不是毫无防备的,频繁、规律的请求会很快被识别并拦截。
- 频率限制:这是最常见的。直接表现为返回
429 Too Many Requests或自定义的错误码。解决方案:在代码中为每个请求之间加入随机延迟(例如time.sleep(random.uniform(1, 3))),模拟人类阅读的不规律操作。对于批量获取书籍内容,更要“慢工出细活”。 - User-Agent与设备指纹:服务器会检查请求头中的
User-Agent,甚至通过一些JavaScript收集设备信息生成指纹。解决方案:完全模拟官方App的请求头。从Charles中复制一套完整的Headers,包括User-Agent(如WeRead/6.3.2 (iPhone; iOS 15.4; Scale/3.00))、X-Requested-With、Referer等,并在整个会话中保持一致。 - IP限制:如果一个IP在短时间内产生过多请求,可能会被暂时封禁。解决方案:对于大规模数据采集,需要考虑使用代理IP池。但对于个人用途,控制请求频率通常已足够。
- 参数校验与签名:如前所述,写操作接口有签名。你必须逆向出签名算法,否则无法成功调用。这通常需要静态分析App的Java/Kotlin(Android)或Objective-C/Swift(iOS)代码。
4.2 数据解析与存储示例
假设我们的目标是同步所有已读图书的划线笔记到本地Markdown文件。下面是一个简化的Python流程框架:
import requests import json import time import random # 1. 配置会话(从Charles复制) SESSION_COOKIES = 'wr_skey=xxx; wr_rt=xxx' HEADERS = { 'User-Agent': 'WeRead/6.3.2 ...', 'Cookie': SESSION_COOKIES, 'Referer': 'https://weread.qq.com/' } BASE_URL = 'https://i.weread.qq.com' # 2. 获取书架列表 def get_bookshelf(): resp = requests.get(f'{BASE_URL}/user/books', params={'type': 0}, headers=HEADERS) if resp.status_code == 200: return resp.json().get('books', []) else: print(f"获取书架失败: {resp.status_code}") return [] # 3. 遍历每本书,获取笔记 def get_notes_for_book(book_id, book_title): # 先获取阅读进度,确认已读 read_info = requests.get(f'{BASE_URL}/read/readinfo', params={'bookId': book_id}, headers=HEADERS).json() if read_info.get('readingProgress', 0) < 0.01: # 进度小于1%视为未读 print(f"跳过未读书籍: {book_title}") return [] # 获取笔记列表 notes_resp = requests.get(f'{BASE_URL}/web/book/bookmarklist', params={'bookId': book_id}, headers=HEADERS) notes = notes_resp.json().get('updated', []) # 处理笔记,可能还需要根据noteId获取详细内容 processed_notes = [] for note in notes: # 解析划线内容、章节、时间等信息 # ... processed_notes.append(note) time.sleep(random.uniform(1, 2)) # 关键延迟,避免请求过快 return processed_notes # 4. 主流程 def main(): all_books = get_bookshelf() all_notes = [] for book in all_books[:5]: # 先测试前5本 book_id = book['bookId'] book_title = book['title'] print(f"处理书籍: {book_title}") notes = get_notes_for_book(book_id, book_title) all_notes.extend(notes) # 5. 导出为Markdown with open('weread_notes.md', 'w', encoding='utf-8') as f: for note in all_notes: f.write(f"## {note['bookTitle']}\n") f.write(f"> {note['markText']}\n\n") f.write(f"*我的笔记*: {note.get('content', '')}\n\n") f.write("---\n\n") if __name__ == '__main__': main()这个脚本只是一个起点。在实际操作中,你需要处理网络异常、登录态过期自动刷新、更复杂的数据结构等问题。
5. 常见问题与排查实录
在分析和使用的过程中,我踩过不少坑。这里把一些典型问题和解决思路记录下来。
5.1 抓包与解密问题
- 问题:Charles抓不到微信读书的包,或者抓到全是
Tunnel to ...:443。- 排查:首先检查手机Wi-Fi代理设置是否正确。然后确认Charles的根证书是否已在手机系统证书库中安装并启用。如果仍不行,大概率是SSL Pinning。
- 解决:必须使用Frida等工具绕过。确保Frida-server在手机上运行,并且电脑端的frida-ps能列出手机进程。运行反pinning脚本后,再尝试抓包。
- 问题:请求能抓到,但响应体是乱码或二进制。
- 排查:查看响应的
Content-Type头。如果是application/protobuf,则需要Proto定义文件来解码。 - 解决:尝试搜索开源项目(如GitHub上的
weread-api相关项目),看是否有人已经逆向出了.proto文件。或者使用protoc工具和猜测的结构进行试错解析。
- 排查:查看响应的
5.2 API请求错误
- 问题:请求返回
400 Bad Request,错误信息类似'type' must be in ["enabled", "disabled", "auto"]。- 排查:这是一个典型的参数校验错误。错误信息很友好,直接告诉你
type字段的值只能是列表中的三个。 - 解决:检查你的请求体或查询参数,确保
type字段的值是enabled、disabled或auto之一。仔细比对从Charles捕获的正确请求的格式。
- 排查:这是一个典型的参数校验错误。错误信息很友好,直接告诉你
- 问题:返回
401 Unauthorized或403 Forbidden。- 排查:登录凭证(Cookie/Token)已过期或无效。
- 解决:重新登录获取新的Cookie。如果是自动化脚本,需要实现一个检测到401错误后,自动调用Token刷新接口或重新登录的逻辑。
- 问题:返回
429 Too Many Requests。- 解决:立即停止请求,延长请求间隔时间。在代码中加入指数退避算法,即每次遇到429后,等待时间加倍。
- 问题:返回
500 Internal Server Error或502 Bad Gateway。- 排查:可能是你发送的请求参数格式完全错误,导致服务器端处理异常;也可能是服务端临时故障。
- 解决:首先核对参数格式。如果格式无误,可能是服务端问题,等待一段时间再重试。
5.3 数据解析与使用限制
- 问题:获取到的书籍内容不全,或者只有前几章。
- 排查:部分热门或版权严格的书籍,接口可能只会返回试读部分。或者,章节列表接口和内容接口需要特定的权限标识。
- 解决:检查返回数据中是否有
hasFullContent之类的字段。对于付费书籍,非付费用户通过API通常也无法获取全文,这是正常的版权限制。
- 问题:想将笔记导出为PDF或EPUB。
- 思路:API本身不提供导出功能。你需要自己实现一个渲染引擎。步骤是:1. 获取书籍元数据和目录。2. 按顺序获取所有章节内容。3. 获取所有笔记并定位到对应章节和段落。4. 使用像
weasyprint(HTML转PDF)或ebooklib(生成EPUB)这样的库,将文本、样式和笔记合并生成最终文件。这是一个工程量不小的项目。
- 思路:API本身不提供导出功能。你需要自己实现一个渲染引擎。步骤是:1. 获取书籍元数据和目录。2. 按顺序获取所有章节内容。3. 获取所有笔记并定位到对应章节和段落。4. 使用像
最后一点体会:分析和使用非官方API始终行走在灰色地带。务必遵守以下原则:仅用于个人学习、数据备份和效率提升,不要进行大规模爬取、商业用途或干扰微信读书正常服务。你的请求行为应该尽可能地“像”一个真实的App用户,尊重服务器的负载能力。技术是用来创造和便利的,而不是破坏。通过这次分析,我不仅实现了墨水屏上的优雅阅读方案,更深刻理解了现代App前后端交互的复杂性,这比单纯拿到数据更有价值。