ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

零到全栈(联调、CORS 与第三方库:从假数据到真分析)

零到全栈(联调、CORS 与第三方库:从假数据到真分析) 到上一篇为止后端已经有了两个接口GET /api/profile 返回主页内容POST /api/analyze 接收文字并返回分析结果——只是分数、标签、拼音还全是写死的占位值接口文档倒是自动生成了这一篇先让前后端真正握上手网页从接口读数据途中撞上 CORS 这道浏览器安全规则、看清 OPTIONS 预检的 “先问后发”再把写死的后端地址收进环境变量随后直面那笔假数据的欠账——去 PyPI 找库、验库用 pypinyin 和 snownlp 换掉接口里的占位实现守住约定只换内部让文字实验室真的会分析前后端联调与 CORS这一部分让前端和后端真正握上手走通 输入 → 请求 → 后端计算 → 响应 → 界面更新 的完整链路并在路上认识浏览器的一道安全规则——CORS要完成的四件事做完这一部分主页的标题、副标题、作品和座右铭会来自 GET /api/profile文字实验室里输入一段文字、点击 开始分析结果区会显示 POST /api/analyze 返回的结果认识并解决跨源限制 CORS顺带看清 OPTIONS 预检把散落在代码里的后端地址收进配置文件还记得讲数据与界面分离时埋的那颗种子吗当时 site.js 里的内容都是写死的我们说过它们以后可以从网络接口获取——今天就是兑现这句话的日子先把两边都跑起来需要两个终端# 终端 1后端 cd ~/zero-to-tech/backend source .venv/bin/activate fastapi dev # → http://localhost:8000# 终端 2前端 cd ~/zero-to-tech npm run dev # → http://localhost:3000一台电脑两个一直运行的程序3000 是前端8000 是后端先让后端数据与前端约定一致正式连接之前先确认两边说的是同一种 数据语言现在前端 data/site.js 里的 home 不只有标题和副标题还有作品与身份信息但 /api/profile 只返回了标题和副标题两个字段——当时说过重点先放在 HTTP 和框架上数据结构等前端真的来调时再对齐这一刻到了如果前端组件按照原来的结构取 featuredWork.title后端却没有返回 featuredWork两边就对不上了这就是那句话API 是调用方和被调用方之间的一份约定当前 /api/profile 返回的字段还不满足前端的需要所以先改后端把 profile 补成和 site.js 的 home 同构为了等会儿一眼看出数据是否真的来自后端先在标题后面临时加一个明显的标记 来自后端profile { heroTitle: 关于我来自后端, # → 临时加的标记验证完删掉 heroSubtitle: 项目创意灵感心得我的作品, featuredWork: { kicker: 作品, title: 文字实验室, copy: 拼音和情绪挖掘中文里的细节, linkLabel: 打开作品, }, identity: { motto: 已识乾坤大尤怜草木青, learning: 零到全栈, }, }保存后先用 curl 确认后端这边正常curl http://localhost:8000/api/profile能看到完整 JSON 和 来自后端 的标记说明接口本身没有问题用现成的前端代码替换后端准备好了轮到前端前端要改的地方不少主页要改成向后端请求数据文字实验室的输入卡和结果卡也要接上接口——这些都是很常规的 取数据、发请求 写法而这一部分真正的重点是联调请求为什么不通、怎么排查所以不逐行讲前端代码直接用改好的版本替换改好的代码放在仓库 joylibo/zero-to-tech-demos 里克隆下来用 zero-to-tech-5-5/ 里的文件覆盖项目中的同名文件git clone https://github.com/joylibo/zero-to-tech-demos.git cp zero-to-tech-demos/zero-to-tech-5-5/components/*.jsx ~/zero-to-tech/components/ cp zero-to-tech-demos/zero-to-tech-5-5/css/lab.css ~/zero-to-tech/css/替换了哪些文件、各自做了什么对照一下即可不必抠语法文件改动HomeView.jsx变成客户端组件打开页面先用 site.js 的数据打底再去 GET /api/profile拿到后端数据后更新界面TextLabView.jsx变成客户端组件把 分析结果 这份状态提升到这里分别下发给输入卡和结果卡学过的 状态提升InputCard.jsx点 开始分析 时把文字 POST 给 /api/analyzeResultCard.jsx改成显示父组件传来的结果还没有结果时用一份默认占位css/lab.css新增一条 .lab-error 样式用于请求失败时的提示有几点先记下来因为要在浏览器里发请求HomeView、InputCard 这些组件顶部都加了 use client成了客户端组件——这个概念讲 Next.js 时提过这里不展开这些组件里后端地址 http://localhost:8000 都是直接写死的现在能跑但并不理想本部分最后会把它收进配置文件请求失败时后端没跑、跨源被拦等代码用 try/catch 做了基本兜底主页失败就保持 site.js 的打底数据输入卡失败就在按钮上方给一行提示界面不会无声崩掉——属于常规的错误处理不是重点不展开替换完保存打开 http://localhost:3000按理说主页大标题应该变成后端返回的 关于我来自后端但页面上仍然是原来的 关于我前端代码是现成的、后端也用 curl 验证过——为什么网页没有拿到数据先不要急着改代码真正的联调往往就是从这种 结果与预期不一致 开始的第一次撞墙顺着线索排查遇到这种情况不要盯着代码猜先弄清楚这次请求走到哪一步断的一次请求会在三个地方留下线索按数据流动的顺序、从发出请求的这一头开始一站一站往下看每一站回答一个问题看哪里回答什么问题浏览器 Network请求发出去了吗后端终端后端收到了吗又是怎么处理的浏览器 Console结果为什么没有交到我们的代码手里第一站浏览器 Network打开浏览器开发者工具切到 Network刷新页面找到 /api/profile请求确实存在——说明它发出去了不是那段 fetch 代码根本没执行开发环境里如果看到两条相同的 GET也不用慌Next.js 的 App Router 默认开启 React 严格模式开发时会多执行一次 Effect 来帮忙检查副作用正式构建不会因为这个检查多发一次第一个问题有了答案请求发出去了那它到没到后端第二站后端终端回到运行后端的终端可以看到类似GET /api/profile HTTP/1.1 200 OK这一行说明两件事请求已经到达后端而且后端处理完并返回了 200——在后端看来这次请求是成功的到这里就有点奇怪了请求发出去了后端也收到并成功返回了页面上却没有数据东西已经送到门口是谁把它扣下的第三站浏览器 Console切到 Console会看到一段红字Access to fetch at http://localhost:8000/api/profile from origin http://localhost:3000 has been blocked by CORS policy: No Access-Control-Allow-Origin header is present...把三站的线索连起来看会发现一个很有意思的现象Network 里能看到请求说明发出去了后端日志显示 GET 返回 200说明收到了也处理成功了之前用 curl 也能拿到完整 JSON但网页 JavaScript 仍然拿不到结果问题不在接口有没有运行也不在路径写错而在浏览器提到的 CORSCORS 到底拦了什么CORS中文叫 跨源资源共享是浏览器对网页 JavaScript 的一条安全规则什么叫 跨源一个 源 由三部分组成协议 域名 端口任何一项不同就是不同的源我们的前端是 http://localhost:3000后端是 http://localhost:8000协议相同、域名相同但端口不同——所以它们是两个源网页脚本从 3000 去访问 8000就是跨源请求其实已经到达了后端对刚才这个简单 GET 来说浏览器已经把请求发给了后端后端也返回了 200CORS 拦下的不是 请求到达服务器而是浏览器不允许当前网页的 JavaScript 读取这份未经授权的跨源响应这也解释了为什么 curl 一直畅通无阻CORS 是浏览器对网页脚本的规则curl 不是网页不受这条规则约束浏览器怎样询问后端怎样回答网页发起跨源请求时浏览器会自动带上Origin: http://localhost:3000意思是 这段网页脚本来自哪里后端如果愿意让这个来源读取响应就在响应头里给出Access-Control-Allow-Origin: http://localhost:3000这就是后端的许可请求里说明来源响应里给出许可浏览器看到两边对得上才把响应交给网页 JavaScript给 FastAPI 加上 CORS要让网页读到响应就得让后端在响应里带上那行 Access-Control-Allow-Origin我们有两个接口与其给每个接口都手写响应头不如用一个现成的中间件统一处理——中间件是加在 请求进入、响应离开 必经之路上的一层处理所有请求和响应都会过它一道打开 backend/main.py顶部增加 importfrom fastapi.middleware.cors import CORSMiddleware紧跟在 app FastAPI() 后面增加app.add_middleware( CORSMiddleware, allow_origins[http://localhost:3000], )allow_origins 就是控制 Access-Control-Allow-Origin 这行响应头的开关把前端地址 http://localhost:3000 填进去后端就认这个来源中间件还有别的参数但眼下这个 GET 只差 来源许可 这一项先加这一行就够其余等真正遇到问题再补保存后后端自动重启刷新主页——标题变成了 关于我来自后端数据终于从 8000 端口流到了 3000 端口的网页再看 Network 里的 /api/profileResponse Headers 多了一行access-control-allow-origin: http://localhost:3000这就是后端发的许可确认成功后可以把标题里的 来自后端 删掉恢复正常文案以后想验证数据是否来自后端也可以临时改一个字段再刷新页面观察第二次撞墙POST 被预检拦下主页的 GET 通了接下来试文字实验室的 POST /api/analyze这个接口已经写好前端也替换好了看起来一切就绪打开文字实验室输入一段文字点击 开始分析——界面上冒出一行红字Failed to fetch又撞墙了这个提示很笼统只说 请求失败了没说为什么于是还是老规矩去现场找线索打开 Network会发现这次和上次不同只点了一次 分析列表里却出现了一条从没写过的 OPTIONS 请求而且它失败了真正想发的那条 POST 反而没有出现OPTIONS /api/analyze ← 失败 没有 POST再看 Console...has been blocked by CORS policy: Method POST is not allowed by Access-Control-Allow-Methods in preflight response.又是 CORS但和第一堵墙不一样第一次是请求发出去了、响应被拦回来这次那条 POST 根本没发出去被这个 OPTIONS 挡在了前面这个 OPTIONS 是浏览器自动发的它有个专门的名字叫 CORS 预检preflight认识 HTTP 方法时 OPTIONS 混过一次脸熟—— 我能对这个资源做什么现在派上了用场要点先说清楚这个 OPTIONS 不是我们写的也不是 Next.js 或 React 的功能而是浏览器自动发的属于 CORS 规则的一部分——这也解释了为什么之前用 curl 从没见过它curl 不是浏览器不受 CORS 约束为什么第一个 GET 没有预检、这个 POST 却有因为浏览器把跨源请求分成两类像主页那个 GET方法普通、也没带特别的头属于简单请求浏览器直接发顶多事后拦下响应不给脚本读而这次的 POST 带了 Content-Type: application/json就成了不简单的请求——对这种请求浏览器会先发一个 OPTIONS 去问后端允许这个来源吗允许 POST 吗允许带这个请求头吗预检通过才发真正的 POST不通过POST 根本不会离开浏览器为什么要多这一道POST 往往会改动数据比如发一条评论、下一个订单之类的要是不问就发即便响应被拦、脚本读不到这件事也已经在后端做了预检 先问后发把没授权、又可能产生副作用的请求挡在发送之前预检问的正是我们没回答的预检失败的原因Console 已经写明Method POST is not allowed回头看刚配的中间件我们只写了 allow_origins——只说了 谁能来没说 能用什么方法预检由 CORSMiddleware 自动应答不需要我们为 OPTIONS 写任何代码它一查来源允许但方法 POST 不在名单里于是打回补上 allow_methods 即可app.add_middleware( CORSMiddleware, allow_origins[http://localhost:3000], allow_methods[GET, POST], )allow_methods 声明这个接口允许被哪些方法跨源调用项目用到 GET 和 POST就如实写这两个预检其实还会问 能不能带某些请求头对应参数是 allow_headers我们带的 Content-Type 属于浏览器默认放行的常见头不必专门声明将来若请求要带不常见的头比如登录用的 token才需要在 allow_headers 里列出保存后端、自动重启再点一次 开始分析这次结果区出来了原文正是刚提交的文字还有分数、判断和占位的拼音换一段文字再点原文随之改变——每一次结果都真的来自后端分数和拼音目前仍是写死、占位的后面才会真正计算回到 Network 再看这次是两条请求先一条 OPTIONS 预检这次通过紧接一条真正的 POST——这就是“先问后发”走通后的样子同一个 CORS两副面孔至此见到了 CORS 的两种场景简单 GET 已经到达后端但没有许可时网页不能读取响应带 JSON 的 POST 会先发 OPTIONS 预检通过后才发送真正的 POST最后一步把写死的地址收进配置GET 和 POST 现在都正常了但还留着一个小尾巴前端代码里后端地址是写死的用 VS Code 全局搜索http://localhost:8000会在 HomeView.jsx 和 InputCard.jsx 里各找到一次一个很现实的问题是这个地址是会变的——后端换个端口、项目挪到另一台机器、以后要部署到线上每一种情况这个地址都得跟着改现在只有两处改起来还不费事可组件一旦多起来同一个地址散落在十几个文件里每变一次都要满项目搜索替换漏掉一处就是一个特别难找的联调 bug再往深一层看一个地址该填什么取决于代码跑在哪个环境——开发机、测试机、线上服务器各不相同这种跟运行环境绑定的值本就不该写死进组件让业务代码为了换个环境反复改动这说明后端地址不属于组件的业务逻辑而是一项配置配置应该集中管理你可能会问后端 allow_origins 里那个 http://localhost:3000不也是把地址写死在代码里了吗没错它同样是一个跟环境绑定的配置值正式项目里也该从配置读取——道理是对称的这里先拿前端这一侧当例子把 地址属于配置 讲透后端这类配置留到部署时一起处理创建 .env.local在前端项目根目录也就是 package.json 所在的目录新建 .env.localNEXT_PUBLIC_API_BASE_URLhttp://localhost:8000NEXT_PUBLIC_ 前缀表示这个值可以进入浏览器代码正因如此它只能放后端地址这类可以公开的配置不能放 API 密钥、密码等秘密信息——那些会被打包进浏览器等于公开在两个组件的 import 下方都增加const API process.env.NEXT_PUBLIC_API_BASE_URL;然后把硬编码地址分别改成fetch(${API}/api/profile)和fetch(${API}/api/analyze, {创建或修改环境变量后要重启前端.env.local 是在开发服务器启动时读取的光保存文件还不够必须重启# 按 Ctrl C 停止原来的开发服务器 npm run dev重启后再分别验证主页 GET 和文字实验室 POST应该一切正常项目的 .gitignore 已经包含 .env*所以 .env.local 默认不会进入 Git为什么环境配置通常不直接提交、部署时又怎样提供真实地址等真正部署后端时再具体处理见证网站真正活了现在把这次 POST 请求的完整链路再看一遍输入文字 → 浏览器发送 POST → OPTIONS 预检通过 → FastAPI 接收并校验请求体 → Python 计算结果 → FastAPI 返回 JSON → 前端拿到 JSON、更新界面 → 结果区自动刷新从那个双击打开的静态页面到今天——浏览器里运行着 React电脑上运行着 Python中间通过真实的 HTTP 请求交换数据整条链路是我们自己搭起来的主页这类编辑型内容其实继续留在 site.js 完全没问题把它接到 GET主要是为了用最简单的数据学习第一次前后端联调真正非后端不可的是 /api/analyze 这种需要根据用户输入实时计算的功能这一段的收束回头看后端这一段走过的路亲手调用真实 API理解了调用方、被调用方和接口约定装好 Python、建好 venv用 pip 和 requirements 管理依赖零依赖手搓 API看清 HTTP 的请求与响应用 FastAPI 重写接口体验路由、校验和自动文档最后让 React 与 Python 真正握手学会联调排查认识 CORS 与 OPTIONS 预检并把配置收进了环境变量现在还有两个明显的欠账/api/analyze 的拼音是占位符情感分数也只是写死的固定值还不是真正的分析每次分析结果用完就丢没有历史记录接下来我们用 Python 生态里的第三方库把分析变成真的再把结果保存下来第三方库和 PyPI并非所有的功能都要从 0 开发——学习使用第三方库拒绝重新造轮子先还上欠着的账后端部分收尾时留了两笔账/api/analyze 的分析是假的每次分析完就丢这一段把它们还清先对付第一笔让分析变成真的那问题来了真的怎么算先说拼音要给任意一段中文标注拼音得有一张覆盖几万个汉字的读音表这还不够——中文有多音字重庆 的 重 和 重要 的 重 不是一个音行长 银行 行走 里的 行 能读出花来还得整理出海量的词组规则才能判断一个字在哪个词里读哪个音再说情感分数凭什么说这句话 0.9 分偏积极、那句话 0.5 分偏中性这个分不是查表查出来的得有一个从大量真实文本里学出来的模型来判断训练模型得先有语料、有方法、有时间掂量一下这两样我们能自己做吗不是不能做只是会很花时间所以面对这种需求正确的第一反应不该是 我怎么把它写出来程序员圈有个特别形象的说法——重新造轮子 (reinvent the wheel)轮子早被造出来了非要从头再搓一个费时费力还多半造得更糙如果有成熟的现成品就别自己重复造所以第一反应应该是先问一句这件事社区里是不是早有人写好了这一部分就把 找现成的库 这套动作完整走一遍去哪找PyPI和几种查法别人写好的库 放在哪前端见过答案npm 的包都放在 npm registry 上npm install animejs 就是从那儿下载的Python 世界的同款叫 PyPI (Python Package Index)网址 pypi.org——这里有几十万个包人人可取怎么从里面找到想要的那一个方式不止一种直接上 pypi.org 搜关键词看简介、安装命令、文档链接、版本历史用搜索引擎搜 python 中文 拼音 库 这类或者干脆问 AI把需求描述给它让它推荐几个候选三条路都能给出 候选但谁都不保证候选靠谱——尤其是 AI一是它的知识有截止时间可能推荐过时的方案二是它偶尔会一本正经地编出一个不存在的包名这叫 幻觉甚至有坏人专门抢注这类名字、往里塞恶意代码再加上 PyPI 上谁都能上传——这是生态繁荣的原因也意味着上面质量参差所以立一条规矩不管候选是搜出来的还是 AI 给的拿到手都得自己验一遍回想那句话—— 照着别人的文档用上别人的能力前提是先找到靠谱的 别人用这套方法锁定 pypinyin 和 snownlp回到两个需求就用上面的方式查一查搜 中文 拼音或者问 AI Python 里给中文标拼音、还能处理多音字的库有哪些——线索都指向同一个名字pypinyin再搜 中文 情感分析”答案落在 snownlp 上候选有了但先别急着 pip install——按刚立的规矩先验验库靠谱吗能不能满足需求验两层第一层靠不靠谱打开候选在 PyPI 的页面一般带 GitHub 仓库链接看两个快信号GitHub 星数——多少人给它点过赞是人气和信任最直观的参考最近更新时间——还活着吗持续发版说明有人在认真维护几年没动的库要谨慎第二层能不能满足需求人气高不等于合用还得翻开文档拿它和需求逐条对照拼音要能带声调还要认多音字重庆 和 重要 的 重 读不同音情感要能给出一个 0 到 1 的分数好换算成 偏积极 / 偏消极读 pypinyin、snownlp 的文档看它们提供的函数是不是正好覆盖这些——覆盖得上才算选对了看这两个库的文档可以去它们各自的 GitHub 项目主页pypinyingithub.com/mozillazg/python-pinyinsnownlpgithub.com/isnowfy/snownlp打开仓库页首屏那篇长长的说明就是 README——它相当于项目的门面、主页库怎么装、提供哪些函数、每个函数怎么用通常都写在这儿PyPI 的包页面上一般也有指向 GitHub 的链接顺着点过去就到读文档时还会注意到一个细节它们的用法示例经常是以开头的 Python 代码比如 pypinyin 的 GitHub README 中的写法 from pypinyin import pinyin, lazy_pinyin, Style pinyin(中心) # or pinyin([中心])参数值为列表时表示输入的是已分词后的数据 [[zhōng], [xīn]] pinyin(中心, heteronymTrue) # 启用多音字模式 [[zhōng, zhòng], [xīn]]这个是什么等下回答先在项目里安装这两个库装上、试运行——顺便认识 REPL安装之前先看提示符确认 venv 环境正确cd ~/zero-to-tech/backend source .venv/bin/activate # 确认提示符前有 (zero-to-tech) pip install pypinyin snownlpsnownlp 的背后是别人已经训练好的模型——模型不是代码而是别人做的训练结果我们 import 一下就能直接用如果对 模型 训练 这些词感觉陌生先不要担心今天毕竟是用轮子不知道轮胎的橡胶是怎么加工的其实也没关系装好之后要跑一下试试可以写个 Python 脚本比如 pinyin_test.py再运行但有一种更便捷的方式值得学一下——刚才文档里满屏的是 Python 的 REPL交互式解释器敲一行代码立刻执行、立刻出结果终端里直接敲 python 命令即可进入注意在 (zero-to-tech) 环境里敲——刚装的库在这个环境里python3回车提示符变成就进来了名字不用记体感记住就行敲一行看一行先别急着 import 库用几行最简单的代码感受一下 REPL 和 写脚本 有什么不一样 1 1 2 name 全栈 name 全栈 print(你好 name) 你好全栈注意第一行敲1 1回车它直接把 2 显示了出来——我们并没有写 print这就是 REPL 和脚本最直观的区别回想手搓 API 时的 handmade.py那是把一整套逻辑写进一个文件python3 handmade.py 从头到尾一次跑完屏幕上只会出现显式 print 的内容上面这几行要是写进 .py 文件去跑1 1、name这两行什么都不会显示想看到 2 得写成 print(1 1)而在 REPL 里敲进去的只要是个 值它就顺手把结果回显出来一句话理清两者的关系都是同一个 Python——.py 脚本是 把动作写全、一次跑完适合正式的程序REPL 是 敲一句、答一句适合把玩、试错、快速验证一个想法或一个新库今天验库正是 REPL 的主场好手感有了先验 pypinyin正好对着那两条需求 from pypinyin import pinyin pinyin(你好) [[nǐ], [hǎo]] pinyin(重庆) [[chóng], [qìng]] pinyin(重要) [[zhòng], [yào]]注意 重庆 和 重要——同一个 重 字在 重庆 里读 chóng、在 重要 里读 zhòngpypinyin 都判对了它不光认多音字还能看词定音文档承诺的、需求要的对上了——这背后就是那张几万字的读音表加词组规则别人替我们整理好了import 一下就能用顺带看清 pinyin 返回的形状[[chóng], [qìng]]——一个 嵌套列表读起来不太直观我们的项目只想要一串干净的拼音用不上这层嵌套pypinyin 另给了一个更省事的函数 lazy_pinyin from pypinyin import lazy_pinyin lazy_pinyin(重庆) [chong, qing]lazy_pinyin 的区别在于① 每个字只给一个音、不再套那层列表直接是扁平的字符串列表② 默认不带声调chong 而非 chóng如果还需要声调可以给 lazy_pinyin 加上 styleStyle.TONE声调就回来了 from pypinyin import lazy_pinyin, Style lazy_pinyin(重庆, styleStyle.TONE) [chóng, qìng]Style 是 pypinyin 提供的一组 拼音样式 开关Style.TONE 就是 带声调符号 这一档记住 lazy_pinyin(text, styleStyle.TONE) 这个写法下一段直接用它再验 snownlp from snownlp import SnowNLP SnowNLP(今天的风很轻适合把想法写下来).sentiments 0.9465... SnowNLP(太失望了再也不来了).sentiments 0.0027...sentiments 给出一个 0 到 1 之间的分数越接近 1 越积极第一句 0.94第二句 0.003——这不是查表是包里那个别人训练好的模型在 判断跑出来的小数位可能略有出入这也是正常的两条需求都验证通过就可以退出 REPL exit()以后拿到任何新库都可以尝试进 REPL 玩两下——敲一行看一行比闷头读半天文档更快建立手感REPL 平时还能当计算器、当小试验田随开随用顺带认识这片生态中文 NLPpypinyin 和 snownlp 都属于同一片生态——中文自然语言处理 (NLP让程序 处理人话 的那一类技术)这片地界上还有一位常客值得认一下jieba结巴分词把一句话切成一个个词——我来到北京清华大学 切成 我 / 来到 / 北京 / 清华大学分词是很多中文处理的第一步搜索、统计词频、做词云……都先得切词我们的项目用不上认得名字就行不安装更值得了解的是每个领域都有自己的一片生态——图像处理、爬虫、数据分析、AI……套路全是今天这一套找库 → 验库 → REPL 玩两下 → 接进项目这一部分学的不只是这两个库而是这个套路记上账requirements.txt两个重要的库装好了最后别忘了那两条老规矩.venv 不进 Git但 requirements 清单要进库变多了重新记一次账pip freeze requirements.txt这样 pypinyin、snownlp 这两个依赖就连同它们各自的依赖一起记进来了这份清单的意义是任何人拿到这个项目只需要——pip install -r requirements.txt一条命令整个环境原样复现任何人 三个字里也包括将来站在服务器上的我们——到时候会亲手体会这份清单值多少钱对照老直觉它就是后端的 package.jsonpip install -r 就是后端的 npm install顺手把改动提交了——这一部分代码一行没动只有 requirements.txt 变了正好是一次干净的提交让网页真的会分析文字两个库装好了、也验证过了接下来把它们应用起来通过 /api/analyze 对外提供服务——只改接口的 内部不动接口的 约定只动一处这一部分只需要动 analyze 函数的内部API 的访问地址不动、方法不动、请求体不动、返回的字段一个不加一个不减看一下现状此前做好的接口形状长这样——分析值全是写死的占位app.post(/api/analyze) def analyze(req: AnalyzeRequest): return { text: req.text, score: 0.5, label: 偏平静, pinyin: 先占位, }分数永远 0.5标签永远 偏平静拼音干脆写着先占位——现在就把这几个值都搞活动手换芯打开 backend/main.py先在文件顶部把两位新成员请进来import 照例放顶部from pypinyin import lazy_pinyin, Style from snownlp import SnowNLPlazy_pinyin 之前体验过Style 是它的搭档——用它让拼音带上声调再看占位版返回的四个字段text、score、label、pinyintext 本来就是真的原样回传用户输入剩下三个是假的先挑两个能直接从库里拿到的下手——score 和 pinyin把 /api/analyze 改成app.post(/api/analyze) def analyze(req: AnalyzeRequest): text req.text score round(SnowNLP(text).sentiments, 2) # 真模型打的分 return { text: text, score: score, label: 偏平静, # ← 先留着下面处理 pinyin: .join(lazy_pinyin(text, styleStyle.TONE)), # 真拼音带声调 }主要变化有两处一处是用 SnowNLP 算出一个 score交给 return 里的 score另一处是用 lazy_pinyin 算出拼音交给 return 里的 pinyin具体语法不理解也没事知道发生了什么就行现在只剩 label 还占着位它和前两个不一样——这两个库里并没有一个函数能直接吐出 偏积极 或 偏平静 这几个字label 是给人看的结论得由我们从 score 这个数字换算出来接近 1 说 偏积极接近 0 说 偏消极中间地带算 中性这段 数字 → 结论 的翻译逻辑值得单独拎成一个小函数放在 analyze 上面elif 就是 else if手搓路由时见过这种连排判断def score_label(score): if score 0.6: return 偏积极 elif score 0.4: return 偏消极 else: return 中性这两个阈值0.6 / 0.4不是什么标准答案是拿一批句子实测分数之后拍板的产品决定——分数是模型给的但 多少分算积极 由我们说了算也可以调成别的有了它把 analyze 里那行占位的 label: 偏平静 变成 label: score_label(score)这个 API 就完全写好了app.post(/api/analyze) def analyze(req: AnalyzeRequest): text req.text score round(SnowNLP(text).sentiments, 2) return { text: text, score: score, label: score_label(score), pinyin: .join(lazy_pinyin(text, styleStyle.TONE)), }代码其实没多几行但能力已经强了许多——它真的可以做计算了见证效果改完代码分别启动前后端服务cd ~/zero-to-tech npm run devcd ~/zero-to-tech/backend source .venv/bin/activate fastapi dev直接访问 http://localhost:3000进入文字实验室在输入框里打一句话点 开始分析拼音真的出来了——带着声调多音字也对顺带一提句子里的逗号会原样留在拼音里因为 lazy_pinyin 只翻译汉字非汉字原样透传这是正常的分数是模型打的——试试这几句实测过的分数跑出来应该一致输入scorelabel我特别喜欢这部电影0.95偏积极今天的风很轻适合把想法写下来0.95偏积极太失望了再也不来了0.00偏消极此前欠下的账全部都清了为什么前端不用改有没有注意到这一节前端一点都没改为什么因为虽然接口的实现变了但接口的约定没变路径还是 /api/analyze方法还是 POST请求体还是 {text: ...}返回还是那四个字段——变的只是约定背后的具体实现方案讲 API 时说过 API 的调用方不需要知道服务端内部怎么实现今天站在服务方这一侧可以体会到这句话的另一面只要守住约定内部随便换。调用方不知道、也不需要知道顺手看一眼 http://localhost:8000/docs文档也纹丝没动因为约定没变我们哪怕用 Java 把后端重写一遍或者换一个新的模型来计算情感——只要这个 API 的约定不变前端都不用改模型的边界接下来多试试这个情感模型试试这两句输入scorelabel问题今天下午三点开会0.26偏消极一句毫无感情的话被判了 偏消极呵呵真是太棒了呢0.94偏积极阴阳怪气它当了真翻车了为什么因为 snownlp 的情感模型主要是在商品评论语料上训练出来的——它擅长判断 像评论的句子好评差评那种但 下午三点开会 这种中性陈述、以及反讽阴阳怪气都在它的训练经验之外这不代表 snownlp 太差而是所有模型的共性模型没有“常识”只有“训练时见过的世界”用任何模型之前先弄清它的边界——知道它哪里不准比迷信它的分数重要得多这句话在大模型时代照样成立ChatGPT、DeepSeek 也有各自的边界幻觉就是一种只是边界更远、更隐蔽放眼看看从小模型到大模型说到大模型——也许已经想到了情感分析这件事今天完全可以调用大模型的 API 来做就像调 DeepSeek 那样把句子发过去让它打分那为什么我们不用把本地小模型和云端大模型 API 这两条路摆在一起本地小模型snownlp大模型 API如 DeepSeek花钱免费按量计费联网不需要必须速度本地毫秒级网络往返推理秒级准确度够用边界明显强得多连反讽都懂隐私数据不出自己的机器用户的文本要发给第三方再往远看一步大模型也不只有 调云端 API 这一条路像 DeepSeek 这类开源大模型权重是公开的可以下载到自己的机器上跑——业内叫 本地部署 或 私有化部署数据不出门、也不按次付费代价是得自己备一台够劲的机器还得自己维护而且这类开源模型通常有好几种尺寸参数量常写成 1.5B、7B、70B、671B 这样B 十亿——尺寸越大越聪明但也越吃显卡、越慢具体部署哪一种尺寸得根据自己手上的机器配置来挑没有绝对的好只有合不合适我们的文字实验室是教学项目免费、快、离线的本地库完全够用从 snownlp 这样的小模型到自己部署的开源大模型再到云端顶配 API是一条连续的谱系——真做产品时按预算、隐私、精度选其中一段而不论选哪段对我们这个项目来说都只是 再换一次芯 的事壳还是不用动如果哪天不满足 snownlp 的能力、想把后端改为大语言模型完全可以基于学到的知识继续改结语到这里项目的功能开发基本完成接下来本该转入部署、安全与运营不过在此之前还有一笔账要还——每次分析的结果用完即弃项目还没有 记忆给文字实验室配上记忆让它记下用户查过的句子这就是后面要做的事
返回列表