From c612dcd0da55d845beac0dead07c150960e75108 Mon Sep 17 00:00:00 2001 From: karuo Date: Sun, 22 Feb 2026 13:08:18 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=84=20=E5=8D=A1=E8=8B=A5AI=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=202026-02-22=2013:08=20|=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=EF=BC=9A=E5=8D=A1=E6=9C=A8=E3=80=81=E6=80=BB=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=E4=B8=8E=E5=85=A5=E5=8F=A3=E3=80=81=E8=BF=90=E8=90=A5=E4=B8=AD?= =?UTF-8?q?=E6=9E=A2=E5=B7=A5=E4=BD=9C=E5=8F=B0=20|=20=E6=8E=92=E9=99=A4?= =?UTF-8?q?=20>20MB:=208=20=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../木叶_视频内容/抖音视频解析/SKILL.md | 106 +++++++++ .../抖音视频解析/参考资料/ID与文案解析规则.md | 57 +++++ .../抖音视频解析/脚本/douyin_parse.py | 224 ++++++++++++++++++ SKILL_REGISTRY.md | 1 + 运营中枢/工作台/gitea_push_log.md | 1 + 运营中枢/工作台/代码管理.md | 1 + 6 files changed, 390 insertions(+) create mode 100644 03_卡木(木)/木叶_视频内容/抖音视频解析/SKILL.md create mode 100644 03_卡木(木)/木叶_视频内容/抖音视频解析/参考资料/ID与文案解析规则.md create mode 100644 03_卡木(木)/木叶_视频内容/抖音视频解析/脚本/douyin_parse.py diff --git a/03_卡木(木)/木叶_视频内容/抖音视频解析/SKILL.md b/03_卡木(木)/木叶_视频内容/抖音视频解析/SKILL.md new file mode 100644 index 00000000..96daac32 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/抖音视频解析/SKILL.md @@ -0,0 +1,106 @@ +--- +name: 抖音视频解析 +description: 抖音链接 → 解析ID → 提取文案 → 下载视频。输入任一抖音视频链接,自动解析aweme_id/video_id/file_id、提取标题与文案、下载无水印视频。 +triggers: 抖音视频、抖音链接、抖音解析、抖音下载、提取抖音文案、抖音无水印 +owner: 木叶 +group: 木 +version: "1.0" +updated: "2026-02-22" +--- + +# 抖音视频解析 + +> **输入**:抖音视频链接(短链 `v.douyin.com/xxx` 或完整 `www.douyin.com/video/xxx`) +> **输出**:解析出的 ID、文案(标题/正文/话题)、下载的视频文件 + +--- + +## 核心能力 + +1. **解析 ID**:从链接或页面提取 `aweme_id`、`video_id`、`file_id` +2. **提取文案**:从页面 metadata 提取标题、正文、话题标签 +3. **下载视频**:获取无水印视频并保存到本地 + +--- + +## 触发词 + +- 抖音视频、抖音链接 +- 抖音解析、抖音下载 +- 提取抖音文案、抖音无水印 + +--- + +## 执行步骤 + +### 用户提供抖音链接时 + +1. **解析链接**:识别短链 / 完整链接,提取 `aweme_id` +2. **获取页面**:requests 获取页面(移动端 UA);失败时可用 MCP 浏览器访问 +3. **提取文案**:从页面 title、meta、`__vid`、`ROUTER_DATA` 等提取标题、正文、话题 +4. **提取视频 URL**:从 `` 或 JSON 中获取 CDN/Play 链接 +5. **下载视频**:requests 流式下载,优先无水印链接(`playwm`→`play`) + +### 一键命令 + +```bash +cd /Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/抖音视频解析/脚本 +python3 douyin_parse.py "https://v.douyin.com/SpVK8mlOUUo/" + +# 仅解析不下载 +python3 douyin_parse.py "https://v.douyin.com/xxx" --no-download + +# 指定输出目录 +python3 douyin_parse.py "https://v.douyin.com/xxx" -o /path/to/output +``` + +--- + +## 相关文件 + +| 文件 | 说明 | +|------|------| +| `脚本/douyin_parse.py` | 解析与下载主脚本 | +| `参考资料/ID与文案解析规则.md` | ID、文案、视频 URL 提取规则说明 | + +--- + +## 输出目录 + +- **视频文件**:`/Users/karuo/Documents/卡若Ai的文件夹/视频/`(或 `-o` 指定) +- **文案 JSON**:同目录下 `{aweme_id}_文案.json` + +--- + +## AI 执行说明(Cursor) + +当用户给出抖音链接时: + +1. 读本 SKILL.md +2. 执行 `python3 douyin_parse.py "用户提供的链接"` +3. 若 requests 被拦截(403/超时),使用 MCP 浏览器: + - `browser_navigate` 到链接 + - `browser_snapshot` 获取页面 title(含文案) + - 从 snapshot 或页面源码提取 `__vid`、`video_id`、`file_id`、`` + - 将提取结果传给脚本或手动拼装下载 URL +4. 结果按复盘格式回复用户 + +--- + +## 依赖 + +- Python 3.8+ +- requests +- 可选:MCP 浏览器(requests 失败时) + +--- + +## 解析规则(简要) + +| 字段 | 来源 | 示例 | +|------|------|------| +| aweme_id / __vid | URL 或 `__vid=` | 7607519346462286491 | +| video_id | `video_id=` | v02f52g10003d69l7afog65sirkjgcag | +| file_id | `file_id=` | f7a8f7b2af594e6d93f3588e7ff4ec66 | +| 文案 | 页面 title、meta、ROUTER_DATA | 标题+正文+话题 | +| 视频 URL | `` 或 play API | douyinvod.com / aweme/v1/play | diff --git a/03_卡木(木)/木叶_视频内容/抖音视频解析/参考资料/ID与文案解析规则.md b/03_卡木(木)/木叶_视频内容/抖音视频解析/参考资料/ID与文案解析规则.md new file mode 100644 index 00000000..5156e107 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/抖音视频解析/参考资料/ID与文案解析规则.md @@ -0,0 +1,57 @@ +# 抖音视频 ID 与文案解析规则 + +> 用于从抖音页面/HTML 中提取 aweme_id、video_id、file_id、文案、视频 URL。 + +--- + +## 一、链接格式 + +| 格式 | 示例 | 说明 | +|------|------|------| +| 短链 | `https://v.douyin.com/SpVK8mlOUUo/` | 需 resolve 到完整链接 | +| 完整链接 | `https://www.douyin.com/video/7607519346462286491` | 可直接提取 aweme_id | + +--- + +## 二、ID 解析规则 + +| 字段 | 来源 | 正则/位置 | 示例 | +|------|------|----------|------| +| **aweme_id** | URL `/video/(\d+)` 或 `__vid=` | `r"/video/(\d+)"` | `7607519346462286491` | +| **video_id** | HTML `video_id=` | `r'video_id["\']?\s*[:=]\s*["\']?([a-zA-Z0-9_]+)'` | `v02f52g10003d69l7afog65sirkjgcag` | +| **file_id** | HTML `file_id=` | `r'file_id["\']?\s*[:=]\s*["\']?([a-f0-9]{32})'` | `f7a8f7b2af594e6d93f3588e7ff4ec66` | + +--- + +## 三、文案提取规则 + +| 字段 | 来源 | 说明 | +|------|------|------| +| **title** | `` 或 ROUTER_DATA | 页面标题,通常含标题+正文 | +| **desc** | ROUTER_DATA 或 title 后半部分 | 正文描述 | +| **hashtags** | 正文/标题中的 `#xxx` | 话题标签 | + +--- + +## 四、视频 URL 提取 + +| 来源 | 说明 | +|------|------| +| `<source src="...">` | 视频标签中的 CDN 直链 | +| `window._ROUTER_DATA` | JSON 中的 play_addr.url_list 或 url_list | +| 无水印 | 将 URL 中的 `playwm` 替换为 `play` | + +--- + +## 五、Play API 格式(备用) + +``` +https://www.douyin.com/aweme/v1/play/ + ?file_id=xxx + &video_id=xxx + &sign=xxx + &uifid=xxx + ... +``` + +需 sign、uifid 等动态参数,CDN 直链或 ROUTER_DATA 更稳定。 diff --git a/03_卡木(木)/木叶_视频内容/抖音视频解析/脚本/douyin_parse.py b/03_卡木(木)/木叶_视频内容/抖音视频解析/脚本/douyin_parse.py new file mode 100644 index 00000000..b142fb93 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/抖音视频解析/脚本/douyin_parse.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +抖音视频解析:链接 → 解析ID → 提取文案 → 下载视频 +输入:抖音短链 (v.douyin.com) 或完整链接 (www.douyin.com/video/xxx) +输出:aweme_id, video_id, file_id, 文案(标题/正文/话题), 视频文件 +""" + +import argparse +import json +import re +import sys +from pathlib import Path +import requests + +# 默认输出目录:卡若Ai的文件夹/视频 +DEFAULT_OUTPUT = Path.home() / "Documents" / "卡若Ai的文件夹" / "视频" +DEFAULT_OUTPUT.mkdir(parents=True, exist_ok=True) + +# 移动端 UA,减少被拦截 +MOBILE_UA = ( + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1" +) + + +def parse_url_to_aweme_id(url: str) -> str | None: + """从抖音链接提取 aweme_id""" + url = url.strip() + # 完整链接可直接提取 + m = re.search(r"/video/(\d+)", url) + if m: + return m.group(1) + return None + + +def fetch_and_parse(url: str) -> tuple[dict, str | None]: + """ + 请求视频页面,解析 ID、文案、视频 URL。 + 支持短链 v.douyin.com 或完整链接。 + 返回 (info_dict, video_url) + """ + url = url.strip() + # 短链需先 resolve 到完整链接 + if "v.douyin.com" in url: + try: + r = requests.get(url, allow_redirects=True, timeout=15, headers={"User-Agent": MOBILE_UA}) + url = r.url + html = r.text + except Exception as e: + return {"error": str(e), "aweme_id": None}, None + else: + try: + r = requests.get(url, headers={"User-Agent": MOBILE_UA, "Referer": "https://www.douyin.com/"}, timeout=15) + r.raise_for_status() + html = r.text + except Exception as e: + return {"error": str(e), "aweme_id": None}, None + + aweme_id = parse_url_to_aweme_id(url) + info = { + "aweme_id": aweme_id or "unknown", + "video_id": None, + "file_id": None, + "title": "", + "desc": "", + "hashtags": [], + "author": "", + } + video_url = None + + # 1. 解析 __vid, video_id, file_id + for pattern, key in [ + (r'["\']?__vid["\']?\s*[:=]\s*["\']?(\d+)["\']?', "aweme_id"), + (r'video_id["\']?\s*[:=]\s*["\']?([a-zA-Z0-9_]+)["\']?', "video_id"), + (r'file_id["\']?\s*[:=]\s*["\']?([a-f0-9]{32})["\']?', "file_id"), + ]: + m = re.search(pattern, html) + if m: + info[key] = m.group(1) + + # 2. 从 <source src="..."> 提取视频 URL + src_match = re.search(r'<source[^>]+src=["\']([^"\']+)["\']', html) + if src_match: + video_url = src_match.group(1) + if "&" in video_url: + video_url = video_url.replace("&", "&") + + # 3. 从 ROUTER_DATA 提取视频 URL 和文案(备选) + router = re.search(r"window\._ROUTER_DATA\s*=\s*(\{.*?\});?\s*</script>", html, re.DOTALL) + if router: + try: + data = json.loads(router.group(1).strip()) + # 深度查找 play_addr / url_list + def find_url(obj): + if isinstance(obj, dict): + if "play_addr" in obj and "url_list" in obj.get("play_addr", {}): + return obj["play_addr"]["url_list"][0] + if "url_list" in obj and obj["url_list"]: + return obj["url_list"][0] + for v in obj.values(): + u = find_url(v) + if u: + return u + elif isinstance(obj, list): + for item in obj: + u = find_url(item) + if u: + return u + return None + + u = find_url(data) + if u and not video_url: + video_url = u.replace("playwm", "play") # 无水印 + + # 文案 + def find_desc(obj, key="desc"): + if isinstance(obj, dict): + if key in obj and obj[key]: + return str(obj[key]) + for v in obj.values(): + r = find_desc(v, key) + if r: + return r + elif isinstance(obj, list): + for item in obj: + r = find_desc(item, key) + if r: + return r + return "" + + info["desc"] = find_desc(data) or info["desc"] + except json.JSONDecodeError: + pass + + # 4. 从 <title> 提取标题(含文案) + title_match = re.search(r"<title>([^<]+)", html) + if title_match: + raw = title_match.group(1).strip() + if " - 抖音" in raw: + raw = raw.replace(" - 抖音", "") + parts = raw.split(None, 1) + info["title"] = parts[0] if parts else raw + if len(parts) > 1 and not info["desc"]: + info["desc"] = parts[1] + + # 5. 话题标签 + tag_matches = re.findall(r"#([^#\s]+)", info.get("desc", "") + " " + info.get("title", "")) + info["hashtags"] = list(dict.fromkeys(tag_matches)) # 去重保序 + + # 6. 若 title 为空,用 desc 首行 + if not info["title"] and info["desc"]: + info["title"] = info["desc"].split("\n")[0].strip()[:80] + + # 无水印处理 + if video_url and "playwm" in video_url: + video_url = video_url.replace("playwm", "play") + + return info, video_url + + +def download_video(url: str, out_path: Path) -> bool: + """下载视频到本地""" + try: + r = requests.get(url, headers={"User-Agent": MOBILE_UA}, stream=True, timeout=60) + r.raise_for_status() + with open(out_path, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + return True + except Exception: + return False + + +def main(): + parser = argparse.ArgumentParser(description="抖音视频解析:链接 → ID + 文案 + 下载") + parser.add_argument("url", help="抖音视频链接(短链或完整)") + parser.add_argument("-o", "--output", type=Path, default=DEFAULT_OUTPUT, help="输出目录") + parser.add_argument("--no-download", action="store_true", help="仅解析,不下载视频") + args = parser.parse_args() + + url = args.url.strip() + if not url: + print("请提供抖音视频链接", file=sys.stderr) + sys.exit(1) + + # 1. 请求并解析页面 + info, video_url = fetch_and_parse(url) + aweme_id = info.get("aweme_id") + if not aweme_id or aweme_id == "unknown": + print("无法解析视频,请检查链接格式或网络", file=sys.stderr) + sys.exit(1) + if info.get("error"): + print(f"解析失败: {info['error']}", file=sys.stderr) + sys.exit(1) + + # 3. 输出文案 JSON + args.output.mkdir(parents=True, exist_ok=True) + caption_path = args.output / f"{aweme_id}_文案.json" + with open(caption_path, "w", encoding="utf-8") as f: + json.dump(info, f, ensure_ascii=False, indent=2) + print(f"✅ 文案已保存: {caption_path}") + + # 4. 下载视频 + if not args.no_download and video_url: + safe_title = re.sub(r'[^\w\s-]', '', info.get("title", aweme_id))[:50] + out_file = args.output / f"{aweme_id}_{safe_title}.mp4" + if download_video(video_url, out_file): + print(f"✅ 视频已下载: {out_file}") + else: + print("⚠️ 视频下载失败,请检查网络或尝试 MCP 浏览器获取页面", file=sys.stderr) + elif args.no_download: + print("已跳过下载 (--no-download)") + else: + print("⚠️ 未解析到视频 URL,可尝试 MCP 浏览器访问页面", file=sys.stderr) + + # 5. 打印摘要 + print("\n--- 解析结果 ---") + print(json.dumps(info, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/SKILL_REGISTRY.md b/SKILL_REGISTRY.md index 38c58678..4fe56e6e 100644 --- a/SKILL_REGISTRY.md +++ b/SKILL_REGISTRY.md @@ -61,6 +61,7 @@ | # | 技能 | 成员 | 触发词 | SKILL 路径 | 一句话 | |:--|:---|:---|:---|:---|:---| | M01 | 视频切片 | 木叶 | **视频剪辑、切片发布、切片动效包装、程序化包装、片头片尾、批量封面、视频包装** | `03_卡木(木)/木叶_视频内容/视频切片/SKILL.md` | 长视频切片+字幕+发布;联动切片动效包装(片头/片尾/程序化) | +| M01b | 抖音视频解析 | 木叶 | **抖音视频、抖音链接、抖音解析、抖音下载、提取抖音文案、抖音无水印** | `03_卡木(木)/木叶_视频内容/抖音视频解析/SKILL.md` | 链接→解析ID→提取文案→下载无水印视频 | | M02 | 网站逆向分析 | 木根 | 逆向分析、模拟登录 | `03_卡木(木)/木根_逆向分析/网站逆向分析/SKILL.md` | 网站 API 分析、SDK 生成 | | M03 | 项目生成 | 木果 | 生成项目、五行模板 | `03_卡木(木)/木果_项目模板/项目生成/SKILL.md` | 按五行模板生成新项目 | | M04 | 开发模板 | 木果 | 创建项目、初始化模板 | `03_卡木(木)/木果_项目模板/开发模板/SKILL.md` | 前后端项目模板库 | diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index f4260947..ecfb3a9b 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -86,3 +86,4 @@ | 2026-02-22 11:44:40 | 🔄 卡若AI 同步 2026-02-22 11:44 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | | 2026-02-22 11:47:38 | 🔄 卡若AI 同步 2026-02-22 11:47 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 8 个 | | 2026-02-22 11:58:17 | 🔄 卡若AI 同步 2026-02-22 11:58 | 更新:金仓、水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | +| 2026-02-22 12:42:56 | 🔄 卡若AI 同步 2026-02-22 12:42 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 1139138c..cab99700 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -89,3 +89,4 @@ | 2026-02-22 11:44:40 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 11:44 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-02-22 11:47:38 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 11:47 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-02-22 11:58:17 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 11:58 | 更新:金仓、水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | +| 2026-02-22 12:42:56 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 12:42 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |