
Hi,大家!我是马哥,一名10年+程序猿。
短视频内容越来越多以后,一个很常见的工作流是:看到一个不错的选题、脚本或案例,先把链接保存下来,再回头整理文案、截图、视频素材和话题标签。
如果只是偶尔保存一两条内容,手动复制当然也能完成。但当这个动作每天重复几十次,问题就会变成工程问题:链接来自不同平台,分享文本里混着口令和短链,视频和图文的媒体结构不同,移动端保存还要处理权限、进度、失败重试、防盗链等细节。
基于此出发点,我开发了一个轻量级工具:马哥文案提取器。它是一个微信小程序,核心能力是粘贴短视频或图文链接后,解析出视频、图片、文案、话题标签和基础互动数据,并提供保存、复制、收藏和历史记录等操作。
此文不讲宏大的产品方法论,只从工程角度拆一下:一个看起来简单的“粘贴链接并解析”,背后需要哪些模块配合。
这个需求有几个明显特点:
所以我一开始没有把它做成一个完整web系统,也没有先上登录、会员、支付、后台管理这些重模块,而是选了一个更短的路径:
这类工具型产品,早期最重要的是让核心路径足够顺:复制链接、打开、解析、保存或复制。功能少一点没关系,但每一步都要稳。
项目可以分成三层:
微信小程序
├─ 首页:链接输入、平台识别、解析结果、保存/复制
├─ 历史:最近解析记录、重新解析、复制文案
└─ 我的:文案收藏、使用帮助、关于
FastAPI 后端
├─ POST /api/parse 统一解析入口
├─ GET /api/proxy 媒体代理下载
├─ parsers/ 各平台解析器
└─ config.py 缓存、限流、下载限制
基础设施
├─ Redis:解析缓存、IP 限流
└─ Caddy:HTTPS 与反向代理
小程序端只知道自己的后端地址,例如:
App({
globalData: {
baseUrl: 'https://api-video.mgnb.pro'
}
})
前端不会直接请求抖音、小红书、快手或B站。所有平台差异都收敛在后端解析器里,这样以后调整某个平台的解析逻辑,不需要频繁改小程序端。
首页的交互很简单:用户粘贴分享文本,小程序从文本中提取URL,然后识别平台。
function extractUrl(text) {
if (!text) returnnull;
var urlRegex = /(https?:\/\/[^\s<>"{}|\\^`\[\]]+)/g;
var match = text.match(urlRegex);
return match ? match[0] : null;
}
function detectPlatform(url) {
if (!url) return'';
if (/douyin\.com|iesdouyin\.com/.test(url)) return'抖音';
if (/xhslink\.com|xiaohongshu\.com/.test(url)) return'小红书';
if (/kuaishou\.com|gifshow\.com/.test(url)) return'快手';
if (/bilibili\.com|b23\.tv/.test(url)) return'哔哩哔哩';
return'';
}
真正发起解析时,前端只调用一个接口:
request('/api/parse', 'POST', { url: url })
后端返回的数据结构已经被统一过。无论原始平台是什么,前端都尽量只关心这些字段:
这样的好处是,页面层不用理解每个平台自己的字段命名。平台解析器可以很复杂,但前端保持简单。
保存视频看起来是一个按钮,实际需要处理三件事。
第一,平台媒体资源常常有防盗链或跨域限制,直接把原始视频地址交给小程序下载并不稳定。因此后端提供了 /api/proxy,前端把媒体URL包装成代理地址:
data.video.proxy_url =
app.globalData.baseUrl
+ '/api/proxy?url='
+ encodeURIComponent(data.video.url)
+ '&platform='
+ (data.platform || 'douyin');
第二,保存到相册要走微信授权。用户第一次保存时需要允许相册权限;如果之前拒绝过,需要引导去设置页重新打开。
第三,下载较大视频时不能让用户只看到一个卡住的按钮,所以页面里做了下载进度弹窗。
后端代理层也做了一些兼容处理:例如图片代理会根据 content-type 判断媒体类型,遇到WebP图片时会尝试转成JPEG,以提高小程序端保存的稳定性。
后端的统一入口是 POST /api/parse。流程大致如下:
读取请求体里的 url
↓
检查Redis限流
↓
按URL识别平台
↓
查解析缓存
↓
调用对应平台Parser
↓
统一返回ParseResult
代码结构上,后端维护了一个解析器表:
PARSERS = {
"douyin": DouyinParser(),
"xiaohongshu": XiaohongshuParser(),
"kuaishou": KuaishouParser(),
"bilibili": BilibiliParser(),
}
这样新增平台时,只需要补一个 parser,并在检测器里加 URL 规则。接口层不需要知道每个平台怎么取详情、怎么取视频、怎么解析图文。
解析结果使用统一的 ParseResult 数据模型:
@dataclass
class ParseResult:
platform: str
type: str
title: str = ""
caption: str = ""
hashtags: list[str] = field(default_factory=list)
author: dict = field(default_factory=dict)
stats: dict = field(default_factory=dict)
video: dict | None = None
images: list[str] | None = None
这个模型很朴素,但它解决了一个关键问题:前端展示不再依赖平台原始JSON。
小红书是这个项目里比较典型的例子。图文和视频笔记的数据结构不同,分享链接可能是短链,也可能带 xsec_token,视频地址还可能需要从多个位置兜底提取。
当前实现的核心路径是:
分享链接
↓
跟随重定向,得到真实笔记页
↓
提取 note_id 和 xsec_token
↓
调用 web feed 接口获取 note_card
↓
如果是视频,补抓笔记页 HTML 兜底提取原视频地址
↓
统一构造成 ParseResult
主接口是小红书web侧的feed接口:
POST https://edith.xiaohongshu.com/api/sns/web/v1/feed
请求体里会带上笔记ID、图片场景、xsec_source 和 xsec_token 等字段。因为这个接口需要签名和Cookie,项目里单独封装了签名逻辑,业务代码只关心“给我一个链接,返回结构化笔记数据”。
这里有一个工程上的取舍:解析器不是追求“只用一种方式拿到所有数据”,而是允许多级兜底。
例如视频地址优先从feed数据中的 originVideoKey 推导;如果拿不到,再从页面HTML中查找 masterUrl、backupUrls 等候选;最后再从note数据里挑一个质量相对更好的视频URL。
这种方式不优雅,但在面对外部平台经常变动的数据结构时,单一策略反而更脆。
这类解析服务有两个现实问题:
同一个链接可能被重复解析。 接口可能被频繁调用,甚至被刷。 因此后端使用 Redis 做两件事。
第一是缓存解析结果:
CACHE_TTL = 7200 # 2小时
同一个链接在缓存有效期内再次解析,会直接返回缓存结果,减少对外部平台的请求。
第二是IP级别限流:
RATE_LIMIT_PER_MINUTE = 10
超过限制时返回:
{
"code": 4029,
"message": "请求过于频繁,请稍后再试"
}
还有一个容易被忽略的细节:视频下载限制没有写死在小程序端,而是在后端配置里控制。
MAX_VIDEO_DURATION_SECONDS = 300
后端会给解析结果补充:
{
"downloadable": false,
"limit": {
"message": "视频超过5分钟,暂不支持下载"
}
}
前端保存视频时只看 downloadable 和 limit.message。这样以后调整限制,不需要重新发小程序版本。
很多工具型产品一上来就想做账号、同步、会员、后台。这个项目没有这么做。
历史记录和文案收藏都存在小程序本地:
var HISTORY_KEY = 'parse_history';
var FAVORITE_KEY = 'favorites';
var MAX_HISTORY = 50;
每次解析成功后,把标题、平台、作者、封面、文案、时间和原始链接保存下来。历史页里可以复制文案、删除记录,也可以点击记录重新回到首页解析。
这里有个小细节:微信小程序的tab页面跳转不能直接带query参数,所以历史页重新解析时用了一个本地存储中转值:
history 页面写入 pending_parse_url
↓
wx.switchTab 回首页
↓
首页 onShow 消费 pending_parse_url
↓
自动填入链接并重新解析
这个方案很轻,但足够稳定。
收藏夹也是同样思路。用户看到值得保留的文案,可以点“收藏文案”,之后在“我的”页面统一复制和管理。
整体方案采用:Caddy + FastAPI + Redis
部署层也尽量保持简单。
当前小程序访问的是一个 HTTPS API 域名,Caddy 负责证书和反向代理:
api-video.mgnb.pro {
reverse_proxy 127.0.0.1:8000
}
后端服务跑在本机 8000 端口,Redis 监听本地:
REDIS_HOST = "127.0.0.1"
REDIS_PORT = 6379
对于这个规模的工具来说,Caddy + FastAPI + Redis 已经够用。没有MySQL,没有复杂队列,没有后台管理系统,也没有登录态同步。少一层系统,就少一类维护成本。
从用户角度看,完整流程只有几步:
如果你想体验完整流程,可以在微信里搜索 “马哥文案提取器”,或点击下方卡片,立即使用:
小程序使用演示:
这个小程序看起来只是一个“解析链接”的工具,但开发过程中反复遇到几个典型问题。
第一,外部平台的数据结构不稳定,所以解析器要做好兜底,而不是假设某个字段永远存在。
第二,前端不要直接理解平台差异。统一数据模型比在页面里写一堆平台判断更重要。
第三,能放在后端控制的策略尽量放后端,比如下载时长限制、缓存版本、限流规则。小程序发版周期比后端重,不适合承载频繁调整的策略。
第四,不是所有数据都需要服务端持久化。历史和收藏先放本地,既保护隐私,也降低系统复杂度。
第五,工具产品的体验很大一部分来自边界处理。权限拒绝、链接无效、下载失败、缓存命中、旧历史记录缺少原始链接,这些都不是核心功能,但会决定用户觉得它“能不能用”。
如果把这个项目抽象一下,它其实是一个很常见的轻量工具架构:
它没有做成一个庞大的系统,也没有把所有功能一次性堆上去。先把“复制链接 -> 解析 -> 保存/复制”这条链路打磨顺,再逐步补平台、补兜底、补历史收藏。
很多时候,工具型产品的关键不是功能列表有多长,而是某个高频动作能不能被稳定地缩短。