ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python+微信小程序智能笔记实战:关键词抽取与自动摘要

Python+微信小程序智能笔记实战:关键词抽取与自动摘要 如果你和我一样平时会在微信里随手记点想法、读书摘抄和工作要点大概率也遇到过同一个烦恼记的时候很爽找的时候就傻眼了。我最近基于 Python 和微信小程序做了个智能笔记核心思路一句话小程序负责快速记录和展示Python 后端负责把碎片化内容自动整理成带关键词、能摘要、可全文搜索的结构化数据。项目从想法到跑通花了两三周踩了不少坑也沉淀了一些经验。这篇文章不写空泛的概念直接把需求和选型、关键词和摘要的落地代码、小程序端的翻页与搜索交互、以及真机联调和体验版的完整链路从头捋一遍。适合谁看呢想用 Python 给小程序做后端能力的开发者、想做个人知识库工具的业余玩家以及正卡在“开发环境调通、一上真机就出问题”阶段的朋友。1. 为什么把“智能”放在 Python 后端而不是塞进小程序1.1 笔记应用里的“智能”到底指哪几件事很多人一听到“智能笔记”第一反应就是上大模型。但我实际做下来觉得个人笔记这个场景真正的痛点不是“生成”而是“召回”——你记了一百条散装笔记能不能在需要的时候快速找到对的那一条。所以我把“智能”拆成了这么几个具体能力自动打标签碎片笔记不需要手动归类系统根据内容自动归入“工作”“学习”“Python”“阅读”等分类。关键词抽取列表页不用翻正文扫一眼关键词就知道这篇笔记讲了什么。自动摘要长文笔记在列表里不能只从头截取需要用算法把真正有信息量的句子捞出来。全文搜索搜“FastAPI”能把所有相关笔记都找出来而不是只靠标题匹配。相似笔记关联这个我放在二期不阻塞主体功能。把这些需求翻译成技术就是一个典型的 NLP 小项目分词、停用词过滤、TF-IDF / TextRank 抽取、标签规则引擎、全文检索。这些东西如果全塞进小程序端基本是折磨自己——小程序代码包有体积限制开发者工具里的 JavaScript 环境跑分词词典都有点吃力更别提换模型了。所以第一版架构就直接定了Python 做后端小程序只做前端的壳。1.2 技术选型对比为什么是 Python 后端 原生小程序我先对比了几条路线下面的表格是我当时做的选型记录实现方案优点缺点我的结论纯小程序端实现全部功能部署简单不用租服务器分词性能差、代码包体积受限、算法升级要发版不推荐Python 后端 原生小程序NLP 生态成熟、算法可随时升级、小程序保持轻量需要服务器、域名备案和 HTTPS最终采用App体验完整、能力不受限安装成本高、分发难不适合个人工具H5开发快、更新方便入口不够轻、微信原生能力弱只作为兜底Python 的优势不用多说生态里有 jieba、scikit-learn、FastAPI 这些东西一个人开发的时候效率很重要。Java 或者 Go 写接口也不难但 NLP 这一块明显还是 Python 顺手。Web 框架我在 FastAPI 和 Flask 之间犹豫过最后选了 FastAPI。原因是 FastAPI 自带 OpenAPI 文档联调的时候直接打开/docs页面就能测接口Pydantic 做参数校验也省了我不少事异步支持让后面接入后台任务变得很干净。如果你的项目只有三五个接口用 Flask 也行但一旦涉及请求体校验和异步任务FastAPI 的体验会好很多。1.3 链路设计与分层一次保存请求的完整旅途整个系统的调用链路大致是这样的小程序页面发起请求 → Nginx 反向代理HTTPS→ FastAPI 接口层 → 文本清洗与分词 → 关键词和摘要抽取 → 标签规则引擎 → 数据库写入 → 返回结构化 JSON。我在设计的时候特意把“NLP 处理”和“接口返回”解耦。用户点保存笔记后端先把笔记基础数据写入数据库返回一个“已接收”的状态然后后台异步跑分词和摘要处理完再回填到记录里。如果同步去跑第一次加载 jieba 词典就要一秒钟左右用户会明显感觉到卡顿。还有一个好处是只要接口协议稳定同一个后端可以服务多条业务线——小程序只是入口未来如果想加一个每周笔记邮件推送或者做一个 Web 端访问入口后端基本不用重写。2. 智能笔记的“智能”部分从原始文本到结构化信息需求捋清楚之后下一步就是把“智能”拆成能落地的代码。这一块是整个项目里最有意思的部分。先说一下环境准备。如果你还在装 Python 的阶段就去官网下载 3.10 以上的安装包装的时候记得勾选“Add Python to PATH”。装好之后在终端敲python --version能看到版本号就说明环境通了。项目依赖只需要几个库pip install jieba fastapi uvicorn2.1 先清理再分词脏文本会毁掉所有统计我见过不少新手直接拿原始文本做分词结果词频统计里全是被截断的 URL、乱七八糟的符号和无意义的虚词。笔记文本还有一个特点就是从微信复制过来的内容经常带一堆换行、空格和特殊符号不清洗的话后面所有统计都会偏。所以清洗这一步比算法本身更重要。我的清洗逻辑大概是这样的import re import jieba STOPWORDS set() def load_stopwords(pathstopwords.txt): 加载中文停用词表 with open(path, encodingutf-8) as f: for line in f: word line.strip() if word: STOPWORDS.add(word) def clean_text(raw: str) - str: # 去掉 URL、话题标签 text re.sub(rhttps?://\S, , raw) text re.sub(r#\S, , text) # 压缩连续空白 text re.sub(r\s, , text) text text.strip(。、,.!? ) return text def tokenize(text: str) - list[str]: return [ w for w in jieba.lcut(text) if w.strip() and len(w) 1 and w not in STOPWORDS ]停用词表建议自己准备一份中文的。网上搜“中文停用词表”能找到但我实际用下来还要自己过滤掉“我们”“就是”“一个”“这种”这类口语词因为个人笔记里这类词出现的频率特别高不滤掉关键词就会被它们占满。这里有一个特别容易被忽略的坑jieba 对计算机领域的新词识别不准。比如“大模型”默认可能被切成“大/模型”对 AI 主题笔记来说这种错切会直接影响标签和摘要质量。解决办法是准备自定义词典大模型 50 n 私有化部署 30 nz RAG 20 nz Fine-tuning 20 nz然后在代码里加载jieba.load_userdict(userdict.txt)这个自定义词典一定要在分词之前配置好否则后面所有关键词结果都会偏离再回头排查的时候很难意识到是分词器的问题。2.2 关键词和摘要TF-IDF 与 TextRank 双管齐下关键词抽取我直接用 jieba 自带的analyse.extract_tags它本质上是 TF-IDF 的实现。核心思想用大白话说就是如果一个词在你这一篇笔记里出现得多但在你所有笔记里很少出现那它就很有可能是这篇笔记的主题词。就像一群人聊天只有你这儿反复提“FastAPI”那这次聊天十有八九跟 FastAPI 有关。摘要我用了jieba.analyse.textrankTextRank 算法类似于网页排名。把句子看成节点句子之间如果共用了某些关键词就建立一条边然后迭代计算每个句子的得分最后取分数最高的几个句子作为摘要。实际使用代码很短from jieba.analyse import extract_tags, textrank def extract_info(text: str, top_k: int 5): keywords extract_tags(text, topKtop_k, withWeightTrue) summary_sentences textrank(text, topK3) return keywords, summary_sentences拿一段真实示例文本跑一下大概是这个效果demo_text FastAPI是一个现代的Python Web框架基于Starlette和Pydantic构建 支持异步接口和自动生成API文档。我在智能笔记项目中使用FastAPI提供后端服务 同时结合jieba分词库实现了关键词抽取和自动摘要功能。 keywords, summaries extract_info(demo_text) print(keywords) # [(FastAPI, 0.356), (摘要, 0.233), (关键词, 0.201), (后端, 0.175), (分词, 0.152)] print(summaries) # [我在智能笔记项目中使用FastAPI提供后端服务同时结合jieba分词库实现了关键词抽取和自动摘要功能。]这里要提醒一下TF-IDF 里的 IDF 是整个语料库的统计值。如果笔记很少IDF 计算就不稳定。我的方案是把所有笔记合并成一个语料池定期重算一遍 IDF新笔记写入后会触发一次后台任务更新语料统计这样关键词抽取会随着笔记增多越来越准。2.3 自动标签规则保底、关键词补充标签是整个笔记列表里最直观的分类入口也是用户感知“智能”最强烈的一个点。我做自动标签用了两层策略规则层加关键词层。规则层本质是一组关键词映射表LABEL_RULES { 工作: [会议, 需求, 验收, 日报, 周报], 学习: [课程, 教程, 学习, 笔记], Python: [python, flask, fastapi, django, 爬虫, venv], 阅读: [阅读, 读完, 书摘, 章节], } def auto_labels(text: str, keywords: list[tuple[str, float]]) - list[str]: labels set() lower_text text.lower() for label, words in LABEL_RULES.items(): if any(w in lower_text for w in words): labels.add(label) for word, _ in keywords: if len(word) 4: labels.add(word) return list(labels)[:5]规则层保证“工作笔记”不会莫名其妙被归到“阅读”类关键词层则负责补充个性化标签。我当时也试过完全用聚类算法用 KMeans 把笔记向量化再分组效果很一般——个人笔记主题太分散聚类中心不稳定远不如“规则 关键词”直观可控。2.4 FastAPI 接口把能力包成可调用的 API现在把这些能力包成接口小程序端只需要 POST 一条笔记就能拿回关键词、摘要和标签。from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel from typing import Optional app FastAPI() class NoteIn(BaseModel): title: str content: str tags: Optional[list[str]] [] class NoteOut(BaseModel): id: int title: str keywords: list[str] summary: str labels: list[str] def process_note(note_id: int): 后台处理清洗文本、抽关键词、生成摘要、打标签 # 这里省略数据库读取逻辑 raw_text 从数据库读取的笔记正文 cleaned clean_text(raw_text) keywords, summaries extract_info(cleaned) labels auto_labels(cleaned, keywords) # 回填数据库字段keywords、summary、labels、statusdone # update_note(note_id, keywordskeywords, summary..., labelslabels) app.post(/api/note, response_modelNoteOut) async def create_note(note: NoteIn, background_tasks: BackgroundTasks): # 先快速写入基础数据statuspending # note_id insert_note(titlenote.title, contentnote.content) background_tasks.add_task(process_note, note_id) return NoteOut( idnote_id, titlenote.title, keywords[], summarynote.content[:50], labelsnote.tags )开发阶段数据库用 SQLite 就够了一行sqlite3.connect就能跑起来。生产环境建议换 MySQL 或者 PostgreSQL至少用 SQLAlchemy 做 ORM否则后面表结构变更会非常痛苦。我当时的做法是先用 SQLite 把整个流程跑通确认所有功能没问题后再切 MySQL切换成本主要就是改数据库连接配置。3. 小程序端的核心交互列表翻页、搜索防抖和接口封装后端接口搞定前端这些页面就好做多了。但小程序端的交互不是简单渲染数据有几个细节直接影响体验。3.1 页面规划减少页面的心智负担整个小程序我控制在三个页面首页笔记列表、编辑页、搜索页。首页卡片列表每张卡片显示标题、摘要、标签和时间。编辑页标题输入框加正文 textarea底部放一个“保存”按钮。搜索页输入框加搜索结果列表顶部几个标签快捷筛选。这里有一个前端适配细节不同机型的顶部导航栏高度不一样写自定义导航栏时不要写死数值用微信提供的胶囊按钮位置做动态计算不然刘海屏和全面屏会出现明显的错位。我选择用原生小程序而不是 uni-app 或 Taro是因为项目就三个页面前端很轻没必要引入跨端框架的构建链路。如果你打算将来同时发布到支付宝小程序或百度小程序再考虑跨端框架也不迟。3.2 列表加载更多onReachBottom 与翻页参数列表页最核心的问题就是分页。微信小程序的onReachBottom会在页面滚动到底部时触发但触发时机可能比预期频繁如果不加锁会连续发出多个重复请求。我的实现如下// pages/index/index.js Page({ data: { list: [], page: 1, pageSize: 10, hasMore: true, loading: false }, onLoad() { this.loadList(true); }, onReachBottom() { if (this.data.loading || !this.data.hasMore) return; this.loadList(false); }, loadList(reset) { if (this.data.loading) return; this.setData({ loading: true }); const page reset ? 1 : this.data.page; wx.request({ url: ${BASE_URL}/api/notes, data: { page, pageSize: this.data.pageSize }, success: (res) { const rows res.data.rows || []; this.setData({ list: reset ? rows : this.data.list.concat(rows), page: page 1, hasMore: rows.length this.data.pageSize, loading: false }); }, fail: () { this.setData({ loading: false }); } }); } });两个细节说明一下。第一loading字段的主要作用不是给用户看加载动画而是锁住并发请求——触底事件可能连续触发如果没有锁同一页数据会被请求十几次。第二hasMore的判定用rows.length pageSize如果返回不足一页就说明没有更多数据了。后端接口也要记得给列表字段加索引不然数据量上来了全表扫描会很慢。后端对应的分页接口可以按这个思路写app.get(/api/notes) def list_notes(page: int 1, pageSize: int 10): # 按创建时间倒序返回 rows 和 total rows get_notes_page(page, pageSize) return {rows: rows, page: page, pageSize: pageSize}3.3 搜索防抖不只是性能还有结果乱序搜索框要防抖这个很多朋友都知道。但防抖还有一个容易被忽略的附带作用避免结果乱序。具体来说用户输入“Python”后停顿了一下然后改成“人工智能”。如果不做防抖刚才那个“Python”请求可能还没返回用户就已经发起了“人工智能”请求。网络情况一波动先发出的请求可能后返回最终页面上显示的是上一次搜索的结果这时候用户会以为系统坏了。我的代码里用一个requestSeq变量来解决这个问题// pages/search/search.js let searchTimer null; let requestSeq 0; Page({ data: { keyword: , results: [], searching: false }, onInput(e) { const keyword e.detail.value.trim(); this.setData({ keyword }); clearTimeout(searchTimer); if (!keyword) { this.setData({ results: [] }); return; } searchTimer setTimeout(() { this.doSearch(keyword); }, 400); }, doSearch(keyword) { const seq requestSeq; this.setData({ searching: true }); wx.request({ url: ${BASE_URL}/api/search?q${encodeURIComponent(keyword)}, success: (res) { if (seq ! requestSeq) return; // 丢弃过期的响应 this.setData({ results: res.data.items || [], searching: false }); }, fail: () { if (seq requestSeq) this.setData({ searching: false }); } }); } });这个seq ! requestSeq判断非常关键。我第一次做的时候没加这个设施结果就是“Python”的结果晚于“人工智能”返回页面显示的是旧关键词的搜索结果。排查了半天最后发现是响应乱序导致的加上序号判断后问题立刻消失。搜索接口本身也要做一点处理。个人笔记场景数据量不大用数据库的 LIKE 查询就够但关键词最好做分词拆解把分词结果用 OR 组合这样匹配范围更合理。3.4 封装 request 与用户身份绑定所有请求都直接写wx.request会导致大量重复代码。我封装了一个简单的 Promise 版本const BASE_URL https://api.example.com/api; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method, data, header: { content-type: application/json, Authorization: Bearer (wx.getStorageSync(token) || ) }, success: (res) { if (res.statusCode 200 res.statusCode 300) { resolve(res.data); } else if (res.statusCode 401) { // token 失效引导重新登录 wx.navigateTo({ url: /pages/login/login }); reject(res); } else { reject(res); } }, fail: reject }); }); } module.exports { request };用户身份绑定走的是标准微信登录流程小程序端wx.login()拿到临时 code发给后端后端用 code 调用微信的jscode2session接口换 openid再生成自己的 token 返回给小程序。小程序把 token 存到wx.setStorageSync后续所有请求带上 token。这里要注意不要在客户端拿 code 当身份凭证。code 有时效性而且按微信的规范必须由后端换 openid小程序端不应该感知 openid 的存在。3.5 草稿保存监听 onHide 和 onUnload编辑页有一个很实际的场景用户写了一大段笔记切到微信回了个消息回来发现草稿丢了。这时候就需要监听页面隐藏和卸载事件。小程序页面生命周期里有onHide和onUnload分别在页面切到后台和页面销毁时触发。我在这两个事件里把编辑框内容写入本地 storage// pages/edit/edit.js Page({ data: { title: , content: }, onTitleInput(e) { this.setData({ title: e.detail.value }); }, onContentInput(e) { this.setData({ content: e.detail.value }); }, onHide() { this.saveDraft(); }, onUnload() { this.saveDraft(); }, saveDraft() { if (!this.data.title !this.data.content) return; wx.setStorageSync(draft_note, { title: this.data.title, content: this.data.content, time: Date.now() }); } });下次进入编辑页时读取本地草稿并提示用户是否恢复。这个功能虽然不起眼但实际用起来非常提升好感度——笔记类工具最怕丢内容一次丢失就可能让用户直接放弃整个产品。4. 真机联调、抓包和体验版连接开发与真实用户到这里开发层面的功能基本齐了。真正折磨人的是联调阶段——本机跑得好好的一到真机就各种翻车。这块我踩坑最多单独拿出来写。4.1 本机能跑、手机就崩先解决本地联调开发者工具里可以勾选“不校验合法域名”所以后端跑在http://127.0.0.1:8000时开发者工具里能正常请求。一旦用真机预览请求大概率直接失败。原因很简单手机上的微信根本不认识127.0.0.1这个地址在手机上看指代的是手机自己。解决办法分几步后端启动时监听所有网卡uvicorn main:app --host 0.0.0.0 --port 8000。手机和电脑连同一个 Wi-Fi。电脑 IP 用ipconfigWindows或者ifconfigMac/Linux查看假设是192.168.1.10。小程序里BASE_URL临时改成http://192.168.1.10:8000/api。微信开发者工具的“预览”弹层里勾选“启用开发调试模式”手机端才允许访问 http 域名。这一步我当时卡了差不多一个晚上最后发现就是手机微信的域名校验在捣鬼。注意这只能是开发阶段的临时方案正式发布必须换成有备案的 HTTPS 域名。4.2 用 Charles 抓包看真实请求一次难忘的 401真机上接口报错但开发者工具里一切正常这种情况下最有效的排查方式就是用抓包工具看真实请求。Charles 是我常用的选择。操作流程电脑打开 Charles默认代理端口 8888。手机 Wi-Fi 设置里把 HTTP 代理指向电脑 IP:8888。手机浏览器访问chls.pro/ssl下载并安装 Charles 的 SSL 证书。重新打开小程序Charles 里就能看到所有wx.request请求的完整链路请求地址、请求头、请求体、响应体。抓包能定位到很多真机上才暴露的问题。我印象最深的一次是搜索接口在真机上始终返回 401开发者工具里却完全正常。抓包一开发现手机端请求头里的 token 是空的——原因是页面跳转太快wx.login的回调还没执行完页面就已经发出了请求。这类时序问题不看真实请求很难定位。需要强调一下Charles 抓包是用来做正常开发调试的不要绕过任何证书校验机制也不要拿它做非法用途。常规的抓包分析完全可以满足联调需求。4.3 体验版分发收集试用反馈的正确姿势功能写完想喊几个朋友试用这时候不需要提审上线走体验版通道就行。具体流程微信开发者工具右上角点“上传”填版本号和备注。登录微信公众平台进入“版本管理”。在“开发版本”里找到刚上传的版本点“设为体验版”。在“成员管理”里添加体验成员填朋友的微信号。朋友在微信里直接打开你的小程序体验版就能正常使用。这里有个关键决策体验版一定要把后端部署到有正式 HTTPS 域名的服务器上不要让朋友连你的笔记本局域网。最简单的低成本方案是买一台云服务器装好 Nginx 和 TLS 证书FastAPI 跑在 8000 端口Nginx 反向代理到 443。然后在微信公众平台配置 request 合法域名。这里还要提醒两件事。第一微信小程序的正式版和体验版都会强制校验 TLS证书不能是自签的必须机构签发。第二域名备案要提前处理不然配置合法域名时会被卡住。个人主体的小程序每隔一年要做一次年审这个时间节点也值得记在日历里过期了会影响服务。我当时的做法是在小程序里加一个简单的“反馈”入口试用者可以直接提交使用感受。这样收集到的反馈会比口头沟通完整得多也方便后续迭代。4.4 上线前检查清单别让细节毁掉体验最后整理一份上线前检查清单都是我吃过亏之后总结出来的检查项说明HTTPS 域名request 合法域名必须配置证书有效且非自签接口鉴权token 过期处理、越权访问、openid 绑定数据备份每天定时 dump 数据库备份文件放对象存储或异地隐私协议小程序审核需要说明收集了哪些用户数据分页索引列表和搜索接口的字段加数据库索引日志监控后端接口打印 request_id出错能快速定位到具体请求连接池数据库连接用连接池避免短连接在高并发下崩溃日志这里多说一句不要打印用户笔记的完整正文一方面涉及隐私另一方面日志文件很快就会膨胀。打印长度截断后的摘要字段就够了。这套项目做到最后我最大的感受是所谓“智能”并不是一开始就要上多复杂的东西。先把分词、关键词、摘要这些基础能力跑通用户就已经能感受到明显差异了。后续如果想做相似笔记推荐可以考虑向量数据库加文本嵌入模型把关键词阶段的输出作为一个特征喂进去这是另一个话题了。给同样想试试的朋友一个建议别一上来就追求完整产品先把“保存一条笔记自动返回关键词和标签”这条链路跑通再逐步加列表翻页、搜索防抖和体验版分发。功能是慢慢长出来的链路通了后面每一步都很快。如果你也在折腾类似的小程序加 Python 项目欢迎分享一下你踩过的那些坑。
返回列表