资讯详情

资讯详情

Python爬虫实战:一键导出微信读书书籍与笔记

简介这份资源是一套用 Python 编写的微信读书书籍与笔记导出工具面向有基础 Python 使用经验、希望把个人书架和读书笔记批量保存到本地的读者可用于个人知识库整理、笔记归档与二次分析等场景。压缩包共 8 个文件约 277KB以 3 个 py 脚本为核心分别承担爬取逻辑、Excel 处理与 PyQt 图形界面入口另含 2 个 txt 依赖与说明文件、2 张 png 界面截图和 1 个 md 说明文档结构紧凑、便于快速上手。目前已有 2180 人学习下载。代码附带依赖安装与运行说明读者可据此完成环境配置并启动图形界面将微信读书中的书籍信息与笔记导出为可编辑表格同时可参考脚本中的请求与解析思路理解爬虫与 GUI 结合的实现方式并自行调试适配个人账号环境。资源仅供个人学习交流请勿用于商业用途。1. 一键导出微信读书的书籍和笔记为什么我最后放弃了截图存档微信读书里的划线、想法和书评攒到几百条之后靠手动复制粘贴基本等于自虐。我最早的做法是每本书开一个备忘录读到金句就切出去粘贴一次坚持了不到两周就放弃了——切屏打断心流是小事真正要命的是笔记散落在不同 App 里想按书检索、想导出成 Markdown 归档完全无从下手。后来我换了个思路既然阅读数据都在账号里那就用 Python 写个爬虫把书架、划线、想法、书评一次性拉下来落成结构化文件。这篇笔记讲的就是这条路怎么走通包括登录态怎么拿、接口怎么找、翻页怎么处理、频率怎么控。适合有 Python 基础、想给自己做一套个人阅读归档的开发者也适合想练手接口分析的新手。需要提前说清楚只导出自己账号下的数据别碰别人的也别拿去批量分发。2. 先搞清楚数据从哪来微信读书的接口结构与登录态动手写代码之前得先弄明白一件事你要的数据到底藏在哪个请求里。很多人一上来就打开开发者工具乱翻翻半天找不到划线列表最后怀疑是不是加密了。其实微信读书 Web 端的数据流并不复杂关键是分清「页面渲染数据」和「接口返回数据」这两类前者是 HTML 里塞的后者是 XHR 异步拉的你要的是后者。2.1 书架、划线、想法分别对应哪类请求打开 Web 端书架页按 F12 切到 Network 面板筛选 XHR刷新页面你会看到几个明显的请求。书架列表通常是一个带bookId数组的接口返回的是你账号下所有书的元信息包括书名、作者、封面、阅读进度。点进某本书的笔记页又会触发另一批请求划线underline和想法thought往往是分开返回的有的版本会合并成一个bookmarklist之类的接口。这里有个血泪经验不同端的接口路径不一样Web 端和 App 端的参数结构经常对不上你照着某篇老教程抄的 URL很可能早就失效了。所以别背接口学会自己抓。判断方法很简单在笔记页滚动加载看哪个请求的响应里出现了你刚划的那句话。找到之后把它的 Request URL、Method、Headers、Payload 全部复制出来这就是你的「数据源模板」。一般来说划线接口会带bookId、chapterUid之类的参数想法接口还会多一个分页游标。2.2 登录态怎么拿Cookie 与关键 Header 的取舍微信读书的接口基本都要登录态核心就是 Cookie。你登录之后浏览器里会存一串wr_skey、wr_vid之类的字段请求时带上就能通过校验。最省事的做法是直接从浏览器复制完整 Cookie 字符串塞进代码里跑。但这里有个坑Cookie 是有有效期的而且换设备、换网络环境可能触发重新验证所以别把它硬编码进仓库用环境变量或者本地配置文件存。除了 Cookie还有几个 Header 值得注意。User-Agent最好伪装成正常浏览器别用 requests 默认的否则容易被识别。Referer有时也校验填成对应的页面地址即可。至于Authorization之类的字段Web 端一般用不到App 端才常见。我一般会先把浏览器里那条成功请求的 Headers 原样抄下来跑通之后再逐个删减看哪些是必需的这样比盲猜高效得多。import os import requests # 从环境变量读取避免硬编码进代码仓库 COOKIE os.environ.get(WEREAD_COOKIE, ) BASE_HEADERS { Cookie: COOKIE, 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://weread.qq.com/, Accept: application/json, text/plain, */*, } def fetch_json(url, paramsNone): 统一封装 GET 请求方便后续替换成 POST resp requests.get(url, headersBASE_HEADERS, paramsparams, timeout10) resp.raise_for_status() return resp.json()这段代码做了三件事把 Cookie 从环境变量读进来避免泄露统一了请求头减少被拦截的概率封装了一个fetch_json后面所有接口调用都走它。参数说明上timeout10是防止某个请求卡死拖垮整个脚本raise_for_status让你在状态码异常时立刻发现而不是拿到一堆空数据还以为是解析问题。实际跑的时候如果返回 401 或 403八成是 Cookie 过期了重新复制一份即可。3. 把书架和笔记拉下来分页、解析与落盘拿到接口模板之后下一步就是把它变成能批量跑的脚本。这里分三块先拉书架拿到所有书的 ID再针对每本书拉划线和想法最后把数据落成 Markdown 或 JSON。听起来线性但真正写起来分页和字段解析是最容易翻车的地方。3.1 书架列表的翻页逻辑与字段提取书架接口一般不会一次性返回所有书尤其是书多的时候会有分页或者滚动加载。常见做法是带一个offset或lastBookId参数每次返回一批直到返回空列表为止。我一般会写一个循环把每页的结果累加同时加一个最大页数保护防止接口异常时死循环。def fetch_bookshelf(): 循环拉取书架直到没有更多数据 books [] offset 0 while True: data fetch_json( https://weread.qq.com/api/user/bookshelf, params{offset: offset, limit: 20}, ) batch data.get(books, []) if not batch: break for item in batch: books.append({ bookId: item.get(bookId), title: item.get(title), author: item.get(author), progress: item.get(readingProgress, 0), }) offset len(batch) if offset 2000: # 保护阈值按自己书量调整 break return books逻辑上每次请求拿 20 本拿到空列表就停。offset累加的是实际返回数量而不是固定步长这样即使接口返回数量有波动也不会漏。字段提取时只保留你需要的别把整个响应塞进内存书多了会很占资源。progress字段有的版本叫readingProgress有的叫finishReading以你实际抓到的为准别照抄。3.2 划线、想法、书评的解析与 Markdown 落盘拿到 bookId 之后针对每本书调笔记接口。划线通常返回一个数组每条包含章节名、划线原文、位置信息想法则多一个content字段和创建时间。解析的时候要注意有些字段是嵌套的比如章节信息可能在chapter对象里别直接取顶层。落盘我习惯用 Markdown一本书一个文件划线用引用块想法用普通段落加时间戳这样在 Obsidian 或 Typora 里直接能看。def export_book_notes(book): 导出单本书的划线和想法为 Markdown data fetch_json( https://weread.qq.com/api/book/bookmarklist, params{bookId: book[bookId]}, ) lines [f# {book[title]}\n, f作者{book[author]}\n] for bm in data.get(updated, []): chapter bm.get(chapterName, 未知章节) text bm.get(markText, ).strip() if not text: continue lines.append(f## {chapter}\n) lines.append(f {text}\n) # 想法可能挂在同一条记录下 for thought in bm.get(thoughts, []): lines.append(f- {thought.get(content, )}\n) filename fnotes/{book[bookId]}.md with open(filename, w, encodingutf-8) as f: f.write(\n.join(lines)) return filename这里的关键是字段容错markText可能为空thoughts可能不存在所以用get加默认值避免 KeyError 直接崩掉。章节名有的接口返回chapterName有的返回chapterTitle跑之前先打印一条完整响应确认。落盘路径建议单独建目录文件名用 bookId 而不是书名避免书名里有特殊字符导致写入失败。3.3 用并发把几百本书的导出时间压下来串行跑几百本书每本一次请求加上网络延迟可能要十几分钟。这时候可以用concurrent.futures开几个线程并发拉取。但注意别开太多微信读书的接口对频率敏感线程数控制在 3 到 5 比较稳妥再高容易触发限流。from concurrent.futures import ThreadPoolExecutor, as_completed def export_all(books): 并发导出控制线程数避免触发限流 results [] with ThreadPoolExecutor(max_workers3) as pool: futures {pool.submit(export_book_notes, b): b for b in books} for future in as_completed(futures): book futures[future] try: path future.result() results.append(path) print(f完成{book[title]} - {path}) except Exception as e: print(f失败{book[title]}原因{e}) return resultsmax_workers3是经验值你可以从 2 开始试观察有没有返回 429 或空数据。每个任务独立捕获异常避免一本书失败导致整批中断。打印进度是为了长任务时心里有数不然跑半天不知道卡在哪。4. 避坑与排查导出微信读书笔记时最容易翻车的五件事这一章是我踩过的坑合集每一条都按「现象 → 原因 → 解决」写你遇到问题时可以直接对号入座。现象一请求返回 200但数据是空的。原因通常是 Cookie 过期或者缺少关键 Header接口没报错但返回了空结构。解决方法是先打印完整响应确认是不是{updated: []}这种如果是重新复制 Cookie并检查Referer是否填对。现象二翻页翻到一半开始重复。原因是offset用了固定步长而接口实际返回数量小于步长导致偏移错位。解决方法是每次用实际返回的len(batch)累加而不是写死 20。现象三划线内容里出现 HTML 标签或转义字符。原因是接口返回的是富文本片段直接写进 Markdown 会显示异常。解决方法是做一次简单清洗把br换成换行把nbsp;换成空格或者用html.unescape处理。现象四跑几十本之后开始大量失败。原因是并发过高或请求间隔太短触发了频率限制。解决方法是把线程数降到 2 到 3并在每次请求之间加time.sleep(0.5)到1秒的随机延迟。现象五书名里有斜杠或冒号写文件时报错。原因是文件名非法字符。解决方法是文件名统一用 bookId书名只写在文件内容里或者用正则把非法字符替换掉。提示调试阶段建议只跑一本书把完整响应打印出来看结构确认字段名之后再批量跑。直接上全量出了问题很难定位。5. 进阶把导出结果接进自己的知识库跑通基础版之后你手里就有了一堆 Markdown 文件。但光导出还不够真正有价值的是让这些笔记能被检索、被关联。我现在的做法是导出时在文件头部加 YAML front matter写上书名、作者、导出时间、划线数量这样 Obsidian 的 Dataview 插件就能直接按作者或时间筛选。另外我会把同一本书的划线和想法按章节聚合而不是按接口返回顺序平铺读起来更接近原书结构。还有一个技巧是增量导出。每次全量跑太浪费可以在本地存一个last_export.json记录每本书上次导出的时间戳下次只拉更新时间晚于这个点的书。接口里一般有updateTime字段拿来比较即可。这样日常维护只需要几秒钟。import json import os from datetime import datetime STATE_FILE last_export.json def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE, encodingutf-8) as f: return json.load(f) return {} def save_state(state): with open(STATE_FILE, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2) def need_update(book, state): 对比本地记录与接口返回的更新时间 last state.get(book[bookId], {}).get(updateTime, 0) return book.get(updateTime, 0) last这段代码的核心是状态文件记录每本书最后一次导出的时间。need_update判断是否需要重新拉取避免无谓请求。注意updateTime的格式可能是秒级时间戳也可能是毫秒跑之前打印一条确认别直接比较。最后说个我自己的习惯导出脚本我从来不放在项目根目录裸跑而是包一层main.py把 Cookie 读取、目录创建、异常汇总都收进去跑完打印一份「成功多少本、失败多少本、跳过多少本」的简报。这样每次执行完一眼就知道有没有问题不用去翻日志。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →