skill_satire_scraper.md 17.7 KB

Skill: Satire News Scraper for Short Drama

技能ID: skill_satire_news_scraper 版本: v1.0.0 分类: L1-Content / Satire Analysis 置信度: 0.89 创建时间: 2026-03-25 创建者: Evolver Agent


1. 技能描述

从讽刺新闻网站抓取内容,识别荒诞元素,评估短剧改编潜力,输出结构化数据供短剧创作使用。

1.1 核心能力

  • 多源抓取: 支持 The Onion、ClickHole、Babylon Bee 等8个主流讽刺新闻源
  • 讽刺识别: 自动识别7种讽刺类型,计算荒诞指数(absurdity_score)
  • 短剧适配: 评估内容的短剧改编潜力,提取场景/人物/对话元素
  • 智能输出: 生成标准化的JSON输出,包含完整的短剧改编建议

1.2 支持的数据源

网站 类型 抓取方式 优先级
The Onion 死板式新闻 网页抓取 P0
ClickHole 病毒内容 RSS P0
Babylon Bee 保守讽刺 RSS P1
Reductress 女性媒体讽刺 RSS P1
The Hard Times 朋克文化 RSS P1
McSweeney's 文学幽默 RSS P2
The Beaverton 加拿大讽刺 RSS P2
Waterford Whispers 爱尔兰讽刺 RSS P2

2. 输入参数

{
  "sources": {
    "type": "array",
    "description": "目标讽刺新闻源列表",
    "default": ["the_onion", "clickhole", "babylon_bee"],
    "items": {
      "type": "string",
      "enum": ["the_onion", "clickhole", "babylon_bee", "reductress", "the_hard_times", "mcsweeneys", "the_beaverton", "waterford_whispers"]
    }
  },
  "fetch_mode": {
    "type": "string",
    "description": "抓取模式",
    "default": "rss",
    "enum": ["rss", "web", "auto"]
  },
  "limit_per_source": {
    "type": "integer",
    "description": "每源抓取数量",
    "default": 20,
    "minimum": 1,
    "maximum": 100
  },
  "min_absurdity_score": {
    "type": "number",
    "description": "最小荒诞分数阈值",
    "default": 6.0,
    "minimum": 0,
    "maximum": 10
  },
  "adaptation_filter": {
    "type": "string",
    "description": "短剧适配等级筛选",
    "default": "B",
    "enum": ["A", "B", "C", "D", "all"]
  },
  "include_political": {
    "type": "boolean",
    "description": "是否包含政治讽刺内容(需本地化)",
    "default": false
  },
  "extract_drama_elements": {
    "type": "boolean",
    "description": "是否提取短剧元素(场景/人物/对话)",
    "default": true
  }
}

3. 输出Schema

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["metadata", "articles"],
  "properties": {
    "metadata": {
      "type": "object",
      "required": ["fetch_time", "total_articles", "sources"],
      "properties": {
        "fetch_time": { "type": "string", "format": "date-time" },
        "total_articles": { "type": "integer" },
        "sources": { "type": "array", "items": { "type": "string" } },
        "filter_stats": {
          "type": "object",
          "properties": {
            "fetched": { "type": "integer" },
            "filtered_by_absurdity": { "type": "integer" },
            "filtered_by_adaptation": { "type": "integer" },
            "included": { "type": "integer" }
          }
        }
      }
    },
    "articles": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/SatireArticle"
      }
    }
  },
  "definitions": {
    "SatireArticle": {
      "type": "object",
      "required": ["id", "title", "content", "source", "satire_analysis"],
      "properties": {
        "id": { "type": "string", "description": "文章唯一ID" },
        "title": { "type": "string", "description": "文章标题" },
        "content": { "type": "string", "description": "文章正文内容" },
        "source": { "type": "string", "description": "来源网站" },
        "category": { "type": "string", "description": "文章分类" },
        "url": { "type": "string", "description": "原文链接" },
        "published_at": { "type": "string", "format": "date-time" },
        "satire_analysis": {
          "type": "object",
          "required": ["absurdity_score", "satire_type", "markers"],
          "properties": {
            "absurdity_score": {
              "type": "number",
              "minimum": 0,
              "maximum": 10,
              "description": "荒诞指数 0-10"
            },
            "satire_type": {
              "type": "string",
              "enum": ["deadpan_news", "viral_content", "conservative_satire", "feminist_media_satire", "subculture_satire", "literary_humor", "national_satire", "mild_satire", "none"]
            },
            "markers": {
              "type": "array",
              "items": { "type": "string" },
              "description": "命中的讽刺标记词"
            },
            "premise": { "type": "string", "description": "荒诞前提摘要" },
            "twist": { "type": "string", "description": "反转/笑点说明" }
          }
        },
        "drama_adaptation": {
          "type": "object",
          "required": ["adaptability_rating", "recommended_duration"],
          "properties": {
            "adaptability_rating": {
              "type": "string",
              "enum": ["A", "B", "C", "D"],
              "description": "A=直接改编, B=需调整, C=大幅改写, D=不适合"
            },
            "recommended_duration": {
              "type": "string",
              "enum": ["15s", "60s", "3-5min"],
              "description": "建议短剧时长"
            },
            "adaptation_notes": { "type": "string", "description": "改编建议" },
            "localization_required": { "type": "boolean" }
          }
        },
        "drama_elements": {
          "type": "object",
          "properties": {
            "setting": {
              "type": "object",
              "properties": {
                "location": { "type": "string" },
                "mood": { "type": "string" }
              }
            },
            "characters": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "role": { "type": "string", "enum": ["protagonist", "antagonist", "supporting"] },
                  "type": { "type": "string" },
                  "trait": { "type": "string" }
                }
              }
            },
            "dialogue": {
              "type": "array",
              "items": { "type": "string" },
              "description": "可直接使用的对话"
            },
            "visual_gags": {
              "type": "array",
              "items": { "type": "string" },
              "description": "视觉笑点建议"
            }
          }
        }
      }
    }
  }
}

4. 执行逻辑

4.1 主流程

┌─────────────────┐
│  1. 参数校验     │
└────────┬────────┘
         ▼
┌─────────────────┐
│  2. 数据源抓取   │ ← RSS/网页抓取
└────────┬────────┘
         ▼
┌─────────────────┐
│  3. 数据清洗     │ ← 去重、格式化
└────────┬────────┘
         ▼
┌─────────────────┐
│  4. 讽刺分析     │ ← 计算absurdity_score
└────────┬────────┘
         ▼
┌─────────────────┐
│  5. 短剧适配评估 │ ← 评级A/B/C/D
└────────┬────────┘
         ▼
┌─────────────────┐
│  6. 元素提取     │ ← 场景/人物/对话
└────────┬────────┘
         ▼
┌─────────────────┐
│  7. 输出结果     │ ← 生成JSON
└─────────────────┘

4.2 数据源抓取策略

RSS 模式(推荐)

RSS_FEEDS = {
    "clickhole": "https://clickhole.com/feed/",
    "babylon_bee": "https://babylonbee.com/feed/",
    "reductress": "https://reductress.com/feed/",
    "the_hard_times": "https://thehardtimes.net/feed/",
    "mcsweeneys": "http://feeds.feedburner.com/mcsweeneys",
    "the_beaverton": "https://www.thebeaverton.com/feed/",
    "waterford_whispers": "https://waterfordwhispersnews.com/feed/"
}

网页抓取模式(The Onion)

WEB_SELECTORS = {
    "container": "article.post",
    "title": "h1.headline",
    "content": ".entry-content",
    "category": ".category-tag"
}

4.3 Absurdity Score 计算算法

def calculate_absurdity_score(article):
    """
    计算荒诞指数 (0-10)
    基于四大维度加权计算
    """
    # 1. 标题讽刺标记 (0-3分)
    headline_markers = {
        "local_man": ["Local Man", "Area Man", "Area Woman"],
        "study_finds": ["Study Finds", "Report:", "Experts Say"],
        "absurd_escalation": ["somehow", "unexpectedly", "tragically"],
        "literal_interpretation": ["literally", "actually", "definitely"],
        "emotional_manipulation": ["Heartbreaking", "Devastating", "Inspiring"]
    }

    headline_score = 0
    title_lower = article.title.lower()
    for marker_type, markers in headline_markers.items():
        if any(m.lower() in title_lower for m in markers):
            headline_score += 0.6
    headline_score = min(3, headline_score)

    # 2. 内容荒诞密度 (0-2.5分)
    absurdity_keywords = [
        "aliens", "time travel", "talking", "supernatural",
        "billion", "million", "37", "entire family",
        "turns out", "little did they know", "shocked to discover"
    ]
    content_lower = article.content.lower()
    keyword_count = sum(1 for kw in absurdity_keywords if kw in content_lower)
    density_score = min(2.5, keyword_count * 0.3)

    # 3. 叙事反转强度 (0-2.5分)
    twist_indicators = [
        ("somehow", 1.0),
        ("turns out", 0.8),
        ("unexpectedly", 0.8),
        ("but actually", 0.7),
        ("discovered that", 0.6)
    ]
    twist_score = sum(score for indicator, score in twist_indicators
                      if indicator in content_lower)
    twist_score = min(2.5, twist_score)

    # 4. 短剧适配度 (0-2分)
    adaptability_indicators = {
        "scene_clear": ["office", "home", "restaurant", "party"],
        "dialogue_rich": ['"', "said", "told"],
        "character_distinct": ["local man", "dad", "boss", "teacher"]
    }
    adapt_score = 0
    for indicator_type, keywords in adaptability_indicators.items():
        if any(kw in content_lower for kw in keywords):
            adapt_score += 0.7
    adapt_score = min(2, adapt_score)

    total = headline_score + density_score + twist_score + adapt_score
    return round(total, 1)

4.4 讽刺类型识别规则

类型 识别规则 代表源
deadpan_news AP新闻格式 + 荒谬内容 The Onion
viral_content 列表/测验/点击诱饵格式 ClickHole
conservative_satire 传统价值观视角反转 Babylon Bee
feminist_media_satire 女性媒体风格 + relatability Reductress
subculture_satire 亚文化黑话 + 内部梗 The Hard Times
literary_humor 高品位文学风格 McSweeney's
national_satire 国家/地域特色讽刺 Beaverton/Waterford

识别函数

def detect_satire_type(article):
    source_mapping = {
        "the_onion": "deadpan_news",
        "clickhole": "viral_content",
        "babylon_bee": "conservative_satire",
        "reductress": "feminist_media_satire",
        "the_hard_times": "subculture_satire",
        "mcsweeneys": "literary_humor",
        "the_beaverton": "national_satire",
        "waterford_whispers": "national_satire"
    }

    # 基于来源初步判断
    base_type = source_mapping.get(article.source, "unknown")

    # 基于内容微调
    title_lower = article.title.lower()

    if "quiz" in title_lower or "which" in title_lower:
        return "viral_content"
    elif any(x in title_lower for x in ["study finds", "report:", "local man"]):
        return "deadpan_news"
    elif "open letter" in title_lower:
        return "literary_humor"

    return base_type

4.5 短剧适配评级算法

def assess_drama_adaptation(article, absurdity_score):
    """
    评估短剧改编潜力,返回 A/B/C/D 评级
    """
    scores = {
        "scene_visualization": 0,  # 场景可视化 (0-2.5)
        "dialogue_richness": 0,    # 对话丰富度 (0-2.5)
        "conflict_clarity": 0,     # 冲突明确性 (0-2.0)
        "twist_impact": 0,         # 反转冲击力 (0-2.0)
        "performance_feasibility": 0  # 表演可行性 (0-1.0)
    }

    content = article.content.lower()
    title = article.title.lower()

    # 场景可视化
    locations = ["office", "home", "restaurant", "party", "wedding", "hospital"]
    if any(loc in content for loc in locations):
        scores["scene_visualization"] = 2.0
    elif "local" in title:
        scores["scene_visualization"] = 1.5
    else:
        scores["scene_visualization"] = 1.0

    # 对话丰富度
    quote_count = content.count('"')
    if quote_count >= 4:
        scores["dialogue_richness"] = 2.5
    elif quote_count >= 2:
        scores["dialogue_richness"] = 1.8
    elif "said" in content or "told" in content:
        scores["dialogue_richness"] = 1.2
    else:
        scores["dialogue_richness"] = 0.8

    # 冲突明确性
    conflict_keywords = ["refuses", "demanding", "accused", "argument", "fight"]
    if any(kw in content for kw in conflict_keywords):
        scores["conflict_clarity"] = 2.0
    elif absurdity_score > 7:
        scores["conflict_clarity"] = 1.5
    else:
        scores["conflict_clarity"] = 1.0

    # 反转冲击力
    if absurdity_score >= 8:
        scores["twist_impact"] = 2.0
    elif absurdity_score >= 6.5:
        scores["twist_impact"] = 1.5
    else:
        scores["twist_impact"] = 0.8

    # 表演可行性
    if any(x in content for x in ["dragon", "alien", "explosion"]):
        scores["performance_feasibility"] = 0.3  # 需要特效
    else:
        scores["performance_feasibility"] = 1.0

    total = sum(scores.values())

    # 评级标准
    if total >= 8 and absurdity_score >= 7:
        return "A"  # 直接改编
    elif total >= 6.5:
        return "B"  # 需少量调整
    elif total >= 4.5:
        return "C"  # 需大幅改写
    else:
        return "D"  # 不适合

5. 错误处理

5.1 错误码定义

错误码 描述 处理建议
RSS_FETCH_ERROR RSS feed获取失败 切换至网页抓取模式
WEB_SCRAPE_ERROR 网页抓取失败 检查选择器,降低请求频率
RATE_LIMITED 请求频率超限 降低频率至20req/min,使用代理
PARSE_ERROR 内容解析失败 检查编码(强制UTF-8)
NO_RESULTS 无匹配结果 放宽筛选条件
TIMEOUT_ERROR 请求超时 增加超时时间,分批请求

5.2 重试策略

retry_policy = {
    "max_attempts": 3,
    "backoff_factor": 2,
    "initial_delay": 1,
    "retryable_errors": ["TIMEOUT_ERROR", "RATE_LIMITED", "RSS_FETCH_ERROR"]
}

6. 示例代码

6.1 Python 调用示例

import requests
import json

# 调用 Skill API
response = requests.post(
    "https://api.lemonai.com/v1/skills/satire-scraper/execute",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "sources": ["the_onion", "clickhole", "babylon_bee"],
        "fetch_mode": "auto",
        "limit_per_source": 10,
        "min_absurdity_score": 7.0,
        "adaptation_filter": "B",
        "extract_drama_elements": True
    }
)

result = response.json()

# 处理高适配度内容
for article in result["articles"]:
    if article["drama_adaptation"]["adaptability_rating"] == "A":
        print(f"[{article['satire_analysis']['absurdity_score']}] {article['title']}")
        print(f"讽刺类型: {article['satire_analysis']['satire_type']}")
        print(f"荒诞前提: {article['satire_analysis']['premise']}")
        print(f"建议时长: {article['drama_adaptation']['recommended_duration']}")
        print("---")

6.2 直接抓取示例

import feedparser

# RSS 抓取示例
def fetch_from_rss(source_url):
    feed = feedparser.parse(source_url)
    articles = []

    for entry in feed.entries[:20]:
        article = {
            "title": entry.title,
            "content": entry.get("summary", entry.get("description", "")),
            "url": entry.link,
            "published_at": entry.get("published", "")
        }

        # 应用 absurdity_score 算法
        article["absurdity_score"] = calculate_absurdity_score(article)

        if article["absurdity_score"] >= 6.0:
            articles.append(article)

    return articles

# 抓取 ClickHole
clickhole_articles = fetch_from_rss("https://clickhole.com/feed/")

7. 性能指标

指标 基准值 说明
单次请求耗时 抓取3个源,每源20条
日调用上限 500次 受数据源限制
成功率 > 95% 包括重试后成功
absurdity_score MAE 与人工评分平均绝对误差
类型分类准确率 > 75% 讽刺类型识别准确率

8. 版本历史

版本 日期 变更内容
v1.0.0 2026-03-25 初始版本,支持8个讽刺新闻源

9. 参考资料


技能定义版本: v1.0.0 最后更新: 2026-03-25 维护者: Evolver Agent