简介:这是一份 2025 年更新的去水印小程序源码包,面向微信小程序开发者或希望零成本搭建去水印工具的个人站长。源码采用免服务器部署方案,内置免费去水印接口,支持替换为自己的接口,且代码无加密,便于学习、调试与二次开发;同时整合激励视频广告位、插屏广告位等流量主变现配置,可作为小程序广告变现的实战学习案例。资源共包含一百四十个文件,压缩包仅 737KB,核心包括 js、json、wxml、wxss 等小程序页面逻辑与界面样式文件,以及 png 图标、sample 示例说明,还保留了 Git 版本管理相关的对象文件,可对照查看 V1.0 到 V1.5.1 各个版本的迭代内容。目前已有四十六人学习下载。源码对下载失败引导、相册权限校验、每日激励广告次数、下载中转站、视频封面下载及文案复制等细节均有优化,配合安装教程,能帮助开发者快速完成去水印接口对接、流量主广告位设置与小程序上线发布。
1. 去水印小程序免服务器:先搞懂这套东西到底能做什么
微信里搜“去水印”,你能找到一大把名字类似的工具类小程序。它们不卖会员,也不做付费解锁,最直接的变现方式就是开流量主、挂广告:用户解析视频,作者赚广告费。标题里的“免服务器”才是这套东西能做起来的关键——不买云主机、不备案域名,用微信云开发就能把解析逻辑跑起来。这篇文章把这类小程序从解析原理、云端部署到流量主接入的完整链路讲清楚,适合想练手小程序云开发的新手,也适合手里有流量、想做广告变现的运营者,还包括那些想低成本验证“去水印需求还能不能赚钱”的个人开发者。后面每一章都落到具体命令、参数和代码上,按着走,你手里的zip包才能变成能上线的小程序。
2. 解析链路拆解:视频地址是怎么从分享链接里挖出来的
2.1 短视频分享链接的跳转链:抖音、快手、小红书结构不一样
大部分短视频平台的产品逻辑是:用户点“分享”复制出来一条短链,短链经过302跳转落到一个H5落地页,无水印视频地址就藏在这条跳转链的某个环节里。只要把这链路手动摸一遍,后面写解析逻辑都是顺水推舟。
抖音的分享链接形如v.douyin.com/xxxx,重定向后落到iesdouyin.com的落地页,页面里能提取到video_id或aweme_id。快手是v.kuaishou.com短链,落地页 HTML 里能抠到photoId。小红书用xhslink.com短链,页面里虽然能看到视频信息,但经常带了xsec_token这类反爬参数,直接抓取拿不到完整地址。B站最规整,b23.tv短链重定向后能直接拿到 BV 号。
| 平台 | 分享链接 | 重定向目标 | 可提取的关键参数 | 解析难度 |
|---|---|---|---|---|
| 抖音 | v.douyin.com | iesdouyin.com 落地页 | video_id / aweme_id | 中等,风控频率高 |
| 快手 | v.kuaishou.com | 落地页 HTML | photoId | 中等,部分接口要 cookie |
| 小红书 | xhslink.com | 落地页 + token 参数 | noteId / xsec_token | 偏高,token 有效期短 |
| B站 | b23.tv | 视频页 | BV 号 | 低,接口最规整 |
这个差异直接决定了解析代码的结构。抖音要重定向后再请求一次页面,快手可以直接从 HTML 里正则匹配,小红书得额外带 token 调详情接口,B站拼接口就行。所以代码里必须按平台拆分支,不能一个函数走天下。2025年各平台的风控频率都在提高,上面的难度判断只代表趋势,不代表某一天的具体状态,但“先看结构再写代码”的思路不会变。
2.2 手动走一遍解析链路:curl 与抓包各看什么
拿到一个短链别急着写代码,先手动把链路跑通。我一般用 curl 模拟手机端请求,看短链最终跳到哪。
# 示例:把短链的完整跳转过程打出来 curl -s -I -L --max-redirs 5 "https://v.douyin.com/示例链接/" \ -H "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1" \ -o /dev/null -w "%{http_code} %{redirect_url}\n"这里用-I只取响应头,-L自动跟随重定向,-w里输出最终响应码和跳转地址。实际写解析代码时,千万不要开自动跟随,而要手动拿Location头再发第二次请求——中间跳转环节有时候会带关键参数,让-L自动处理会把这些参数丢掉。先看redirect_url字段,能一眼确认短链指向哪个域名,如果跳到的是iesdouyin.com,说明链接合法且有效。
第二步是拿落地页内容,从里面找视频 ID 或页面里内嵌的接口数据。
# 拿落地页 HTML,把疑似视频 ID 的字符串打出来 curl -s "https://www.iesdouyin.com/share/video/示例ID/" \ -H "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15" \ | grep -oE "(video_id|aweme_id)[^,]{0,40}"这段代码的关键是 UA 必须模拟手机端。PC 端 UA 会直接被重定向到引导下载 App 的页面,什么都提取不到。如果结果为空,问题基本出在 UA、Cookie 或 Referer 上,和解析逻辑本身无关。先手动走通链路再写代码,也是做小程序抓包排查的基本功——你必须先确认链路是通的,再去自动化。
抓包调试还会用到 Charles 这类工具拦截小程序请求,看第三方解析 API 到底请求了哪个域名、带了哪些 header、返回的 JSON 长什么样。有些第三方解析服务表面是一个接口,实际背后会再去请求好几个上游接口,这些上游行为在小程序里是黑匣子,抓包才能看穿。
2.3 自建解析还是第三方接口:适配器模式才是关键
去水印解析有两条路。第一条是自建解析,自己在云函数里请求各平台公开接口、提取无水印地址,优点是不依赖第三方、没有按次计费,缺点是平台一旦改版就要跟着维护。2025年短视频平台风控频繁,自建解析的维护成本明显变高,经常这周还能跑、下周就返回 403。
第二条是接第三方解析 API,把分享链接发给对方,对方返回 JSON,里面带播放地址、封面和标题。优点是省事、便宜、响应快,缺点是有免费额度限制,接口质量随厂商波动,跑路也是常事。
我一般会给这两种方案套一层适配器,而不是在业务代码里到处写 if。函数签名固定为:接收一个分享链接,返回一个统一结构的对象,包含播放地址、封面、标题、平台。自建解析和第三方接口都实现这个签名,平台 A 挂了换平台 B 的解析,用户端无感知。这是去水印小程序里最重要的一层设计。
这个设计也直接决定了你能不能把一个流传的 zip 源码跑起来。很多源码的 config 里都有一行api地址,那八成是第三方解析接口地址。安装文档让你“把这个地址换成自己的”,意思就是去注册一个第三方解析服务,拿到专属 API Key 再填进去。如果文档没写清楚,按适配器思路去读代码,很快能定位到该改哪里,而不是漫天猜。
3. 用微信云开发部署解析逻辑:免服务器的三个组成与云函数写法
3.1 “免服务器”的真实成本:云函数、云数据库、云存储分工
标题写“免服务器”,严格来说是免运维,不是零成本。微信云开发把传统后端拆成三块:云函数负责跑逻辑,云数据库存结构化数据,云存储存文件。你不需要买 ECS、不用配 Nginx、不用装 MySQL,但这些能力消耗的资源仍然按量计费,只是成本低到个人项目可以忽略。
云函数是这套架构的心脏。解析逻辑跑在云函数里,前端小程序通过wx.cloud.callFunction调用它。云函数发出的网络请求不受小程序 request 合法域名校验限制,这是“免服务器”能成立的核心——你不用去小程序后台配置一堆下载域名和业务域名,因为真正对外发起请求的是云函数,而不是前端。
云数据库在去水印场景里通常用来做三件事:缓存解析结果、记录用户每日解析次数、存储广告配置。云存储则用来临时存放转换后的文件。如果你只是做纯解析,不搞视频转存,云存储甚至可以不用。zip 包里的安装教程一般会带你做这几件事:开通云开发、创建环境、上传云函数、修改app.js里的环境 ID。这个流程不复杂,但环境 ID 配错是最常见的翻车点,代码跑起来报Environment not found八成就是它。
3.2 云函数里写解析转发:完整代码与超时参数
下面是一段完整的解析云函数,按 2.3 节的适配器思路,先实现一个抖音解析分支。依赖wx-server-sdk和got,在云函数目录下执行npm install got安装。
// 云函数:parseVideo const cloud = require('wx-server-sdk') const got = require('got') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 手动处理 302 重定向,拿到跳转后的真实地址 async function resolveLocation(shortUrl, ua) { const res = await got(shortUrl, { followRedirect: false, headers: { 'User-Agent': ua } }) return res.headers.location || shortUrl } exports.main = async (event) => { const { shareUrl } = event const ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148' try { // 第一次请求:短链 -> 落地页链接 const realUrl = await resolveLocation(shareUrl, ua) // 第二次请求:拿落地页 HTML,从中匹配视频 ID const html = await got(realUrl, { headers: { 'User-Agent': ua } }).text() const match = html.match(/video_id[":=]+\s*"?(\d{15,20})"?/i) if (!match) return { code: -1, msg: '未匹配到视频ID' } const videoId = match[1] // 第三次请求:用视频 ID 调详情接口拿无水印播放地址 const apiUrl = `https://www.iesdouyin.com/aweme/v1/web/aweme/detail/?aweme_id=${videoId}` const apiRes = await got(apiUrl, { headers: { 'User-Agent': ua }, responseType: 'json' }) const playUrl = apiRes?.data?.aweme_detail?.video?.play_addr?.url_list?.[0] if (!playUrl) return { code: -1, msg: '播放地址字段缺失' } return { code: 0, data: { playUrl, videoId } } } catch (err) { return { code: -1, msg: err.message } } }逻辑说明:resolveLocation人为关掉自动重定向,是因为有些平台会在 302 的Location头里带签名参数,自动跟随会把第一次响应的关键信息丢掉。正则只匹配抖音最常见的纯数字视频 ID,快手和小红书要写不同分支,这就是适配器存在的意义——云函数里保持一个入口,分平台处理。got的responseType: 'json'让返回体直接变成对象,不用手动JSON.parse。
这里必须注意云函数的超时参数。云函数默认超时 3 秒,而上面的逻辑最少有三次网络请求,3 秒根本不够。需要在云函数目录下建config.json手动调大超时:
{ "permissions": { "openapi": [] }, "timeout": 30 }timeout设为 30 秒适合大多数短视频解析场景。如果超过 30 秒还在跑,大概率不是慢,是请求被风控拦截后进入了无意义的重试,这种情况应该直接报错返回。超时调大意味着并发时资源占用更高,免费额度消耗更快,这两个参数要一起权衡,不要无脑调到 60 秒。
3.3 前端调用云函数:从分享链接到播放地址的完整链路
云函数部署好之后,前端要做的就是把用户输入的分享链接传给它,再把返回的播放地址交给video组件。
// 小程序页面里的解析方法 Page({ data: { shareUrl: '', videoUrl: '' }, async onParse() { const shareUrl = this.data.shareUrl.trim() if (!shareUrl) { wx.showToast({ title: '请粘贴分享链接', icon: 'none' }) return } wx.showLoading({ title: '解析中' }) try { const res = await wx.cloud.callFunction({ name: 'parseVideo', data: { shareUrl } }) const { code, data, msg } = res.result if (code === 0) { this.setData({ videoUrl: data.playUrl }) } else { wx.showToast({ title: msg || '解析失败', icon: 'none' }) } } catch (err) { wx.showToast({ title: '网络异常,请重试', icon: 'none' }) } finally { wx.hideLoading() } } })对应 WXML 里的核心结构:
<input placeholder="粘贴抖音/快手分享链接" bindinput="onInput" /> <button type="primary" bindtap="onParse">解析</button> <video wx:if="{{videoUrl}}" src="{{videoUrl}}" controls style="width: 100%; height: 200px;"></video>这段链路的优势在于:请求从云函数发出,天然绕过了小程序前端的域名校验,不需要在后台配置 downloadFile 合法域名;播放地址是 HTTPS 的,video组件可以直接播放。云函数返回结构里除了playUrl,建议把videoId也带回来,前端可以用它做“复制链接”或“分享好友”的扩展功能。前端showLoading要放在try外面,避免finally里hideLoading报错。
4. 流量主接入:开通条件、广告位配置与收益模型的平衡
4.1 开通流量主的前提:UV 门槛、类目审核与手机号验证策略
流量主不是注册就送的,最常见门槛是累计独立访客(UV)达到 500。对工具类小程序来说,500 UV 不难凑,难的是类目审核。去水印涉及视频版权,审核时经常被要求补充资质或变更类目。常见做法是归类到“工具-信息查询”,用户协议里写清楚“仅解析公开分享链接,不提供侵权内容下载”。类目选错会直接被卡在审核环节,改一次就多等几天,所以第一步就把类目定准。
另一个容易被忽视的是防滥用。去水印小程序上线后,很快会被羊毛党盯上,用脚本批量刷解析。所以很多去水印小程序会要求先登录再使用,这里就要用到微信手机号快速验证组件,拿到脱敏手机号后用来做每日次数限额:每个手机号每天免费解析 5 次,超过看广告或次日恢复。这个策略既保护了第三方解析接口的调用成本,也天然把流量主的有效 UV 做起来了——用户不登录就没法用,登录行为本身就是一个可靠的访客标记。
4.2 Banner 广告位配置:组件引入与 ad-unit-id 的对应关系
流量主后台通过后,第一步是创建广告位,拿到ad-unit-id。Banner 广告适合放在页面底部,不遮挡视频播放画面,是所有广告位里对体验影响最小的。小程序端接入分为两步:先在页面 JSON 里声明广告组件,再在 WXML 里放置。
{ "usingComponents": { "ad-banner": "/components/ad-banner/ad-banner" } }<!-- 放在页面底部固定区域 --> <ad-banner ad-unit-id="adunit-你的广告位ID" binderror="onAdError" bindload="onAdLoad" ></ad-banner>ad-unit-id在流量主后台的广告位管理里创建,测试阶段可以用微信提供的测试广告位,但正式上线前一定要替换成自己申请的正式 ID,否则广告不展示、收益为零。binderror事件一定要处理——广告拉取失败时不能影响解析主流程,常见做法是onAdError里把广告容器隐藏掉,用户该解析还是解析,不广告也不弹错。
Banner 广告的单价是所有广告形式里偏低的,它更适合做底量填充,而不是主要收入来源。去水印场景的特点是用户操作频率高、停留时间短,一次解析 10 秒就结束,Banner 只会被看到两三秒,收益天花板很低。所以真正要重视的是激励视频广告。
4.3 激励视频广告:解析后“看广告解锁”的落地写法
激励视频广告是去水印小程序的主力变现方式。用户首次解析免费,第二次开始需要看一段 15 秒的激励视频才能获取无水印地址。这种形式用户接受度较高,因为付出是明示的,“看广告解锁”比“强制弹广告”体验好太多。
// 解析成功后展示激励视频 let rewardedVideoAd = null function showRewardAndPlay() { if (!rewardedVideoAd) { rewardedVideoAd = wx.createRewardedVideoAd({ adUnitId: 'adunit-你的激励视频位ID' }) } rewardedVideoAd.show().catch(() => { rewardedVideoAd.load() .then(() => rewardedVideoAd.show()) .catch(() => { wx.showToast({ title: '广告暂未就绪,请重试', icon: 'none' }) }) }) }代码里的show().catch()是必须的。激励视频广告首次调用时可能还没加载完成,show()会返回一个 rejected Promise,常见做法是先load()再show()。这个细节不处理,用户首次点击按钮会没有任何反应,体感就是小程序坏了。
按钮文案也要注意。微信对诱导点击的审核越来越严格,按钮必须明确写“看广告解锁无水印地址”,不能伪装成普通解析按钮骗用户点进去。2025年这类违规的处罚是直接封禁广告位。激励视频的收益通常是 Banner 的几倍到十几倍,同样是 500 UV,只挂 Banner 和 Banner 加激励视频,月收入差距能到一个数量级。
5. 部署上线避坑:让去水印小程序翻车的 5 个细节
5.1 短链接 302 重定向没处理,解析接口全部落空
现象:用户输入分享链接后一直提示解析失败,云函数日志里看到请求返回的是 HTML 而不是 JSON。
原因:代码里用了默认自动跟随重定向,而部分平台的短链跳转过程中,关键参数就藏在第一次 302 的Location头里。自动跟随会把第一次响应丢弃,后续请求带不上这些参数,自然解析不到数据。
解决:手动处理重定向。先用followRedirect: false请求短链,拿到location字段,再带同一个请求头去请求这个真实地址。我在 3.2 节代码里写的resolveLocation函数就是干这件事的,这段逻辑无论如何不能省。
5.2 云函数默认超时 3 秒不够用
现象:解析偶尔成功、频繁失败,云函数日志里出现Task timed out after 3 seconds。
原因:云函数默认超时配置是 3 秒,而解析逻辑最少做三次网络请求,加上平台接口响应慢,3 秒几乎一定会超时。这个问题在本地开发者工具里测不出来,因为本地调试不走云端运行时,一上传到线上就原形毕露。
解决:在云函数目录下创建config.json,把timeout调到 30 秒。同时前端要做好加载状态,解析超过 5 秒是正常现象,不要只设一个 3 秒的定时器就报“解析超时”,那会让用户以为小程序坏了。
5.3 平台返回 403:UA、Referer 与风控的关系
现象:本地用 curl 跑得好好的,代码部署到云函数后频繁返回 403。
原因:云函数的出口 IP 是云开发服务的公共 IP 段,平台风控对数据中心 IP 段天然不友好;另外代码里可能漏了 Referer 头,或者 UA 用的是库默认的got,而不是手机端 UA。风控不是针对你的小程序,而是针对“来自云服务器的请求”这个流量特征。
解决:把请求头尽量模拟真实用户,UA 用 iPhone 或 Android 的完整 UA,Referer 按平台落地页域名补齐,带上sec-fetch-*系列头。如果换了请求头仍然 403,就不要和风控硬刚,直接把该平台的解析切到第三方接口,等自建解析恢复再切回来。诚实说,这条没有银弹,适配器模式的好处此时就体现出来了。
5.4 把视频下载到云存储再返回:流量费与响应速度双输
现象:用户播放视频卡顿,月底看账单发现云存储读流量费用高得离谱。
原因:有些中转逻辑会先把视频从原始地址下载到云存储,再用临时链接给前端播放。每次解析都这样做,等于把视频流量全部过一遍服务器,存储读流量加 CDN 流量双重计费,解析速度还慢了一大截。
解决:能直接返回原始播放地址就直接返回,不要过一道云存储。必须转存的场景,加一层缓存:同一个videoId只转存一次,后续命中缓存直接返回云存储链接。这里用云数据库记录videoId -> fileID的映射,比每次盲目下载省太多。
5.5 zip 包里的 AppSecret:上线前最容易忽视的泄露点
现象:收到微信公众平台的安全提醒,检测到 AppSecret 疑似泄露,号被封禁广告位。
原因:流传的 zip 源码包里,如果包含project.config.json、app.js或云函数目录下的配置文件,前一个运营者的 AppSecret、小程序 AppID 甚至微信支付商户密钥可能都留在里面。直接把包里的文件原样上传,密钥就跟着上线了。
解决:上线前全局搜索AppSecret、SessionKey、mch_id等关键词,把密钥值替换成占位符,真实密钥放到云开发的环境变量或云函数配置里,代码里只留process.env.XXX引用。尤其注意不要顺手把整包代码传到 GitHub 公开仓库,这也是泄露重灾区。
6. 把解析成功率从 70% 拉到 95%:接口池、缓存与降级策略
6.1 接口池轮询:用失败次数做健康检查
单个解析接口挂掉,解析成功率会直接掉到 50% 以下。常见做法是维护一个接口池,在云函数里用内存对象记录每个接口的连续失败次数,超过阈值就标记降级,下一轮请求优先走健康接口。
// 接口池健康检查的简化实现 const healthMap = {} async function callWithFailover(shareUrl) { const sources = ['self', 'thirdA', 'thirdB'] for (const source of sources) { if (healthMap[source] >= 3) continue try { const result = await resolveBySource(shareUrl, source) healthMap[source] = 0 return result } catch (err) { healthMap[source] = (healthMap[source] || 0) + 1 } } return { code: -1, msg: '所有解析源暂不可用' } }这段代码的逻辑是:每个解析源连续失败 3 次后进入冷却期,不再被调用;某次成功后计数清零。接口池的粒度不要只放在第三方服务上,自建解析的每个平台分支也可以拆开放进池里——抖音解析挂了你切快手解析源,互不干扰。
6.2 解析结果缓存:同一个链接只解析一次
去水印的流量特点是有大量重复请求,同一个热门视频可能被几十个人解析。云数据库按shareUrl的哈希做 key,解析成功后把结果存 30 天,命中缓存直接返回,既省第三方接口的调用次数费,也省云函数执行时间。
// 缓存命中的快速返回示例 const key = `parse:${hash(shareUrl)}` const cached = await db.collection('parse_cache').doc(key).get().catch(() => null) if (cached?.data) return cached.data缓存的有效期要按平台区分。B站视频地址长期稳定,缓存 30 天没问题;抖音的部分播放地址带有效期签名,缓存放太久反而会拿到过期链接,建议缩短到 3 天。这个细节需要在上线后观察用户反馈微调,属于典型的“参数要看实际效果”的配置,没有任何文档能替你决定。
6.3 降级与验证:上线前跑一遍 curl 确认链路
所有解析源全挂的时候,兜底动作是返回明确提示“解析服务暂不可用,请稍后重试”,同时引导用户直接用浏览器打开原链接,至少不把用户完全挡死,这也符合小程序审核的可用性要求。我的习惯是每次发版前用 curl 跑一遍主流平台的分享短链,确认重定向、落地页和播放地址三段链路都通,再提交审核。这个习惯救过我两次:一次是平台改了落地页结构,一次是第三方接口悄悄换了签名方式,都是靠发布前的五分钟检查发现的。
去水印小程序技术门槛不高,真正的门槛在风控对抗和维护节奏上。如果你打算长期做,把接口池、缓存和降级策略在一开始就搭好,比上线后补要省事得多。希望帮到你。
本文还有配套的精品资源,点击获取