Skill: if530770/bilibili-video-extractor (version 1.0.0)

--- name: bilibili-video-extractor description: 📖📊 扒B站视频数据 + 🧠 拆爆款套路 + 💡 直接抄作业的运营建议 🔍 一键抓取视频的播放量、点赞、收藏、弹幕……数据小能手就是我～ 💬 连弹幕里的“哈哈哈哈”和“泪目了”都给你翻译成人话，读懂用户真实小九九 🧠 帮你盘一盘：这个视频到底凭什么爆？运气？实力？还是那只猫？ 💡 最后甩你几个能直接套用的标题公式 + 选题灵感，拿走就能用，用了就想火🔥 适合谁？内容运营小苦瓜、选题策划头秃人、爆款拆解上瘾选手——统统来！🎯 让爆款不再靠玄学，靠咱的小工具～✨ version:1.0.0 --- # 🎬 B站（bilibili/哔哩哔哩）视频深度分析专家 > ### "别再傻乎乎地盯着播放量了，让我帮你看懂爆款的秘密！" --- ## ✨ 我是谁 Hey！我是**B站爆款拆解小能手** 🔍 别人看到的是一个视频，我看到的是**流量密码**、**收藏理由**、和**为什么它能爆**。 我能帮你： - 🎯 从B站视频里挖出有用的运营洞察 - 📊 读懂弹幕里的用户真实想法 - 💡 告诉你这个视频为什么爆、哪里值得抄 - 🚀 给你直接能用的标题公式和内容灵感 --- ## 🎯 我的专长 | 我擅长 | 我更擅长 | | ------------ | -------------------- | | 提取视频数据 | 告诉你数据背后的意思 | | 分析弹幕情绪 | 挖掘用户真正想要什么 | | 罗列视频信息 | 提炼可直接落地的建议 | **我的风格**：不废话，直接给结论，给可执行的方案！ --- ## 👀 适合谁用 - 📝 B站内容运营er - 🔥 想分析竞品爆款的你 - 💭 不知道发什么内容、选题枯竭的你 - 🎓 想学习爆款内容套路的好奇宝宝 触发本技能并需要执行完整流程时，**必须先读取**与本技能同目录下的 `references/core_workflow.md`，并**完整遵循**其中的触发规则、术语规范、数据来源约束、所有操作步骤、自检清单与注意事项。

path: scripts/batch_extract.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
B站视频批量提取脚本
支持多视频并行处理，自动去重，结果汇总
"""

import argparse
import json
import sys
import os
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Dict, Any
from datetime import datetime

# 添加父目录到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from get_video_content import BilibiliExtractor


class BatchExtractor:
    """批量提取器"""

    def __init__(self, max_workers: int = 3, delay: float = 1.0):
        self.max_workers = max_workers
        self.delay = delay
        self.extractor = BilibiliExtractor()
        self.results = []
        self.errors = []

    def parse_urls(self, input_path: str) -> List[str]:
        """从文件或字符串解析URL列表"""
        urls = []

        if os.path.exists(input_path):
            # 从文件读取
            with open(input_path, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith('#'):
                        urls.append(line)
        else:
            # 逗号分隔的字符串
            urls = [u.strip() for u in input_path.split(',') if u.strip()]

        # 去重
        return list(dict.fromkeys(urls))

    def process_single(self, url: str, mode: str) -> Dict[str, Any]:
        """处理单个视频"""
        video_id = self.extractor.extract_video_id(url)

        if not video_id:
            return {
                "url": url,
                "error": "无法解析视频ID",
                "success": False
            }

        try:
            # 使用摘要模式加快速度
            extract_mode = "summary" if mode == "fast" else "full"
            data = self.extractor.extract(url, mode=extract_mode)

            return {
                "url": url,
                "video_id": video_id,
                "title": data.get("info", {}).get("title", ""),
                "owner": data.get("info", {}).get("owner", {}).get("name", ""),
                "stat": data.get("info", {}).get("stat", {}),
                "data": data,
                "success": True
            }
        except Exception as e:
            return {
                "url": url,
                "video_id": video_id,
                "error": str(e),
                "success": False
            }

    def run(self, urls: List[str], mode: str = "full") -> Dict[str, Any]:
        """执行批量处理"""
        total = len(urls)
        print(f"[信息] 开始批量提取，共 {total} 个视频")
        print(f"[信息] 并发数: {self.max_workers}, 模式: {mode}")
        print("-" * 50)

        start_time = time.time()

        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            future_to_url = {
                executor.submit(self.process_single, url, mode): url
                for url in urls
            }

            for i, future in enumerate(as_completed(future_to_url), 1):
                url = future_to_url[future]
                try:
                    result = future.result()
                    if result["success"]:
                        self.results.append(result)
                        title = result.get("title", "未知")[:30]
                        stat = result.get("stat", {})
                        views = stat.get("view", 0)
                        print(f"[{i}/{total}] ✅ {title}... | 播放: {views:,}")
                    else:
                        self.errors.append(result)
                        print(f"[{i}/{total}] ❌ {url} | {result.get('error', '未知错误')}")
                except Exception as e:
                    self.errors.append({"url": url, "error": str(e)})
                    print(f"[{i}/{total}] ❌ {url} | 处理异常")

                # 控制请求频率
                time.sleep(self.delay)

        elapsed = time.time() - start_time

        return self.generate_report(elapsed)

    def generate_report(self, elapsed: float) -> Dict[str, Any]:
        """生成汇总报告"""
        report = {
            "summary": {
                "total": len(self.results) + len(self.errors),
                "success": len(self.results),
                "failed": len(self.errors),
                "elapsed_seconds": round(elapsed, 1),
                "timestamp": datetime.now().isoformat()
            },
            "results": self.results,
            "errors": self.errors,
            "ranking": self.generate_ranking()
        }

        return report

    def generate_ranking(self) -> Dict[str, List]:
        """生成排行榜"""
        if not self.results:
            return {"by_views": [], "by_likes": [], "by_favorites": []}

        # 按播放排序
        by_views = sorted(
            self.results,
            key=lambda x: x.get("stat", {}).get("view", 0),
            reverse=True
        )[:10]

        # 按点赞排序
        by_likes = sorted(
            self.results,
            key=lambda x: x.get("stat", {}).get("like", 0),
            reverse=True
        )[:10]

        # 按收藏排序
        by_favorites = sorted(
            self.results,
            key=lambda x: x.get("stat", {}).get("favorite", 0),
            reverse=True
        )[:10]

        return {
            "by_views": [
                {"title": r.get("title"), "owner": r.get("owner"), "views": r.get("stat", {}).get("view", 0)}
                for r in by_views
            ],
            "by_likes": [
                {"title": r.get("title"), "owner": r.get("owner"), "likes": r.get("stat", {}).get("like", 0)}
                for r in by_likes
            ],
            "by_favorites": [
                {"title": r.get("title"), "owner": r.get("owner"), "favorites": r.get("stat", {}).get("favorite", 0)}
                for r in by_favorites
            ]
        }


def main():
    parser = argparse.ArgumentParser(description="B站视频批量提取工具")
    parser.add_argument("--input", "-i", required=True,
                        help="输入文件路径(每行一个URL)或逗号分隔的URL字符串")
    parser.add_argument("--output", "-o", default="batch_result.json",
                        help="输出文件路径")
    parser.add_argument("--mode", "-m", default="fast",
                        choices=["fast", "full"],
                        help="提取模式: fast(摘要) / full(完整)")
    parser.add_argument("--workers", "-w", type=int, default=3,
                        help="并发数 (默认3)")
    parser.add_argument("--delay", "-d", type=float, default=1.0,
                        help="请求间隔(秒) (默认1.0)")

    args = parser.parse_args()

    extractor = BatchExtractor(max_workers=args.workers, delay=args.delay)
    urls = extractor.parse_urls(args.input)

    if not urls:
        print("[错误] 未找到有效URL")
        sys.exit(1)

    result = extractor.run(urls, mode=args.mode)

    # 保存结果
    with open(args.output, 'w', encoding='utf-8') as f:
        json.dump(result, f, ensure_ascii=False, indent=2)

    # 打印摘要
    summary = result["summary"]
    print("-" * 50)
    print(f"[完成] 成功: {summary['success']}/{summary['total']} | 耗时: {summary['elapsed_seconds']}s")
    print(f"[保存] 结果已保存至: {args.output}")

    # 打印TOP10
    if result["ranking"]["by_views"]:
        print("\n📊 播放量TOP10:")
        for i, item in enumerate(result["ranking"]["by_views"], 1):
            print(f"  {i}. {item['title'][:25]}... | {item['views']:,}")


if __name__ == "__main__":
    main()


path: scripts/.cache/BV1CM411W7HC.json
content: {
  "video_id": "BV1CM411W7HC",
  "url": "https://www.bilibili.com/video/BV1CM411W7HC/",
  "mode": "full",
  "timestamp": "2026-04-14T14:09:24.786641",
  "info": {
    "bvid": "BV1CM411W7HC",
    "aid": 526084919,
    "title": "记住这5条，黄金首饰最全挑选攻略！省钱还能保值~",
    "desc": "",
    "owner": {
      "name": "小聪明张建国",
      "mid": 1581616848,
      "face": "https://i0.hdslb.com/bfs/face/cc8dd7fb27186e799469b7bdd82e6581c8a0f082.jpg"
    },
    "stat": {
      "aid": 526084919,
      "view": 1752638,
      "danmaku": 6,
      "reply": 31,
      "favorite": 108472,
      "coin": 3267,
      "share": 533,
      "now_rank": 0,
      "his_rank": 0,
      "like": 16035,
      "dislike": 0,
      "evaluation": "",
      "vt": 0
    },
    "duration": 74,
    "pubdate": 1678960800,
    "pubdate_str": "2023-03-16 18:00",
    "pages": [
      {
        "cid": 1055155443,
        "page": 1,
        "from": "vupload",
        "part": "记住这5条，黄金首饰最全挑选攻略！省钱还能保值~",
        "duration": 74,
        "vid": "",
        "weblink": "",
        "dimension": {
          "width": 1920,
          "height": 1080,
          "rotate": 0
        },
        "first_frame": "http://i0.hdslb.com/bfs/storyff/n230316qn1lymged1opxho25m07qi5vs_firsti.jpg",
        "ctime": 1678933890
      }
    ],
    "tname": "",
    "tags": []
  },
  "danmaku": [
    {
      "time": 0.94,
      "text": "省流：直接买金条",
      "type": 1,
      "color": "25",
      "timestamp": 1678987492,
      "page": "记住这5条，黄金首饰最全挑选攻略！省钱还能保值~"
    },
    {
      "time": 1.965,
      "text": "现在还有人去线下买吗那么贵，我都在水贝淘上买的，种类还多",
      "type": 1,
      "color": "25",
      "timestamp": 1744700366,
      "page": "记住这5条，黄金首饰最全挑选攻略！省钱还能保值~"
    },
    {
      "time": 9.841,
      "text": "还有人在线下买吗，我都在水贝淘买的种类多还有证书",
      "type": 1,
      "color": "25",
      "timestamp": 1744700400,
      "page": "记住这5条，黄金首饰最全挑选攻略！省钱还能保值~"
    },
    {
      "time": 49.854,
      "text": "刚和我妈从菜百里出来……",
      "type": 1,
      "color": "25",
      "timestamp": 1679076596,
      "page": "记住这5条，黄金首饰最全挑选攻略！省钱还能保值~"
    },
    {
      "time": 61.212,
      "text": "一体成型，实心",
      "type": 1,
      "color": "25",
      "timestamp": 1695065657,
      "page": "记住这5条，黄金首饰最全挑选攻略！省钱还能保值~"
    },
    {
      "time": 62.679,
      "text": "听你的，以后就买金砖",
      "type": 1,
      "color": "25",
      "timestamp": 1679241901,
      "page": "记住这5条，黄金首饰最全挑选攻略！省钱还能保值~"
    }
  ],
  "comments": []
}

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)