news 2026/9/4 7:13:51

Python爬虫实战:微信读书笔记一键导出与个人知识库构建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫实战:微信读书笔记一键导出与个人知识库构建

简介:这是一套面向Python初学者与爬虫爱好者的微信读书数据导出工具,解决个人学习场景下书籍列表与阅读笔记难以批量保存的问题。资源包共8个文件,含3个核心Python脚本(GUI界面、主爬虫逻辑、Excel处理)、2个文本配置文件、2张界面演示图及1份Markdown说明文档,整体仅277KB,轻量易部署。已有2168人下载学习,适合希望快速上手网页数据抓取、理解登录模拟与API逆向分析的实践者。读者可直接运行pyqt_gui.py启动图形化界面,通过内置流程完成微信读书账号登录、书架拉取、笔记提取及Excel一键导出;配套requirement.txt确保依赖环境可复现,README详述调试要点与法律声明,强调仅限学习交流使用。

1. 项目缘起:为什么我们需要一个自己的“书库”?

作为一个重度阅读爱好者,我几乎把所有的碎片时间都泡在了微信读书上。它确实方便,书多,社区氛围也好,划线、写想法、看别人的批注,体验很流畅。但时间一长,问题就来了:我辛辛苦苦划下的几百条笔记、写下的几十条想法,全都“寄存”在微信读书的服务器里。哪天我想离线整理、想用其他笔记软件做深度关联、或者单纯就是想备份一份属于自己的数据时,就发现非常被动。平台提供的导出功能往往有限,要么格式单一,要么一次只能导出一本书,对于我这种笔记大户来说,效率太低。

更现实的一个场景是,很多书是有版权期限的,或者你读的是“无限卡”免费读的书,一旦下架或者你的会员到期,这些笔记就可能再也看不到了。那种感觉就像自己盖的房子,钥匙却在别人手里。所以,自己动手,把数据拿回来,建立一个本地备份,甚至是一个可以随时检索、编辑的个人知识库,就成了一个很实际的需求。这就是我动手写这个爬虫脚本最直接的动机:拿回数据的自主权

市面上有一些现成的工具或者浏览器插件,但要么已经失效(微信读书前端改个版可能就用不了了),要么功能不符合我的需求(比如我只想导出笔记,它却把整本书都下载了)。所以,最好的办法就是自己写。用Python爬虫来实现,灵活、可控,而且整个过程本身就是一次非常好的学习实践,你能彻底搞清楚微信读书的数据接口、认证机制以及如何优雅地处理反爬策略。今天,我就把这个经过多次迭代、相对稳定的一键导出脚本的实现思路、核心代码以及我踩过的所有坑,毫无保留地分享出来。

2. 核心思路拆解:我们到底在“爬”什么?

在动手写代码之前,我们必须先想明白,我们要从微信读书获取什么,以及它可能以什么形式提供。盲目地去抓取网页HTML是效率最低、也最容易被反爬虫机制拦截的方式。正确的方法是:模拟真实用户操作,直接请求其背后为手机App或网页端提供数据的JSON API接口

通过浏览器开发者工具(F12)的“网络”(Network)选项卡,我们在微信读书网页版进行操作(比如翻到某本书的笔记页面),可以清晰地看到一系列XHR(Fetch)请求。我们的目标就在这些请求里。经过分析,整个导出流程可以分解为以下几个核心环节:

  1. 身份认证:如何让服务器认为我们是合法的用户?这通常依赖于Cookie。我们需要先登录微信读书网页版,从浏览器中提取出关键的Cookie信息。
  2. 获取书籍列表:我们得知道自己的书架上有哪些书。有一个接口可以返回用户所有的图书列表,包含每本书的唯一ID(bookId)、书名、作者等信息。
  3. 获取单本书籍的笔记:这是最核心的一步。根据bookId,请求获取这本书下所有用户自己的划线(笔记)和想法(评论)。这个接口会返回一个结构化的JSON数据,包含了笔记所在的章节、文本内容、创建时间、位置等信息。
  4. 数据解析与存储:将获取到的JSON数据,解析成我们想要的格式,比如纯文本、Markdown或者CSV,然后保存到本地文件。

整个脚本的骨架就是围绕这四个步骤搭建的。下面,我们就一步步来看具体怎么实现,以及每个环节有哪些需要特别注意的“坑”。

3. 环境准备与核心工具选型

工欲善其事,必先利其器。这个项目对Python环境的要求并不高,核心是几个非常常用的库。

3.1 Python与库安装

建议使用Python 3.7及以上版本。我们需要安装的库主要有三个:

  • requests:用于发送HTTP请求,这是爬虫的基石。它比Python自带的urllib更简洁易用。
  • json:Python标准库,用于解析和生成JSON数据。
  • time:用于在请求间添加延时,避免请求过快被服务器限制。

安装命令非常简单:

pip install requests

其他两个是标准库,无需安装。

这里为什么不选用更“重型”的框架如Scrapy?因为我们的目标很明确,就是针对微信读书这一个特定站点的几个特定API。Scrapy更适合构建大规模、复杂的爬虫项目,用它来抓几个API接口,有点杀鸡用牛刀,还会引入不必要的复杂度。requests库轻量、直接,完全够用。

3.2 获取身份凭证(Cookie)

这是整个流程的钥匙,也是最容易出错的一步。微信读书目前主要依靠Cookie来维持登录状态。

操作步骤:

  1. 用Chrome、Edge或Firefox浏览器打开 微信读书网页版 。
  2. 使用微信扫码登录。
  3. 登录成功后,按F12打开开发者工具,切换到Network(网络)选项卡。
  4. 刷新页面,在网络请求列表中,找到任何一个指向weread.qq.com域名的请求(比如第一个document请求或任何一个api请求)。
  5. 点击该请求,在右侧的Headers(标头)标签页中,向下找到Request Headers(请求头)部分。
  6. 在其中找到Cookie这一行,后面那一长串字符串就是我们需要的东西。把它完整地复制下来。

重要提示:Cookie是个人隐私,包含了你的登录会话信息。千万不要将你的真实Cookie分享给他人或上传到公开的代码仓库(如GitHub)。我们后续在代码中会将其保存在本地的配置文件或环境变量中。

Cookie的格式:它看起来像wr_vid=xxx; wr_skey=yyy; wr_rid=zzz; ...这样。我们需要的是整个字符串。

4. 核心代码实现与逐行解析

接下来,我们进入核心的代码部分。我会把完整的脚本拆解开,逐一解释每个函数的作用和关键代码行。

4.1 脚本框架与配置

首先,我们引入必要的库,并定义一些配置。我强烈建议将Cookie等敏感信息放在代码之外。

import requests import json import time import os from typing import List, Dict, Optional # 配置区域 # 方式1:直接写死在代码里(不推荐,仅用于测试) # COOKIE = '你的完整Cookie字符串' # 方式2:从环境变量读取(推荐) COOKIE = os.getenv('WEREAD_COOKIE') if not COOKIE: # 方式3:从本地文件读取(次推荐) try: with open('weread_cookie.txt', 'r', encoding='utf-8') as f: COOKIE = f.read().strip() except FileNotFoundError: print("错误:未找到Cookie。请设置环境变量 WEREAD_COOKIE 或创建 weread_cookie.txt 文件。") exit(1) # 请求头,模拟浏览器 HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Connection': 'keep-alive', 'Cookie': COOKIE # 将Cookie放在请求头中 } # 基础URL BASE_URL = 'https://i.weread.qq.com'

代码解读

  • 我们提供了三种设置Cookie的方式,优先级是:环境变量 > 本地文件 > 代码硬编码。这是工程上的好习惯,保证安全性和灵活性。
  • HEADERS字典模拟了一个普通Chrome浏览器的请求头,其中最关键的一行就是‘Cookie’: COOKIE。服务器就是通过这个字段来识别我们的身份。
  • BASE_URL是微信读书主要API接口的域名。

4.2 获取所有书籍列表

我们需要先知道要导出哪些书。

def get_book_list() -> List[Dict]: """获取用户书架上的所有书籍列表""" url = f"{BASE_URL}/user/books" # 添加一个时间戳参数,防止缓存 params = {'vendor': 'web', 'synckey': 0, 'lectureSynckey': 0, '_': int(time.time() * 1000)} try: response = requests.get(url, headers=HEADERS, params=params, timeout=10) response.raise_for_status() # 如果状态码不是200,抛出HTTPError异常 data = response.json() # 调试:可以打印一下data的结构,方便理解 # print(json.dumps(data, indent=2, ensure_ascii=False)) if data and 'books' in data: books = data['books'] print(f"成功获取到 {len(books)} 本书籍。") # 返回一个简单的书籍信息列表 book_list = [] for book in books: book_info = { 'bookId': book.get('bookId'), 'title': book.get('title'), 'author': book.get('author'), 'cover': book.get('cover') } book_list.append(book_info) return book_list else: print("获取书籍列表失败,响应数据格式异常。") return [] except requests.exceptions.RequestException as e: print(f"网络请求失败: {e}") return [] except json.JSONDecodeError as e: print(f"解析JSON响应失败: {e}") return []

代码解读

  • 接口地址是/user/booksparams里的参数是我通过观察网络请求总结的,其中_参数是一个当前毫秒级时间戳,常用于避免浏览器缓存。
  • response.raise_for_status()是一个好习惯,它能帮我们快速发现401(未授权)、404(未找到)等HTTP错误。
  • response.json()直接将响应内容解析为Python字典或列表。
  • 返回的data[‘books’]是一个列表,里面包含了每本书的详细信息。我们只提取出最关键的bookIdtitleauthor等字段,构造一个新的简洁列表返回。
  • 完整的异常处理 (try…except) 是爬虫脚本健壮性的关键。网络可能不稳定,接口可能变化,良好的错误处理能让脚本在遇到问题时给出明确提示,而不是直接崩溃。

4.3 获取单本书的笔记详情

这是最核心的函数,负责获取一本书里所有的划线和想法。

def get_book_notes(book_id: str) -> Optional[Dict]: """根据bookId获取指定书籍的全部笔记(划线和想法)""" url = f"{BASE_URL}/book/bookmarklist" params = { 'bookId': book_id, 'chapterId': 0, # 0 表示获取全部章节的笔记 'synckey': 0, '_': int(time.time() * 1000) } try: response = requests.get(url, headers=HEADERS, params=params, timeout=15) response.raise_for_status() data = response.json() # 检查是否包含有效数据 if not data or 'updated' not in data: print(f"书籍 {book_id} 可能没有笔记或接口返回空。") return None # 笔记数据通常在 `updated` 字段下,是一个列表 notes = data.get('updated', []) print(f"书籍 {book_id} 找到 {len(notes)} 条笔记。") return {'bookId': book_id, 'notes': notes} except requests.exceptions.RequestException as e: print(f"获取书籍 {book_id} 笔记失败,网络错误: {e}") return None except json.JSONDecodeError as e: print(f"获取书籍 {book_id} 笔记失败,JSON解析错误: {e}") return None

代码解读

  • 接口地址是/book/bookmarklist。关键参数是bookIdchapterId(0代表全部章节)。
  • 返回的data[‘updated’]是一个列表,每一条笔记是一个字典。这个字典结构非常丰富,通常包含:
    • markText: 划线的文本内容。
    • content: 如果是想法,这里就是想法内容。
    • chapterTitle: 笔记所在的章节标题。
    • range: 笔记在章节中的位置范围。
    • createTime: 创建时间戳。
    • style: 划线样式(0-普通,1-重点)。
  • 函数返回一个字典,包含bookIdnotes列表,方便后续处理。

4.4 数据解析与导出为Markdown

获取到原始数据后,我们需要将其转换成易读、易用的格式。Markdown是一个非常好的选择,因为它结构清晰,兼容性强,可以直接导入到Obsidian、Notion、Typora等大多数笔记软件中。

def parse_notes_to_markdown(notes_data: Dict) -> str: """将笔记数据解析为Markdown格式的字符串""" if not notes_data or 'notes' not in notes_data: return "" book_id = notes_data.get('bookId', '未知') notes = notes_data['notes'] if not notes: return f"# 书籍ID: {book_id}\n\n> 本书暂无笔记。\n" # 我们可以从第一条笔记里获取书名(如果接口返回了的话) # 但更可靠的是在调用此函数时传入书名,这里我们先假设没有 md_content = f"# 微信读书笔记导出\n\n**书籍ID:** {book_id}\n**笔记总数:** {len(notes)}\n**导出时间:** {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n---\n\n" # 按章节对笔记进行分组,这样结构更清晰 notes_by_chapter = {} for note in notes: chapter_title = note.get('chapterTitle', '未分类章节') if chapter_title not in notes_by_chapter: notes_by_chapter[chapter_title] = [] notes_by_chapter[chapter_title].append(note) # 按章节生成Markdown for chapter_title, chapter_notes in notes_by_chapter.items(): md_content += f"## {chapter_title}\n\n" # 对该章节的笔记按位置(range)排序,使其保持阅读顺序 chapter_notes.sort(key=lambda x: x.get('range', '')) for note in chapter_notes: mark_text = note.get('markText', '').strip() content = note.get('content', '').strip() abstract = note.get('abstract', '').strip() style = note.get('style', 0) # 处理划线 if mark_text: # 根据划线样式添加不同格式 if style == 1: md_content += f"> **{mark_text}**\n\n" else: md_content += f"> {mark_text}\n\n" # 处理想法(评论) if content: md_content += f"**我的想法:** {content}\n\n" elif abstract: # 有时想法会在abstract字段 md_content += f"**我的想法:** {abstract}\n\n" # 可以添加一个小的分隔符,让每条笔记更清晰 md_content += "---\n\n" return md_content

代码解读

  • 这个函数做了几件重要的事:分组(按章节)、排序(按书中位置)、格式化
  • 按章节分组能让导出的笔记结构化和原书保持一致,复习时更有上下文。
  • range字段排序保证了笔记在章节内的顺序就是你在书中阅读的顺序。
  • 对于style=1的笔记(重点划线),我们将其加粗,在视觉上予以区分。
  • 每条笔记之间用---分隔,提高可读性。

4.5 主流程:一键导出的逻辑

现在我们把所有功能串联起来,形成“一键导出”的主函数。

def export_all_notes(output_dir='weread_notes'): """主函数:导出所有书籍的笔记到指定目录""" # 1. 创建输出目录 if not os.path.exists(output_dir): os.makedirs(output_dir) print(f"创建输出目录: {output_dir}") # 2. 获取书籍列表 print("正在获取书籍列表...") books = get_book_list() if not books: print("未获取到任何书籍,请检查Cookie是否有效。") return # 3. 遍历每本书,获取并导出笔记 total_books = len(books) for idx, book in enumerate(books, 1): book_id = book['bookId'] book_title = book.get('title', f'未知标题_{book_id}')[:50] # 书名可能很长,截取一下 print(f"\n[{idx}/{total_books}] 正在处理: 《{book_title}》 ({book_id})") # 3.1 获取笔记数据 notes_data = get_book_notes(book_id) if not notes_data: print(f" -> 跳过,未获取到笔记数据。") continue # 3.2 解析为Markdown md_text = parse_notes_to_markdown(notes_data) # 3.3 保存文件。文件名使用书名和ID,避免特殊字符问题 safe_title = "".join([c for c in book_title if c.isalnum() or c in (' ', '_', '-')]).rstrip() filename = f"{safe_title}_{book_id}.md" filepath = os.path.join(output_dir, filename) try: with open(filepath, 'w', encoding='utf-8') as f: f.write(md_text) print(f" -> 已导出至: {filepath}") except IOError as e: print(f" -> 文件保存失败: {e}") # 3.4 礼貌性延时,避免请求过快 time.sleep(1) print(f"\n导出完成!所有笔记已保存至 '{output_dir}' 目录。") if __name__ == '__main__': export_all_notes()

代码解读

  • 主函数export_all_notes定义了完整的流水线:创建目录 -> 获取书单 -> 遍历每本书 -> 获取笔记 -> 解析保存 -> 延时。
  • 在文件名处理上,我们过滤掉了非字母数字的字符,防止因书名包含\/:*?"<>|等非法字符导致无法创建文件。
  • time.sleep(1)是至关重要的“礼貌性延时”。虽然微信读书的API没有非常严格的反爬,但连续、高频地请求服务器是不好的行为,可能导致你的IP或账号被临时限制。加上1秒间隔,既能保证效率,也显得更“像人”一些。
  • 使用if __name__ == ‘__main__’:是标准做法,使得这个脚本既可以作为模块被导入,也可以直接运行。

5. 实战中的坑与进阶优化

把上面的代码跑起来,你基本就能导出笔记了。但根据我的实战经验,还有以下几个关键点和潜在问题需要特别注意。

5.1 Cookie失效与更新

Cookie不是永久有效的。微信读书的登录会话有一定有效期,可能几天,也可能几周。当你的脚本突然报错401 Unauthorized或返回的数据为空时,第一反应就应该是:Cookie失效了

解决方案:重新登录微信读书网页版,按第3.2节的步骤,重新获取一次Cookie,并更新你的weread_cookie.txt文件或环境变量。

5.2 接口变更与适配

这是爬虫项目永恒的挑战。微信读书的API接口并非公开协议,它可能随时变更路径、参数或返回的数据结构。如果你的某天脚本突然不能用了,除了检查Cookie,就要用开发者工具重新抓包,看看get_book_listget_book_notes对应的接口地址和参数是否发生了变化。

如何应对:养成观察网络请求的习惯。脚本的核心其实就是对这两个接口的封装。一旦失效,就重新抓取正确的接口信息,然后更新代码中的urlparams

5.3 笔记数据不完整或格式异常

有时你会发现,导出的笔记比App里看到的少,或者想法内容不见了。这可能是因为:

  1. 分页:如果一本书的笔记非常多,接口可能采用了分页机制。我们上面的代码只请求了第一页。需要检查接口返回是否有hasMorenext之类的字段,并实现翻页逻辑。
  2. 字段名变化:笔记的正文可能在markText,也可能在contentabstract。想法内容也可能在不同字段。我们的parse_notes_to_markdown函数已经做了一些兼容处理,但可能需要根据实际情况调整。

5.4 增加导出格式选项

Markdown虽好,但有人可能更喜欢CSV(方便用Excel分析)或纯文本。我们可以很容易地扩展脚本。

def export_notes(notes_data: Dict, format='markdown', output_dir='.'): """根据格式导出笔记""" if format == 'markdown': content = parse_notes_to_markdown(notes_data) ext = '.md' elif format == 'csv': content = parse_notes_to_csv(notes_data) # 需要实现这个函数 ext = '.csv' elif format == 'txt': content = parse_notes_to_txt(notes_data) # 需要实现这个函数 ext = '.txt' else: raise ValueError(f"不支持的格式: {format}") # ... 保存文件逻辑

5.5 增量导出与定时任务

我们不需要每次都导出全部书籍。可以记录上次导出的时间,或者每本书最后笔记的更新时间 (createTime),然后只获取新的笔记。这需要将已导出的书籍ID和最后同步时间持久化存储(比如在一个JSON文件里)。然后,在get_book_listget_book_notes时,带上时间参数,只请求特定时间之后的更新。这能大大减少请求量,实现真正的“同步”。

更进一步,你可以结合系统的定时任务(如Linux的cron,Windows的任务计划程序),让这个脚本每天或每周自动运行一次,实现笔记的自动备份。

6. 安全、合规与伦理边界

在享受技术带来的便利时,我们必须清醒地认识到边界在哪里。

  1. 尊重版权与用户协议:我们导出的笔记,是自己创作的划线和个人想法。这个脚本的用途应严格限定于个人数据备份与管理绝对禁止用于批量下载书籍全文、盗版传播或任何侵犯版权的行为。微信读书的用户协议通常禁止自动化程序访问,我们的行为处于灰色地带,因此更要克制,仅满足个人合理使用需求。
  2. 控制请求频率:务必在代码中设置合理的延时 (time.sleep)。不要试图并发大量请求去“轰炸”服务器,这既不道德,也极易导致你的IP或账号被封禁。我们的目的是拿回数据,不是攻击服务。
  3. 保护个人隐私:你的Cookie就是你的账号凭证。妥善保管weread_cookie.txt文件,不要将其上传至GitHub等公开平台。如果误上传了,应立即在平台上删除,并视为密码泄露,最好在微信读书上检查一下登录设备,必要时可以“下线”所有设备,让旧Cookie失效。
  4. 数据用途:导出的数据建议仅用于个人学习、研究和知识管理。不要将其用于商业用途或公开大量分发。

这个脚本是一个工具,它放大了你管理个人数据的能力,但如何使用它,取决于你的责任心。保持善意和克制,技术才能更好地为我们服务。

最后,完整的脚本代码已经贯穿在上述讲解中。你可以将它们组合到一个.py文件里,准备好你的Cookie,运行python weread_notes_exporter.py,就能在weread_notes文件夹下看到一本本以你命名的Markdown笔记文件了。这个过程,不仅是获取了笔记,更是你对自己数字资产的一次郑重声明。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 7:13:27

AI图像生成实战:用Mossland打造昭和暗黑风假面骑士全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 7:08:09

fpga实现简单的SPI主机模块

一 SPI介绍SPI&#xff08;Serial Peripheral Interface&#xff0c;串行外围设备接口&#xff09;通讯协议&#xff0c;是 Motorola 公司提出的一种同步串 行接口技术&#xff0c;是一种高速、全双工、同步通信总线&#xff0c;在芯片中只占用四根管脚用来控制及数据传输&…

作者头像 李华
网站建设 2026/9/4 7:07:07

智能家居与物联网入门:别只让灯闪,给设备做一套看得懂的状态反馈

智能家居与物联网入门:别只让灯闪,给设备做一套看得懂的状态反馈 [!NOTE] LED是最小的输出实验,却能暴露供电、限流、GPIO方向和程序时序的基础问题。本文只使用经典ESP32、一个普通LED和限流电阻,把“亮一下”升级成启动、正常与错误三种可识别状态。你会知道为什么电阻不…

作者头像 李华
网站建设 2026/9/4 7:06:44

Imx6ull内核开发环境搭建

虚拟机环境 搭建虚拟机环境 电脑型号&#xff1a; surface pro6 i5-8250U 8G内存256G硬盘 WIN11(先用官方镜像U盘重装家庭版&#xff0c;然后升级专业版)&#xff1a; 版本 Windows 11 专业版 版本号 25H2 安装日期 ‎2026/‎8/‎24 操作系统版本 26200.9168 功能包 W…

作者头像 李华
网站建设 2026/9/4 7:06:30

模拟IC版图验证实战:DRC/LVS常见错误排查与Calibre修复指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 7:04:14

嵌入式调试笔记<7>MODBUS协议详解与调试实战

嵌入式调试笔记 一、MAX13487芯片产生数据回环问题 二、Keil在线调试过程中遇到的问题 三、使用VScdoe开发STM32单片机 四、VANTPEK可调电源接负载电压被拉低 五、用CD74HC4067扩展16路ADC采集及两个调试坑 六、4档旋转开关省IO采集&#xff0c;与Modbus中float的拆分还原 七、…

作者头像 李华