简介:这是一套面向Python初学者与爬虫爱好者的微信读书数据导出工具,解决个人学习场景下书籍列表与阅读笔记难以批量保存的问题。资源包含8个文件,总计277KB,以3个核心Python脚本(GUI界面、主爬虫逻辑、Excel处理)、2个文本说明文件(依赖清单与使用指南)、2张界面演示图及1份Markdown文档构成,结构紧凑、即装即用。已有2168人下载学习,适合希望快速掌握登录模拟、页面解析、GUI封装与数据导出全流程的实践者。读者可直接运行pyqt_gui.py启动图形化界面,通过可视化操作完成账号登录、书架抓取、笔记提取与Excel一键导出,配套requirement.txt确保环境一致性,README.md与注释清晰标注关键步骤与调试提示,降低上手门槛。
1. 项目缘起与核心价值
作为一个重度阅读爱好者,我几乎把所有的碎片时间都泡在了微信读书上。划线、写想法、看别人的批注,几年下来积累了上千条笔记。但问题也随之而来:这些宝贵的阅读记录和思考碎片,全都锁在微信读书的服务器里。想整理成个人知识库?想离线保存以防万一?或者单纯就是想拥有一个属于自己的、格式整洁的笔记备份?官方并没有提供一个完美的导出方案。手动复制粘贴?对于动辄几十上百条的笔记来说,这无异于一场噩梦。正是这个痛点,催生了这个项目:用Python写一个爬虫工具,实现一键导出微信读书的书籍信息和全部笔记。
这件事的价值远不止于“备份”这么简单。首先,它关乎数据主权。你的阅读记录、你的思考痕迹,理应有一份完全由你掌控的本地副本。其次,是知识管理的需要。导出的笔记可以轻松导入到Notion、Obsidian、思源笔记等专业工具中,进行二次加工、关联思考,构建个人的阅读知识图谱。最后,它还是一个绝佳的Python爬虫实战项目,涵盖了登录态维持、接口逆向分析、数据清洗整理等爬虫工程师的常见工作流,实用性极强。
接下来,我将彻底拆解这个工具的完整实现过程,从原理分析、环境搭建,到核心代码逐行解读,再到打包成开箱即用的脚本。你会发现,整个过程就像侦探破案,一步步找到数据藏匿的地址,并优雅地把它“请”出来。
2. 核心思路与技术方案选型
在动手写代码之前,我们必须先想清楚:我们要拿什么?以及从哪里拿?
我们的目标是获取两类核心数据:书籍元信息(书名、作者、封面等)和用户笔记数据(包括章节内的划线、以及独立的想法批注)。微信读书作为一个成熟的App,其数据交互必然通过API接口进行。因此,爬虫的核心思路不是去解析复杂的HTML页面,而是直接模拟App或网页端的请求,调用其内部的数据接口。
2.1 技术栈选择与理由
请求库:Requests这是Python生态中公认的HTTP库标杆,语法简洁,功能强大。相比于
urllib,它的封装更人性化;对于这个项目,我们不需要aiohttp那样的异步并发能力,Requests的同步请求简单可靠,完全够用。数据解析:内置的
json模块微信读书的API返回的数据格式基本都是JSON。Python标准库中的json模块足以完美应对解析和序列化任务,无需引入额外的依赖。数据处理与导出:Pandas & Openpyxl
Pandas:数据处理的“瑞士军刀”。我们将获取的笔记列表(通常是JSON数组)转化为DataFrame,可以非常方便地进行清洗、筛选、排序等操作。Openpyxl:专门用于读写Excel文件的库。选择它而不是Pandas自带的to_excel,是因为我们需要对导出的Excel格式进行更精细的控制,比如调整列宽、设置单元格样式(将笔记内容自动换行),提升可读性。
登录态维持:Session对象
Requests库的Session对象是关键。它能够自动处理Cookies,在一次会话中保持登录状态。我们只需要在开始时成功“登录”一次,后续的所有请求都会自动携带身份凭证。
为什么不直接用Selenium?这是一个常见的疑问。Selenium模拟浏览器操作,对于需要执行JavaScript或应对复杂反爬的网站很有效。但微信读书的API接口清晰,数据以结构化JSON返回,使用Selenium相当于用大炮打蚊子,会引入不必要的复杂度(安装浏览器驱动、运行效率低)。直接进行HTTP接口请求是更轻量、更高效的选择。
2.2 逆向分析:找到数据之门
这是整个项目最具挑战性也最有趣的部分。我们需要扮演“侦探”,找出微信读书请求数据的真实地址。
方法一:抓包移动端App使用像Fiddler或Charles这样的抓包工具,在电脑上设置代理,并将手机的网络代理指向电脑。然后在手机上打开微信读书App,浏览书籍、查看笔记。此时,抓包工具会捕获到所有网络请求。你需要从中筛选出包含books、notes、highlights、bookmarks等关键词的请求,分析其URL、请求头(特别是Cookie和Authorization类字段)以及请求参数。
方法二:分析网页端直接在电脑浏览器(Chrome或Edge)中打开微信读书网页版,使用开发者工具。操作步骤类似:打开“网络”标签页,在网页中翻看书籍、点击笔记选项卡。观察发出的XHR或Fetch请求,找到返回笔记列表的那个接口。
通过分析,你会发现核心接口通常形如:https://i.weread.qq.com/user/notebooks或https://i.weread.qq.com/book/bookmarklist。 请求头中通常会包含一个至关重要的字段:wr_vid、wr_skey或Authorization: Bearer xxx。这就是你的身份令牌。
实操心得:获取身份凭证最直接稳定的方式是从网页版获取。登录微信读书网页版后,在开发者工具的“应用程序”标签页中,查看
Cookies,找到名为wr_vid或类似名称的项,其value就是我们需要的东西。这个vid(或skey)通常有较长的有效期,足以完成一次导出任务。
3. 环境准备与核心依赖安装
工欲善其事,必先利其器。我们先来搭建一个干净的项目环境。
我强烈建议使用conda或venv创建独立的Python虚拟环境,避免与系统或其他项目的包版本冲突。
# 创建并激活一个名为 weread-export 的虚拟环境 python -m venv weread-export # Windows 激活 weread-export\Scripts\activate # macOS/Linux 激活 source weread-export/bin/activate激活虚拟环境后,安装所需的第三方库:
pip install requests pandas openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simplerequests: 用于发送HTTP请求。pandas: 用于数据处理和转换。openpyxl: 用于生成格式良好的Excel文件。
一个良好的项目结构有助于代码管理,建议如下:
wechat_read_exporter/ ├── config.py # 配置文件,存放Cookie等敏感信息 ├── wechat_read_api.py # 核心API请求模块 ├── data_processor.py # 数据处理与导出模块 ├── main.py # 主程序入口 └── requirements.txt # 项目依赖列表在config.py中,我们以变量的形式存放凭证:
# config.py # 请将从浏览器中获取的Cookie值替换到此处 USER_COOKIE = "wr_vid=YOUR_ACTUAL_VID_VALUE; wr_skey=YOUR_ACTUAL_SKEY_VALUE" # 或者使用Bearer Token(如果抓包得到的是这种形式) BEARER_TOKEN = "your_bearer_token_here"注意事项:安全第一
config.py文件绝对不能上传到GitHub等公开代码仓库。你应该将它添加到.gitignore文件中。- 凭证(Cookie/Token)是你的账户通行证,泄露可能导致账户被他人滥用。务必妥善保管。
- 本项目所有代码仅用于个人学习和技术交流,请尊重微信读书的用户协议,不要进行高频、大量的请求,以免对服务器造成压力。
4. 核心模块实现:从请求到数据
接下来,我们分模块构建这个爬虫工具。首先从最核心的API交互开始。
4.1 构建请求客户端
在wechat_read_api.py中,我们创建一个类来封装所有与微信读书服务器通信的逻辑。
# wechat_read_api.py import requests import json import time from config import USER_COOKIE, BEARER_TOKEN class WeReadClient: def __init__(self): self.session = requests.Session() self.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', 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Connection': 'keep-alive', } # 优先使用Bearer Token,如果没有则使用Cookie if BEARER_TOKEN: self.headers['Authorization'] = f'Bearer {BEARER_TOKEN}' else: self.headers['Cookie'] = USER_COOKIE self.base_url = 'https://i.weread.qq.com' def _make_request(self, method, endpoint, params=None, data=None): """统一的请求发送方法,处理异常和重试""" url = f"{self.base_url}{endpoint}" max_retries = 3 for attempt in range(max_retries): try: response = self.session.request(method, url, headers=self.headers, params=params, json=data, timeout=10) response.raise_for_status() # 如果状态码不是200,抛出HTTPError异常 return response.json() # 尝试解析为JSON except requests.exceptions.RequestException as e: print(f"请求失败 ({attempt+1}/{max_retries}): {e}") if attempt < max_retries - 1: wait_time = 2 ** attempt # 指数退避策略 print(f"等待 {wait_time} 秒后重试...") time.sleep(wait_time) else: print("重试次数用尽,请求最终失败。") raise except json.JSONDecodeError: print("响应内容不是有效的JSON格式。") return response.text # 返回文本内容 return None def get_bookshelf(self): """获取书架上的书籍列表""" # 实际接口可能需要参数,如排序方式、分页等,需根据抓包结果调整 endpoint = '/user/books' params = {'type': 1, 'count': 1000} # 假设参数 return self._make_request('GET', endpoint, params=params) def get_book_detail(self, book_id): """获取指定书籍的详细信息(元数据)""" endpoint = f'/book/info' params = {'bookId': book_id} return self._make_request('GET', endpoint, params=params) def get_book_notes(self, book_id): """获取指定书籍的全部笔记(划线和想法)""" # 这个接口可能是分页的,需要循环获取 all_notes = [] page_num = 1 page_size = 100 # 每页数量,根据接口支持调整 while True: endpoint = '/book/bookmarklist' params = {'bookId': book_id, 'count': page_size, 'currentPage': page_num} data = self._make_request('GET', endpoint, params=params) if not data or 'updated' not in data or not data.get('updated'): # 根据接口实际返回结构判断是否还有数据 break notes = data.get('updated', []) if not notes: break all_notes.extend(notes) # 如果返回数量小于请求数量,说明是最后一页 if len(notes) < page_size: break page_num += 1 time.sleep(0.5) # 礼貌性延迟,避免请求过快 return all_notes关键点解析:
- Session对象:
self.session保证了在一次运行中,所有请求共享Cookies,维持登录状态。 - 请求头:
User-Agent模拟浏览器,Accept声明接受JSON格式,这些都是让服务器认为请求来自合法客户端的常见操作。 - 错误处理与重试:
_make_request方法封装了请求过程,加入了简单的重试机制(指数退避)和异常捕获,增强了程序的健壮性。 - 分页处理:
get_book_notes方法中实现了循环请求,直到获取所有笔记。这是处理列表接口的通用模式。
4.2 数据处理与导出引擎
获取到原始的JSON数据后,我们需要将其转换成结构清晰、便于阅读和存档的格式。这里选择Excel作为导出格式,因为它普及率高,且能很好地处理表格数据。
在data_processor.py中,我们创建数据处理类:
# data_processor.py import pandas as pd from openpyxl import load_workbook from openpyxl.styles import Alignment, Font import os class DataExporter: @staticmethod def export_notes_to_excel(notes_list, book_info, output_dir='output'): """将笔记列表导出为格式化的Excel文件""" if not notes_list: print(f"书籍《{book_info.get('title', '未知')}》没有笔记。") return None # 创建输出目录 os.makedirs(output_dir, exist_ok=True) # 将数据转换为Pandas DataFrame # 原始笔记数据结构复杂,需要展平提取关键字段 processed_notes = [] for note in notes_list: # 根据实际接口返回的字段名调整 processed_note = { '章节': note.get('chapterTitle', ''), '位置': note.get('range', ''), # 可能是"location-1"或"第x章-第y节" '标记内容': note.get('markText', '').replace('\n', ' '), # 替换换行方便显示 '我的想法': note.get('abstract', ''), # 想法/批注 '标记时间': note.get('createTime', ''), '更新时间': note.get('updateTime', ''), } processed_notes.append(processed_note) df = pd.DataFrame(processed_notes) # 处理时间戳(如果接口返回的是Unix时间戳) if '标记时间' in df.columns: df['标记时间'] = pd.to_datetime(df['标记时间'], unit='s', errors='coerce').dt.strftime('%Y-%m-%d %H:%M:%S') if '更新时间' in df.columns: df['更新时间'] = pd.to_datetime(df['更新时间'], unit='s', errors='coerce').dt.strftime('%Y-%m-%d %H:%M:%S') # 生成文件名 safe_title = "".join([c for c in book_info.get('title', '未命名书籍') if c.isalnum() or c in (' ', '_', '-')]).rstrip() filename = f"{safe_title}_笔记导出.xlsx" filepath = os.path.join(output_dir, filename) # 使用Pandas先写入Excel with pd.ExcelWriter(filepath, engine='openpyxl') as writer: df.to_excel(writer, index=False, sheet_name='我的笔记') # 获取workbook和worksheet对象进行格式调整 workbook = writer.book worksheet = writer.sheets['我的笔记'] # 设置列宽自适应(粗略估计) column_widths = {} for column in df: # 获取列字母 column_letter = openpyxl.utils.get_column_letter(df.columns.get_loc(column) + 1) # 以列标题和内容的最大长度为基础设置宽度 max_length = max(df[column].astype(str).map(len).max(), len(column)) adjusted_width = min(max_length + 2, 50) # 设置最大宽度为50 column_widths[column_letter] = adjusted_width for col_letter, width in column_widths.items(): worksheet.column_dimensions[col_letter].width = width # 设置“标记内容”和“我的想法”列自动换行 wrap_columns = ['标记内容', '我的想法'] for col_name in wrap_columns: if col_name in df.columns: col_idx = df.columns.get_loc(col_name) + 1 col_letter = openpyxl.utils.get_column_letter(col_idx) for row in range(2, len(df) + 2): # 从第2行开始(第1行是标题) cell = worksheet[f'{col_letter}{row}'] cell.alignment = Alignment(wrapText=True, vertical='top') # 设置标题行加粗 for col in range(1, len(df.columns) + 1): cell = worksheet.cell(row=1, column=col) cell.font = Font(bold=True) print(f"笔记已成功导出至:{filepath}") return filepath @staticmethod def export_bookshelf_to_csv(books_list, output_dir='output'): """将书架列表导出为CSV文件,便于查看""" if not books_list: print("书架为空或获取失败。") return None os.makedirs(output_dir, exist_ok=True) processed_books = [] for book in books_list: processed_book = { '书名': book.get('title', ''), '作者': book.get('author', ''), '封面URL': book.get('cover', ''), '书籍ID': book.get('bookId', ''), '最后阅读时间': book.get('lastReadTime', ''), '是否付费': '是' if book.get('isPaid') else '否', } processed_books.append(processed_book) df = pd.DataFrame(processed_books) filepath = os.path.join(output_dir, '我的微信读书书架.csv') df.to_csv(filepath, index=False, encoding='utf-8-sig') # utf-8-sig解决Excel打开中文乱码 print(f"书架列表已导出至:{filepath}") return filepath关键点解析:
- 数据清洗:原始API返回的笔记数据嵌套可能较深。
processed_notes循环的目的就是提取我们关心的核心字段(章节、划线内容、想法等),将其扁平化,方便放入表格。 - 时间格式转换:API返回的时间往往是Unix时间戳(秒级)。
pd.to_datetime(unit='s')可以将其转换为可读的日期时间字符串。 - 文件名安全处理:
safe_title这行代码移除了书名中可能对文件系统不友好的字符(如/ \ : * ? " < > |),避免保存文件时出错。 - 格式美化:这是使用
openpyxl的直接价值。我们不仅导出数据,还调整了列宽、设置了自动换行(对于长文本的笔记至关重要)、加粗了标题行,让生成的Excel文件开箱即用,体验更佳。 - 编码问题:导出CSV时使用
utf-8-sig编码,可以确保用Excel直接打开时,中文字符正常显示,无需手动选择编码。
5. 主程序串联与使用流程
现在,我们将各个模块组合起来,形成一个完整的、用户友好的脚本。在main.py中:
# main.py import argparse from wechat_read_api import WeReadClient from data_processor import DataExporter import json def main(): parser = argparse.ArgumentParser(description='微信读书笔记一键导出工具') parser.add_argument('--book-id', type=str, help='指定要导出笔记的书籍ID,如不指定则导出书架列表') parser.add_argument('--all-notes', action='store_true', help='导出书架中所有书籍的笔记(慎用,可能请求过多)') parser.add_argument('--output', type=str, default='output', help='导出文件存放目录,默认为当前目录下的output文件夹') args = parser.parse_args() client = WeReadClient() exporter = DataExporter() # 测试连接和登录状态 try: # 尝试获取书架,验证凭证是否有效 bookshelf = client.get_bookshelf() if not bookshelf or 'books' not in bookshelf: print("错误:无法获取书架信息,请检查Cookie/Token配置是否正确且未过期。") return print("登录状态验证成功!") except Exception as e: print(f"初始化客户端失败:{e}") return if args.book_id: # 模式1:导出指定单本书的笔记 print(f"开始处理书籍ID: {args.book_id}") book_detail = client.get_book_detail(args.book_id) if not book_detail: print(f"未找到书籍ID为 {args.book_id} 的详细信息。") return notes = client.get_book_notes(args.book_id) exporter.export_notes_to_excel(notes, book_detail, args.output) elif args.all_notes: # 模式2:导出书架所有书的笔记(警告:可能耗时较长) confirm = input("警告:此操作将尝试导出书架所有书籍的笔记,可能产生大量请求。是否继续?(y/N): ") if confirm.lower() != 'y': print("操作已取消。") return books = bookshelf.get('books', []) print(f"检测到 {len(books)} 本书,开始批量导出...") for idx, book in enumerate(books): book_id = book.get('bookId') book_title = book.get('title', f'未知书籍{idx+1}') print(f"[{idx+1}/{len(books)}] 正在处理《{book_title}》...") notes = client.get_book_notes(book_id) if notes: exporter.export_notes_to_excel(notes, book, args.output) else: print(f" 《{book_title}》暂无笔记,跳过。") # 每处理完一本书,稍作停顿 import time time.sleep(1) print("批量导出完成!") else: # 模式3:默认行为,仅导出书架列表 print("未指定书籍ID,默认导出书架列表。") exporter.export_bookshelf_to_csv(bookshelf.get('books', []), args.output) print("请查看生成的CSV文件中的‘书籍ID’,然后使用 --book-id 参数导出特定书籍的笔记。") if __name__ == '__main__': main()使用方式:
- 获取Cookie/Token:按前述方法,从微信读书网页版获取
wr_vid等Cookie值,填入config.py。 - 导出书架列表:在命令行运行
python main.py。程序会验证登录状态,然后生成一个我的微信读书书架.csv文件,里面包含所有书的ID和基本信息。 - 导出单本书笔记:从CSV中找到你想导出的那本书的
书籍ID,运行python main.py --book-id [书籍ID]。程序会生成一个以书名命名的Excel文件。 - (谨慎)批量导出:运行
python main.py --all-notes,程序会询问确认后,遍历书架上的书并逐一导出笔记。
6. 常见问题、排查技巧与进阶优化
在实际操作中,你几乎一定会遇到一些问题。下面是我在开发和多次使用中踩过的坑和总结的经验。
6.1 常见问题速查表
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
运行脚本立即报错KeyError或无法导入config | config.py文件不存在或格式错误 | 1. 确认项目根目录下有config.py文件。2. 检查 config.py中变量名是否与代码中调用的一致(USER_COOKIE)。3. 确认Cookie值字符串被正确引号包裹。 |
| 提示“登录状态验证失败”或返回空数据 | Cookie/Token已过期或无效 | 1.这是最常见的问题。Cookie(特别是wr_vid)有有效期。重新登录微信读书网页版,在开发者工具中复制新的Cookie值替换config.py中的旧值。2. 检查复制的Cookie是否完整,包含了 wr_vid和wr_skey(如果有)。3. 尝试使用完整的请求头,包括 Referer等字段,模拟得更像真实请求。 |
| 能获取书架,但获取笔记时返回空数组或错误 | 1. 书籍ID不正确。 2. 接口地址或参数已更新。 3. 该书籍确实没有笔记。 | 1. 确认书籍ID来自有效的接口(如书架接口),而非肉眼猜测。 2.进行抓包对比。手动在网页版打开那本书的笔记页面,抓取真实的笔记接口请求,与代码中的 endpoint和params进行对比,更新代码。3. 在网页版确认该书是否有笔记。 |
| 导出Excel文件打开乱码 | CSV文件编码问题 | 确保使用utf-8-sig编码写入CSV文件(代码已处理)。Excel文件乱码较少见,如遇到可尝试用WPS或文本编辑器打开检查。 |
| 请求一段时间后突然失败,返回异常响应 | IP或账号被临时限制 | 1.最重要的原则:礼貌爬取。在循环请求中加入time.sleep(),模拟人工操作间隔。我设置的0.5-1秒是比较安全的。2. 不要短时间内高频请求所有书籍。批量导出( --all-notes)功能请谨慎使用,最好分批次进行。3. 如果被限制,等待几个小时或第二天再试。 |
| 笔记内容不全,只导出了一部分 | 接口是分页的,代码分页逻辑有误 | 检查get_book_notes方法中的分页逻辑。根据抓包结果,确认接口返回数据中是否有has_more、total等字段来判断是否还有下一页,并调整循环终止条件。 |
6.2 进阶优化与扩展思路
一个基础可用的工具已经完成,但我们可以让它更强大、更智能:
导出为Markdown:对于使用Obsidian、思源笔记等Markdown编辑器的用户,将笔记导出为
.md文件可能更直接。可以扩展DataExporter类,增加一个export_notes_to_markdown方法,按照“书名.md”生成文件,每条笔记以引用的格式> 划线内容和段落我的想法:...来组织。集成到工作流:使用
schedule库或系统级的cron/Task Scheduler,让脚本每周自动运行一次,实现笔记的定期自动备份。增加图形界面:使用
PyQt、Tkinter或更现代的Flet框架,为脚本包装一个简单的GUI。用户可以直接输入Cookie,从下拉列表选择书籍,点击按钮导出,体验更友好。数据去重与合并:如果你多次导出同一本书,笔记可能会有重复。可以在导出前,根据“位置”或“标记内容”对
DataFrame进行去重。解析章节结构:更高级的玩法是,不仅导出笔记,还尝试获取书籍的目录结构(章/节),并将笔记归属到具体的章节标题下,生成一个带层级结构的读书笔记大纲。
这个项目的代码虽然不长,但完整走通了一个“分析-获取-处理-输出”的爬虫实战流程。它解决了一个真实的需求,过程中涉及的接口分析、状态维持、数据处理、异常处理等知识点,都是爬虫乃至后端开发中的通用技能。希望这份详细的拆解,不仅能帮你导出笔记,更能让你理解背后每一步的“所以然”。
本文还有配套的精品资源,点击获取