前阵子给一个用 Gradio 搭的 ChatBot 演示站加访问统计刚开始以为就是把百度统计的 JS 往 HTML 里一贴就行结果折腾了大半天发现 Gradio 这个“伪前端”框架和普通网页差别太大。代码粘进去了请求也发出了但百度统计后台就是没有数据最后查出来的原因既不是账号问题也不是网络问题而是 Gradio 对页面渲染模式导致统计脚本的作用域整个变了。这篇文章就把我在 Gradio 应用里集成百度统计的完整过程和踩坑经历写出来从最基础的全局注入到按钮级别的事件埋点再到开启登录认证后的统计盲区一次性讲清楚。如果你正在用 Gradio 做模型 Demo、内部工具或者对外发布的产品页面希望知道“到底有多少人用了、用户点了什么按钮、哪个功能最受欢迎”那这篇文章刚好解决你的问题。1. 为什么 Gradio 集成百度统计不是简单粘一段代码1.1 Gradio 页面结构和传统 Web 页面的区别Gradio 表面上看起来是个网页但它不是传统后端模板渲染出来的静态页面也不是前后端分离 SPA 那种一手掌控路由和模板的项目。Gradio 的核心是 Python 后端启动一个 FastAPI 服务前端通过一套固定模板和 JavaScript 运行时把 Python 里定义的 Blocks 组件动态渲染到浏览器。换句话说你写的是 Python 组件但浏览器里生成的那一大段 HTML、script、style 你基本控制不了。传统网站集成统计只需要在head或body里插入百度统计的一段script。但 Gradio 不给你直接改 HTML 的机会你只能在它提供的几个入口里做文章比如Blocks(js...)、custom_css、gr.HTML组件或者干脆绕到 FastAPI 层做中间件。不了解这层机制就会像我一开始那样把统计代码塞进奇怪的地方导致压根不执行。1.2 百度统计代码到底在干什么百度统计的集成代码本质非常简单拆开看就是两部分。第一部分是在全局维护一个_hmt数组用来暂存还没发出去的统计事件第二部分是动态创建出一个script标签请求https://hm.baidu.com/hm.js?xxxxxxxx这个地址会返回一个真正统计脚本。统计脚本加载完后会把_hmt数组里积压的事件逐个发送到百度服务器并且后续的_hmt.push(...)调用也由这个脚本接管。所以不管用哪种方式集成核心只有两件事把这个初始化代码放到页面加载时会执行的位置并且保证后面要记录事件时能拿到同一个_hmt。听起来简单放在 Gradio 里却有两个坑一是注入位置不对代码根本不会执行二是 Gradio 对 JS 的处理方式可能把_hmt变成局部变量导致按钮事件里 push 失败。这两个坑下面都会给到对应解法。2. 把百度统计代码放进 Gradio 应用三种可靠落地方案2.1 方法一Blocks(js...) 全局注入推荐Gradio 的Blocks构造函数里有一个js参数专门用来接收一段自定义 JS。官方文档写的是“页面加载时执行的自定义 JavaScript”这其实就是最合适的统计代码注入入口。用法很直观import gradio as gr analytics_js window._hmt window._hmt || []; (function() { var hm document.createElement(script); hm.src https://hm.baidu.com/hm.js?YOUR_BAIDU_ANALYTICS_ID; var s document.getElementsByTagName(script)[0]; s.parentNode.insertBefore(hm, s); })(); with gr.Blocks(jsanalytics_js) as demo: demo.launch()这里我用了window._hmt而不是官方示例里的var _hmt这个改动后面细说。第一次用的时候我把百度统计官方代码整个复制进去结果后来浏览器控制台里一直报_hmt is not defined排查了很久才发现是作用域问题。所以建议在任何注入方式里都统一写成window._hmt。这个方案的好处是接入成本最低不需要改造 Gradio 启动方式也不影响业务代码。需要注意的版本点是Gradio 3.x、4.x、5.x 的Blocks(js...)参数都一直存在只是高版本里对 JS 字符串的注入位置和包装方式略有变化用window._hmt都能兼容。2.2 方案二gr.HTML 组件挂脚本简单但有隐患还有一种网上一搜就能看到的做法是在页面顶部放一个gr.HTML组件把统计脚本用script标签塞进去import gradio as gr def tracking_component(): return script window._hmt window._hmt || []; (function() { var hm document.createElement(script); hm.src https://hm.baidu.com/hm.js?YOUR_BAIDU_ANALYTICS_ID; var s document.getElementsByTagName(script)[0]; s.parentNode.insertBefore(hm, s); })(); /script with gr.Blocks() as demo: gr.HTML(tracking_component()) btn gr.Button(开始)这种方法不推荐用在正式环境。Gradio 的组件是动态渲染的gr.HTML里的script标签虽然能执行但会遇到几个很尴尬的问题如果页面有局部刷新这段 HTML 可能被重复渲染统计代码被注入两次如果组件被 Gradio 内部机制隐藏或延迟加载统计代码的执行时机就不稳定。另外gr.HTML组件本身无论在页面上还是肉眼可见的都需要额外处理样式不然一个空 HTML 块占位会很突兀。但这个方法也并非一无是处如果你有一个临时页面只是想快速看一眼能不能统计到数据用gr.HTML是最快的验证方式。我一般用它做 5 分钟临时验证验证完了再切换到Blocks(js...)。2.3 方案三FastAPI 中间件注入最灵活还能统计登录页Gradio 应用底层是一个 FastAPI 应用launch()后可以通过demo.app拿到这个 FastAPI 实例。既然能拿到实例就可以用 FastAPI 的中间件机制拦截每一个返回 HTML 的响应在/head前插入统计脚本。这种方案最大的价值在于无论访问的是哪个页面包括 Gradio 自带的认证登录页、页面入口、错误页全部都能注入统计。import gradio as gr from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import Response TRACKING_SCRIPT script window._hmt window._hmt || []; (function() { var hm document.createElement(script); hm.src https://hm.baidu.com/hm.js?YOUR_BAIDU_ANALYTICS_ID; var s document.getElementsByTagName(script)[0]; s.parentNode.insertBefore(hm, s); })(); /script class BDTrackingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): response await call_next(request) content_type response.headers.get(content-type, ) if text/html in content_type: body b async for chunk in response.body_iterator: body chunk html body.decode(utf-8) if /head in html: html html.replace(/head, TRACKING_SCRIPT /head) return Response( contenthtml, status_coderesponse.status_code, headersdict(response.headers), ) return response with gr.Blocks() as demo: gr.Markdown(Hello World) demo.launch()代码里demo.app在 Gradio 4.x 之后都能直接拿到部分旧版本可能需要通过launch()的返回参数或者demo.server_app获取这点根据你实际的 Gradio 版本稍微调整即可。中间件方案的缺点是代码量更大而且要操作 StreamResponse如果处理不当会影响前端加载。我的建议是只要你的 Gradio 应用不是部署在比较特殊的环境或者没有“必须统计到登录前访问量”这个需求优先用方案一别杀鸡用牛刀。2.4 三种方案怎么选方案接入成本稳定性能否统计登录页适用场景Blocks(js...)最低最稳定不能大多数常规应用gr.HTML 组件低有一定风险不能快速验证FastAPI 中间件较高稳定但需谨慎能需要完整统计或带登录认证的应用如果你用的是默认形态没有加auth就用方案一。如果加了auth想看完整到访量用方案三。方案二只适合临时测试。3. 进阶埋点追踪按钮点击、生成次数、用户行为3.1 用事件的 _js 参数做行为埋点百度统计的基础代码只能记录页面访问量 PV但 Gradio 应用里更有价值的数据是“用户点了哪个按钮”“某个生成功能被用了多少次”“是否有人反复调整参数”。这些行为数据可以通过_js参数埋点。Gradio 的事件方法click、submit、change、input都支持一个_js参数用来在前端执行一段 JS。利用这个参数可以在每次按钮点击时额外向百度统计发送一个事件import gradio as gr def chat_with_bot(message, history): return 这是回复 def noop(*args): return None with gr.Blocks(jsanalytics_js) as demo: chatbot gr.Chatbot() msg gr.Textbox() send_btn gr.Button(发送) send_btn.click( chat_with_bot, inputs[msg, chatbot], outputschatbot, _js(message, history) { window._hmt.push([_trackEvent, chat, send, message]); return [message, history]; } )注意_js接收的函数参数需要和inputs数量一致并且返回值会作为 Python 侧函数的输入。如果不想让 JS 影响业务数据我的习惯是让_js原样返回输入值然后在 Python 侧照常处理。上面代码里_js把message和history原样返回所以chat_with_bot拿到的参数不会受统计代码影响。如果你的按钮点击不需要执行 Python 逻辑纯粹只是想发送统计事件可以把fn设为Nonesend_btn.click( fnNone, inputs[msg], outputs[], _js(message) { window._hmt.push([_trackEvent, chat, send, message]); return null; } )这种方式非常干净统计事件和业务逻辑完全分离。3.2 在浏览器端监听 DOM 事件做通用埋点_js参数有一个限制只能在 Gradio 事件里用而且是“前置钩子”。如果你想把页面上所有按钮点击都统一记下来每个按钮都写一遍_js会非常繁琐而且还要维护一份事件名映射。更通用的做法是在初始化统计代码的同时写一个原生 DOM 监听器监听所有按钮的点击。由于 Gradio 的按钮渲染出来后就是普通的button元素可以用MutationObserver观察动态插入的 DOM然后给按钮绑定点击事件window._hmt window._hmt || []; (function() { var hm document.createElement(script); hm.src https://hm.baidu.com/hm.js?YOUR_BAIDU_ANALYTICS_ID; var s document.getElementsByTagName(script)[0]; s.parentNode.insertBefore(hm, s); })(); function trackButtonClick(button) { if (button.dataset.bdTracked) return; button.dataset.bdTracked 1; button.addEventListener(click, function() { var text (button.innerText || button.textContent || unknown).trim().slice(0, 20); window._hmt.push([_trackEvent, button, click, text]); }); } var observer new MutationObserver(function(mutations) { document.querySelectorAll(button).forEach(trackButtonClick); }); observer.observe(document.body, { childList: true, subtree: true });这段 JS 可以直接放进Blocks(js...)。它比逐个_js埋点的好处在于只要 Gradio 界面上出现新按钮马上自动绑定事件以后加新功能不用再改统计代码。缺点是拿到的只是按钮上的文字区分度不够高比如多个按钮都叫“提交”事件名称就一样了。要想更精细还是得结合 3.1 的方案在关键业务按钮上单独写_js。3.3 结合 Gradio 的生命周期事件做路径埋点Gradio 是单页面应用用户从首页切换到 Tab A、Tab B 其实不会产生新的页面请求这时 PV 无法自动递增。如果你的 Gradio 应用里有多个 Tab并且希望知道不同 Tab 的访问热度可以用 Tab 的select事件配合_js发送_trackEvent或者更接近百度统计语义的话发送_trackPageviewwith gr.Blocks(jsanalytics_js) as demo: with gr.Tab(对话): ... with gr.Tab(绘图): ... # 给每个 Tab 绑定统计 gr.Tab(对话).select( fnNone, inputs[], outputs[], _js() { window._hmt.push([_trackPageview, /tab/chat]); return null; } ) gr.Tab(绘图).select( fnNone, inputs[], outputs[], _js() { window._hmt.push([_trackPageview, /tab/draw]); return null; } )需要明确一点百度统计后台的“页面分析”模块主要依赖hm.js自动发送的真实访问地址。手动_trackPageview发送的虚拟路径也能进“页面分析”但有一定延迟而且展现逻辑和真实页面不太一样所以重点看趋势和横向对比就行别纠结绝对数字。4. 集成中的坑与排查技巧80% 的人会踩4.1 统计代码被 Gradio 过滤或重复注入我遇到过的第一个坑是统计代码的换行和引号问题。Gradio 的Blocks(js...)接收的是一个字符串如果你在 Python 代码里用三引号包住 JS但内部又出现了或未转义的特殊字符会导致 Gradio 在渲染页面时直接报语法错误。解决办法是把统计 JS 单独写在一个analytics.js文件里然后读取文件内容再传给Blocks这样代码可维护性也更好from pathlib import Path analytics_js Path(analytics.js).read_text(encodingutf-8) with gr.Blocks(jsanalytics_js) as demo: ...另一个坑是重复注入。如果你既在Blocks(js...)里写了统计脚本又用gr.HTML塞了一遍百度统计会加载两次后台 PV 直接翻倍。这种问题不太好发现因为浏览器 Network 面板里会看到两个hm.js请求。排查时优先确认页面上hm.js请求数量只有一个才是正常的。4.2 开启 auth 后统计不到未登录用户很多内部工具会开启 Gradio 的auth认证功能比如demo.launch(auth[(admin, 123456)])接入auth后Gradio 会先渲染一个独立的登录页。这个登录页和你的应用页面共享同一个 FastAPI 服务但它并不是Blocks渲染出来的。所以如果你用的是Blocks(js...)方案统计代码只会出现在应用页面未登录用户停留在登录页时根本不会触发统计代码。最终结果是后台显示的 PV 和访客数全部偏小漏掉了大量“到了门口却没进门”的流量。要补齐这部分数据只能回退到 2.3 的 FastAPI 中间件方案。中间件会对所有返回text/html的响应注入统计脚本包括登录页。我在正式给一个内部工具上统计时就是用了中间件才把登录页的访问量一起记进来不然数据差得离谱。4.3 性能与异步加载问题百度统计的官方代码看起来是同步创建一个script标签并插入到页面但动态创建脚本这种方式的阻塞程度远低于 HTML 里直接写script src。不过在高并发或关键场景下如果不想让统计脚本的加载影响 Gradio 首屏渲染可以等window.onload之后再初始化window._hmt window._hmt || []; window.addEventListener(load, function() { var hm document.createElement(script); hm.src https://hm.baidu.com/hm.js?YOUR_BAIDU_ANALYTICS_ID; var s document.getElementsByTagName(script)[0]; s.parentNode.insertBefore(hm, s); });这个方案能避免统计脚本拖延页面加载但代价是如果用户打开页面后立刻关闭浏览器在onload之前就离开了这次访问就统计不到。以我的经验Gradio 应用通常都是工具型或模型 Demo 型用户停留时间普遍偏长用onload延迟加载影响不大反而能明显改善首字节体验。4.4 百度统计后台看不到实时数据的排查表现象排查方向处理方式hm.js 请求根本没有发出统计代码未注入浏览器控制台执行window._hmt检查是否为数组hm.js 发出但状态码异常站点 ID 问题核对hm.js?后面的 ID 与统计站点是否一致有请求但后台无数据本地环境测试localhost 默认可能不统计部署到正式域名测试数据只有 PV 没有访客浏览器拦截关闭 AdBlock、隐私模式再试多个 hm.js 请求重复注入检查是否同时用了多种方案登录页流量缺失Gradio 认证页不含应用 JS改用 FastAPI 中间件方案百度统计的数据也不是实时的通常有几分钟到几小时的延迟。刚接入半小时看不到数据很正常别急着改代码去 Network 面板确认请求没问题就耐心等。5. 一个完整的可复制实例给 ChatBot 演示站加统计5.1 完整代码基础 PV 统计 按钮点击事件下面给一套完整示例包含基础 PV、按钮点击事件、对话 Tab 切换统计整体代码可以直接复制部署from pathlib import Path import gradio as gr # 建议单独保存到 analytics.js analytics_js window._hmt window._hmt || []; (function() { var hm document.createElement(script); hm.src https://hm.baidu.com/hm.js?YOUR_BAIDU_ANALYTICS_ID; var s document.getElementsByTagName(script)[0]; s.parentNode.insertBefore(hm, s); })(); // 通用按钮点击统计 function trackButtonClick(button) { if (button.dataset.bdTracked) return; button.dataset.bdTracked 1; button.addEventListener(click, function() { var text (button.innerText || button.textContent || unknown).trim().slice(0, 20); window._hmt.push([_trackEvent, button, click, text]); }); } var observer new MutationObserver(function() { document.querySelectorAll(button).forEach(trackButtonClick); }); observer.observe(document.body, { childList: true, subtree: true }); def chat_fn(message, history): return 模拟回复 message with gr.Blocks(jsanalytics_js) as demo: gr.Markdown(# ChatBot 演示站) with gr.Tab(对话): chatbot gr.Chatbot() msg gr.Textbox(label输入消息) send_btn gr.Button(发送) send_btn.click( chat_fn, inputs[msg, chatbot], outputschatbot, _js(message, history) { window._hmt.push([_trackEvent, chat, send, message]); return [message, history]; } ) with gr.Tab(关于): gr.Markdown(这个页面用于演示百度统计集成。) gr.Tab(关于).select( fnNone, inputs[], outputs[], _js() { window._hmt.push([_trackPageview, /tab/about]); return null; } ) demo.launch()部署后打开浏览器控制台输入window._hmt回车能看到一个数组里面包含若干_trackEvent、_trackPageview记录就说明统计代码已经在工作了。5.2 部署后如何验证统计是否生效验证统计代码是否生效第一步是看浏览器 Network 面板。打开页面后刷新一次筛选hm.js如果出现一条状态码 200 的请求说明统计脚本加载成功。然后随便点一个页面按钮控制台执行window._hmt会看到数组里多了一条[_trackEvent, button, click, 发送]。我再建议一个更严谨的验证方式新开一个无痕窗口访问应用重复上面的操作确认无痕模式下也能正常发送。因为有些浏览器插件会屏蔽第三方统计请求无痕窗口更容易排除是插件干扰。如果以上都没问题登录百度统计后台在“实时访客”里面等一两分钟看到新记录就说明全链路通了。5.3 上线一周的数据复盘思路统计代码接入后重点看三块。第一是 PV / UV 趋势判断整体曝光量有没有波动第二是按钮事件分布比如“发送”按钮被点了多少次“重置”按钮被点了多少次事件数据能直接告诉你哪些功能最受关注第三是 Tab 页面的虚拟 PV能反映不同功能模块的热度。我在实际项目里发现很多用户会反复点击同一个生成按钮但真正的有效调用次数其实比点击数少得多。所以如果做运营分析不要直接拿按钮点击事件当“功能使用次数”最好在 Python 侧再统计一份真实业务调用日志用运行日期和参数维度做比对。百度统计主要负责流量侧业务侧数据还是得靠应用日志。6. 最后再分享一点个人经验从第一次在 Gradio 里塞统计代码到后来稳定跑了好几个项目我最大的体会是Gradio 这类框架的统计集成关键不是代码怎么写而是搞清楚它渲染页面的时机和注入位置。Blocks(js...)适合大多数场景但千万别忽略window._hmt这个全局变量的写法这是我踩过最深的一个坑如果不写成window._hmt高版本 Gradio 里后续事件埋点会各种报错。另外给个小技巧统计脚本里的站点 ID 不要直接写死在代码里可以从环境变量读取。这样在测试环境和正式环境用不同的百度统计站点数据不会被混在一起切环境也不需要改代码。比如import os analytics_js analytics_js.replace( YOUR_BAIDU_ANALYTICS_ID, os.getenv(BAIDU_ANALYTICS_ID, ) )这个替换逻辑虽然简单但在多环境部署时能省不少事。最后再次提醒开启百度统计后不要同时再用其他统计脚本做同样的事否则数据重复你会纠结很久。
