본문으로 건너뛰기
cccskills
무료GitHub에 공개

sns-bunseki

SNS・ソーシャルメディアの監視、言説(ナラティブ)の追跡、オープンソース・インテリジェンス(OSINT)を記者向けに支援する。投稿の拡散経路の追跡、組織的キャンペーンや協調的拡散(コーディネートされた非真正な行動)の分析、速報の横断監視、アカウントの真正性・ボット検知、誤情報パターンの把握に使う。

「拡散を追いたい」「ステマ/世論工作を調べたい」「ボットか見分けたい」「炎上を監視したい」などの依頼で使う。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

sgbms1/ja-news-skills스타 3(GitHub 3 + cccskills 0)

실행 점수 미검증

설치 방법 보기

포함된 파일(1)

  • SKILL.md39.8 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

SNS分析・ソーシャルメディアインテリジェンス

SNS・ソーシャルメディアを報道目的で監視・分析・調査するための体系的な手順とコード集。 記者の問い(何の言説を、どの範囲で、いつまでに追うか)を聞き取り、適切な監視設計・検証・証拠保全を提案する。公開の最終判断と事実確認は記者が行う。

注意:プラットフォームのAPI仕様・料金・利用規約、利用可能なツールは頻繁に変わる。本書の各値・ツールの稼働状況は設計前に最新を確認すること。協調・ボット・真正性のスコアはいずれも仮説(手がかり)であって結論ではない。本スキルは一般的な手引きであり、法的助言ではない。

いつ使うか

  • ある話題・言説がプラットフォーム間でどう広がったかを追う
  • 組織的・協調的な非真正行動(ステマ、世論工作、雇われ拡散)の疑いを調べる
  • 速報を複数のSNSで横断監視する
  • アカウントの相互関係(ネットワーク)を可視化する
  • ボット活動や扇動キャンペーンを検知する
  • デジタル調査の証拠の鎖(チェーン・オブ・カストディ)を作る
  • 削除される前に投稿を保全(アーカイブ)する

日本語プラットフォーム環境の前提

設計の前に、対象とするサービスの特性を押さえる。

## 日本で監視対象になりやすい場
- X(旧Twitter):日本は世界有数の利用国。速報・炎上・世論工作の主戦場。引用・リポストの連鎖が追跡しやすい
- YouTube/ニコニコ動画:動画・配信・コメント。切り抜き拡散の起点になりやすい
- Instagram/TikTok:画像・短尺動画。発信者特定が難しく、転載・リミックスが多い
- LINE:原則クローズド(個別・グループ)。公開アカウント/オープンチャット以外は取得・監視の対象外
- note・ブログ:長文の言説・一次主張の発生源になりやすい
- 5ch等の掲示板・まとめサイト:匿名。言説の発生・増幅・転載の連鎖を追う対象
- Threads/Bluesky/Mastodon・Misskey等の分散SNS:移行先・避難先として規模拡大
- Telegram:公開チャンネルは閲覧可。非公開グループは対象外
  • クローズドな場(LINEの個別/グループ、非公開アカウント、DM、限定公開)は監視・自動取得の対象にしない。例外は、取材源が自発的に提供した範囲を本人の同意のもとで扱う場合に限る(後述の倫理・法令を参照)。
  • 匿名掲示板・まとめサイトは「誰が言ったか」より「いつ・どこで発生し、どう増幅されたか」を追う。発信者の特定は慎重に。

リアルタイム監視

マルチプラットフォーム・トラッカー

from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Optional, Dict
from enum import Enum
import hashlib

class Platform(Enum):
    X = "x"               # 旧Twitter。"twitter"は過去データとの互換のため別途扱う
    TWITTER = "twitter"   # 2023年以前の旧称。レガシーデータ用に保持
    FACEBOOK = "facebook"
    INSTAGRAM = "instagram"
    TIKTOK = "tiktok"
    YOUTUBE = "youtube"
    NICONICO = "niconico"   # ニコニコ動画
    THREADS = "threads"
    BLUESKY = "bluesky"
    MASTODON = "mastodon"   # Misskey等の分散SNS含む
    TELEGRAM = "telegram"
    LINE = "line"           # 公開アカウント/オープンチャットのみ
    NOTE = "note"           # note
    BBS = "bbs"             # 5ch等の掲示板・まとめ

@dataclass
class SocialPost:
    platform: Platform
    post_id: str
    author: str
    content: str
    timestamp: datetime
    url: str
    engagement: Dict[str, int] = field(default_factory=dict)
    media_urls: List[str] = field(default_factory=list)
    archived_urls: List[str] = field(default_factory=list)
    content_hash: str = ""

    def __post_init__(self):
        # 重複検出のため本文をハッシュ化
        self.content_hash = hashlib.md5(
            f"{self.platform.value}:{self.content}".encode()
        ).hexdigest()

@dataclass
class MonitoringQuery:
    keywords: List[str]
    platforms: List[Platform]
    accounts: List[str] = field(default_factory=list)
    hashtags: List[str] = field(default_factory=list)
    exclude_terms: List[str] = field(default_factory=list)
    start_date: Optional[datetime] = None

    def to_search_string(self, platform: Platform) -> str:
        """プラットフォーム別の検索クエリ文字列を生成する。"""
        parts = []

        # キーワード
        if self.keywords:
            parts.append(' OR '.join(f'"{k}"' for k in self.keywords))

        # ハッシュタグ
        if self.hashtags:
            parts.append(' OR '.join(f'#{h}' for h in self.hashtags))

        # 除外語
        if self.exclude_terms:
            parts.append(' '.join(f'-{t}' for t in self.exclude_terms))

        return ' '.join(parts)

日本語特有の注意:表記ゆれ(全角/半角、カタカナ/ひらがな/漢字、長音記号「ー」、伏せ字、隠語)でキーワードが分散する。同義語・別表記・誤変換・スラングを keywords に並べる。検索の取りこぼしを前提に、複数クエリで補完する。

速報スパイク検知

from collections import defaultdict
from datetime import datetime, timedelta

class BreakingNewsDetector:
    """キーワード言及の急増(スパイク)を検知する。"""

    def __init__(self, baseline_window_hours: int = 24):
        self.baseline_window = timedelta(hours=baseline_window_hours)
        self.mention_history = defaultdict(list)

    def add_mention(self, keyword: str, timestamp: datetime):
        """キーワードの言及を記録する。"""
        self.mention_history[keyword].append(timestamp)
        # 古いデータを間引く
        cutoff = datetime.now() - self.baseline_window * 2
        self.mention_history[keyword] = [
            t for t in self.mention_history[keyword] if t > cutoff
        ]

    def is_spiking(self, keyword: str, threshold_multiplier: float = 3.0) -> bool:
        """ベースラインを超えて急増しているか判定する。"""
        now = datetime.now()
        recent = sum(1 for t in self.mention_history[keyword]
                    if t > now - timedelta(hours=1))

        baseline_hourly = len([
            t for t in self.mention_history[keyword]
            if t > now - self.baseline_window
        ]) / self.baseline_window.total_seconds() * 3600

        if baseline_hourly == 0:
            return recent > 10  # 新規トピック用の暫定しきい値

        return recent > baseline_hourly * threshold_multiplier

    def get_trending(self, top_n: int = 10) -> List[tuple]:
        """スパイクの強さ順にキーワードを返す。"""
        spikes = []
        for keyword in self.mention_history:
            if self.is_spiking(keyword):
                recent = sum(1 for t in self.mention_history[keyword]
                           if t > datetime.now() - timedelta(hours=1))
                spikes.append((keyword, recent))

        return sorted(spikes, key=lambda x: x[1], reverse=True)[:top_n]

アカウント分析

真正性の指標

from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional

@dataclass
class AccountAnalysis:
    username: str
    platform: Platform
    created_date: Optional[datetime] = None
    follower_count: int = 0
    following_count: int = 0
    post_count: int = 0

    # 真正性のシグナル
    profile_photo_is_stock: Optional[bool] = None   # アイコンがストック画像/生成画像か
    bio_contains_keywords: List[str] = field(default_factory=list)
    posts_primarily_reshares: Optional[bool] = None  # 投稿の大半がリポスト/転載か
    posting_pattern_irregular: Optional[bool] = None # 投稿パターンが不自然か
    engagement_ratio_suspicious: Optional[bool] = None  # 反応比率が不自然か

    def calculate_red_flags(self) -> dict:
        """アカウントの不審点をスコア化する。"""
        flags = {}

        # アカウントの作成からの経過
        if self.created_date:
            age_days = (datetime.now() - self.created_date).days
            if age_days < 30:
                flags['new_account'] = f"作成から{age_days}日"

        # フォロワー比率
        if self.following_count > 0:
            ratio = self.follower_count / self.following_count
            if ratio < 0.1:
                flags['low_follower_ratio'] = f"比率: {ratio:.2f}"

        # 投稿頻度
        if self.created_date and self.post_count > 0:
            age_days = max(1, (datetime.now() - self.created_date).days)
            posts_per_day = self.post_count / age_days
            if posts_per_day > 50:
                flags['excessive_posting'] = f"1日あたり約{posts_per_day:.0f}件"

        # ストック画像/生成画像のアイコン
        if self.profile_photo_is_stock:
            flags['stock_profile_photo'] = "アイコンがストック/生成画像の疑い"

        return flags

    def authenticity_score(self) -> int:
        """0〜100。高いほど真正(人間の本人運用)の可能性が高い。"""
        score = 100
        flags = self.calculate_red_flags()

        penalty_per_flag = 20
        score -= len(flags) * penalty_per_flag

        return max(0, score)

このスコアは手がかり。新規アカウント=偽、ではない(被災時の臨時開設、匿名の内部告発など正当な理由は多い)。必ず人間が裏取りする。

ネットワークの可視化

from collections import defaultdict
from typing import Set, Dict

class AccountNetwork:
    """アカウント間の関係を地図化する。"""

    def __init__(self):
        self.interactions = defaultdict(lambda: defaultdict(int))
        self.accounts = {}

    def add_interaction(self, from_account: str, to_account: str,
                       interaction_type: str = "mention"):
        """アカウント間のやり取り(メンション・リポスト等)を記録する。"""
        self.interactions[from_account][to_account] += 1

    def find_clusters(self, min_interactions: int = 3) -> List[Set[str]]:
        """頻繁にやり取りするアカウント群(クラスタ)を見つける。"""
        # 最小しきい値で隣接関係を構築
        adjacency = defaultdict(set)
        for from_acc, targets in self.interactions.items():
            for to_acc, count in targets.items():
                if count >= min_interactions:
                    adjacency[from_acc].add(to_acc)
                    adjacency[to_acc].add(from_acc)

        # 連結成分を探索
        visited = set()
        clusters = []

        for account in adjacency:
            if account in visited:
                continue

            cluster = set()
            stack = [account]

            while stack:
                current = stack.pop()
                if current in visited:
                    continue
                visited.add(current)
                cluster.add(current)
                stack.extend(adjacency[current] - visited)

            if len(cluster) > 1:
                clusters.append(cluster)

        return sorted(clusters, key=len, reverse=True)

    def coordination_score(self, accounts: Set[str]) -> float:
        """アカウント群がどれだけ協調的に見えるかをスコア化する。"""
        if len(accounts) < 2:
            return 0.0

        total_possible = len(accounts) * (len(accounts) - 1)
        actual_connections = 0

        for acc in accounts:
            for other in accounts:
                if acc != other and self.interactions[acc][other] > 0:
                    actual_connections += 1

        return actual_connections / total_possible if total_possible > 0 else 0

言説(ナラティブ)の追跡

主張の伝播トラッカー

from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict, Optional

@dataclass
class Claim:
    text: str
    first_seen: datetime
    first_seen_url: str
    variations: List[str] = field(default_factory=list)   # 言い換え・派生表現
    appearances: List[Dict] = field(default_factory=list)

    def add_appearance(self, url: str, platform: Platform,
                       timestamp: datetime, author: str):
        """この主張が現れた場所を記録する。"""
        self.appearances.append({
            'url': url,
            'platform': platform.value,
            'timestamp': timestamp,
            'author': author
        })

    def spread_timeline(self) -> List[Dict]:
        """主張の拡散を時系列で返す。"""
        return sorted(self.appearances, key=lambda x: x['timestamp'])

    def platforms_reached(self) -> Dict[str, int]:
        """プラットフォーム別の出現数を数える。"""
        counts = defaultdict(int)
        for app in self.appearances:
            counts[app['platform']] += 1
        return dict(counts)

    def velocity(self, window_hours: int = 24) -> float:
        """拡散速度(直近の1時間あたり出現数)を計算する。"""
        if not self.appearances:
            return 0.0

        recent = [
            a for a in self.appearances
            if a['timestamp'] > datetime.now() - timedelta(hours=window_hours)
        ]
        return len(recent) / window_hours

言説追跡の要は初出(ペイシェント・ゼロ)の特定。最初に言った人・媒体、最初の改変、最初に大手アカウント/メディアが拾った時点を押さえると、自然拡散か仕掛けられた拡散かの判断材料になる。

ハッシュタグ分析

from collections import Counter
from datetime import datetime, timedelta

class HashtagAnalyzer:
    """ハッシュタグの使われ方を分析する。"""

    def __init__(self):
        self.hashtag_posts = defaultdict(list)

    def add_post(self, hashtags: List[str], post: SocialPost):
        """投稿のハッシュタグを記録する。"""
        for tag in hashtags:
            self.hashtag_posts[tag.lower()].append(post)

    def co_occurrence(self, hashtag: str, top_n: int = 10) -> List[tuple]:
        """そのタグと一緒に使われやすいタグを見つける。"""
        co_tags = Counter()

        for post in self.hashtag_posts.get(hashtag.lower(), []):
            # 投稿本文からハッシュタグを抽出
            tags = [
                word.lower() for word in post.content.split()
                if word.startswith('#')
            ]
            for tag in tags:
                if tag != f'#{hashtag.lower()}':
                    co_tags[tag] += 1

        return co_tags.most_common(top_n)

    def posting_pattern(self, hashtag: str) -> Dict:
        """そのタグ付き投稿が現れる時間帯のパターンを分析する。"""
        posts = self.hashtag_posts.get(hashtag.lower(), [])

        hour_counts = Counter(p.timestamp.hour for p in posts)
        day_counts = Counter(p.timestamp.strftime('%A') for p in posts)

        return {
            'by_hour': dict(hour_counts),
            'by_day': dict(day_counts),
            'total_posts': len(posts),
            'unique_authors': len(set(p.author for p in posts))
        }

日本語のハッシュタグは単語が空白で区切られないため、post.content.split() だけでは抽出漏れが出る。正規表現(例 #[^\s##、。,.]+、全角#も対象)で拾うと精度が上がる。時間帯分析は時刻が日本時間(JST)か確認する(APIはUTC返却が多い)。

証拠の保全

消える前にアーカイブする

レート制限・一括処理・壊れたアーカイブUIからの復旧など、本格的なアーカイブ手順は web-archiving(ウェブアーカイブ) スキルを参照。

import re
import requests
from datetime import datetime
from typing import Optional
from urllib.parse import quote, urljoin

class SocialArchiver:
    """削除される前にソーシャル投稿をアーカイブする。"""

    def __init__(self):
        self.archived = {}

    def archive_to_wayback(self, url: str) -> Optional[str]:
        """URLをInternet Archive(Wayback Machine)に送る。

        匿名保存は毎分15件程度に制限され、有料記事や重いJS描画ページは
        黙って取りこぼすことがある。大量に保存するなら Internet Archive の
        S3キーを登録し、Authorization ヘッダを付ける。
        """
        try:
            save_url = f"https://web.archive.org/save/{quote(url, safe='')}"
            response = requests.get(save_url, timeout=30)
            if response.status_code == 200:
                archived_url = response.url
                self.archived[url] = {
                    'wayback': archived_url,
                    'archived_at': datetime.now().isoformat(),
                }
                return archived_url
        except Exception as e:
            print(f"Waybackへの保存に失敗: {e}")
        return None

    def archive_to_archive_today(self, url: str) -> Optional[str]:
        """URLをarchive.todayに送る。

        運用上の注意(2026年時点):archive.todayは運営者特定をめぐる法的圧力や、
        引用ソースとしての信頼性低下が報じられている。Wayback Machineで
        描画できないページの保全には依然有用だが、Internet Archiveを主、
        本サービスを従とし、依拠した場合はその旨を記録する。スクレイパーには
        CAPTCHAを返し、レート制限も厳しい。
        """
        try:
            response = requests.post(
                'https://archive.today/submit/',
                data={'url': url, 'anyway': '1'},
                timeout=60,
                allow_redirects=False,
                headers={'User-Agent': 'Mozilla/5.0 (verification archive bot)'},
            )
            # archive.todayはスナップショットURLを次の2通りで返す:
            #   - 30x で Location: https://archive.today/<snapshot_id>
            #   - 200 で Refresh: 0;url=https://archive.today/<snapshot_id>
            # リダイレクトを黙って追うと /wip/ ページに着いたり正規URLを
            # 見失うので、両方のヘッダを明示的に処理する。
            if response.status_code in (301, 302, 303, 307, 308):
                location = response.headers.get('Location')
                if location:
                    # Locationは相対URLのことがある(RFC 7231)。リクエストURL基準で解決。
                    return urljoin(response.url, location)
            if response.status_code == 200:
                refresh = response.headers.get('Refresh', '')
                # Refreshのキーワードは大文字小文字を区別しない。;区切りのパラメータを含む
                # ことがあるので url= 指示子そのものを拾う。
                m = re.search(r'\burl\s*=\s*(.+)', refresh, re.IGNORECASE)
                if m:
                    target = m.group(1).strip().strip('\'"')
                    return urljoin(response.url, target)
        except Exception as e:
            print(f"archive.todayへの保存に失敗: {e}")
        return None

    def full_archive(self, url: str) -> dict:
        """冗長化のため複数サービスにアーカイブする。"""
        results = {
            'original_url': url,
            'archived_at': datetime.now().isoformat(),
            'archives': {},
        }

        wayback = self.archive_to_wayback(url)
        if wayback:
            results['archives']['wayback'] = wayback

        archive_today = self.archive_to_archive_today(url)
        if archive_today:
            results['archives']['archive_today'] = archive_today

        return results

公開アーカイブに送るのは公開投稿のみ。非公開・限定公開の内容を公開アーカイブに送らない。非公開の素材(取材源提供のエクスポート、限定公開URL等)は、タイムスタンプ付きスクリーンショットや画面録画で保全し、ハッシュ値を取り、各社の証拠保全規程に従って保管する。投稿時刻・取得時刻・取得経路を必ず併記する。

協調的拡散の検知

行動シグナルのチェックリスト

## 協調的な非真正行動(ステマ・世論工作・雇われ拡散)の指標

### タイミングのパターン
- [ ] 複数アカウントが同じ内容を数分以内に投稿
- [ ] 複数アカウントで投稿時刻が同期している
- [ ] 一斉に活動した後に沈黙する(バースト→休眠)
- [ ] 人間の入力速度を超える速さで投稿が出る

### 内容のパターン
- [ ] 複数アカウントで本文が同一・ほぼ同一
- [ ] 同じ画像・動画を複数アカウントが共有
- [ ] 同じ誤字・同じ書式崩れが共通
- [ ] コピペの痕跡(不自然な改行・残った定型文)

### アカウントのパターン
- [ ] 作成時期が近い
- [ ] 命名規則が似ている(名前+数字の連番など)
- [ ] アイコンが汎用画像・ストック画像・生成画像
- [ ] 個人的な投稿がほぼ無く、共有・転載が中心
- [ ] 同じアカウント群をフォローしている
- [ ] 互いに不自然に多く反応し合う

### ネットワークのパターン
- [ ] ネットワーク分析で密なクラスタを形成
- [ ] 同じ外部ソースを一斉に増幅
- [ ] 同じアカウント・同じハッシュタグを標的にする
- [ ] プラットフォームをまたいだ連動が見える

協調スコアの自動算出

def coordination_likelihood(posts: List[SocialPost]) -> dict:
    """投稿群が協調的な活動である可能性をスコア化する。"""

    if len(posts) < 2:
        return {'score': 0, 'signals': []}

    signals = []
    score = 0

    # 本文の重複をチェック
    contents = [p.content for p in posts]
    unique_contents = set(contents)
    if len(unique_contents) < len(contents) * 0.5:
        signals.append("本文の重複が多い")
        score += 30

    # 投稿タイミングの集中をチェック
    timestamps = sorted(p.timestamp for p in posts)
    rapid_posts = 0
    for i in range(1, len(timestamps)):
        if (timestamps[i] - timestamps[i-1]).seconds < 60:
            rapid_posts += 1

    if rapid_posts > len(posts) * 0.3:
        signals.append("投稿時刻が不自然に集中")
        score += 25

    # 投稿者の数をチェック
    authors = set(p.author for p in posts)
    if len(authors) > 5 and len(contents) / len(authors) > 2:
        signals.append("少人数の投稿者が似た投稿を多数")
        score += 20

    return {
        'score': min(100, score),
        'signals': signals,
        'posts_analyzed': len(posts),
        'unique_authors': len(authors)
    }

留意:公式RTやテンプレ拡散の呼びかけ、災害時の定型注意喚起など、正当な一斉投稿も同じシグナルを示す。高スコア=工作、と即断しない。スコアは取材の出発点。

プラットフォーム別の取得手段

2026年時点の状況。プラットフォームのAPIは頻繁に変わる。設計前に料金・アクセス可否を必ず確認すること。

X(旧Twitter)の証拠パケット作成手順

Xが記事の中心になるときは、分析や執筆の前に「ソースパケット」を組み立てる。

  1. 取得権限を記録する。 どこから取得したか(公式のX API、X Pro Search、ライセンス契約のデータ事業者、記者本人のアカウント閲覧、取材源提供のエクスポート)を明記し、それがプラットフォームの利用規約と自社の取材倫理に適合することを取得前に確認する。
  2. 取得手段を権限に合わせ、スクレイピングはしない。 一括・プログラム的な取得は公式APIかライセンス契約の事業者に限る。Xの2023年以降の規約は公開投稿でもスクレイピングを禁止している。記者本人のアカウント閲覧、X Pro Search、取材源提供のエクスポートは、サイトに自動アクセスせず手作業で取得・保全する(手順5)。各取得のクエリ・経路をログに残す。
  3. 取得と「行動」を分ける。 投稿・返信・DM・メディアアップロード・アカウント変更・監視・予約・公開といった操作は、デスク(編集責任者)が個別に承認しない限り、証拠ワークフローに混ぜない。
  4. 後から検証する人が必要とする項目を取る。 URL、投稿ID、数値の発信者ID、ハンドル名、表示名、取得した本文、投稿時刻、反応数、返信・引用の文脈、メディアURL、取得クエリ、取得経路、取得時刻。
  5. 直ちにアーカイブし、公開投稿だけを公開アーカイブに送る。 公開投稿のURLはウェブアーカイブに送り、アーカイブURLを生データの隣に保存する。非公開の素材は公開アーカイブに送らず、タイムスタンプ付きスクリーンショットやエクスポートで保全し、ハッシュ値を取り、自社の証拠保全規程に従って保管する。

ここで出る協調スコア・ボットスコア・真正性ラベルは、人間が根拠を確認するまでは結論ではなく手がかりとして扱う。

プラットフォーム調査向けアクセス注意
X(旧Twitter)従量課金の開発者API(developer.x.com)/X Pro Search(X Premium+の一般向け機能)/Brandwatch・Sprinklr等の有料サードパーティ無料の学術・研究枠は2023年初頭に終了。2024年のBasic/Proの月額層は、2026年2月に従量課金(API呼び出し単位の課金、月額なし)へ置換。単価・レート上限は開発者ポータルで要確認。2023年以降の規約はスクレイピングを明示的に禁止
Facebook/InstagramMeta Content Library+同API(研究アクセス)/Junkipedia(無料・報道向け)/NewsWhip(有料)CrowdTangleは2024年8月14日に完全終了。Meta Content Libraryが後継。申請はMetaのポータルで直接。適格性は学術・非営利の研究者が中心で、実務記者は所属機関を通じてのみ通ることが多い
TikTokResearch API(適格な学術・非営利研究者/EUではDSAで認証された研究者)/Exolyt・Pentos(有料)developers.tiktok.comで申請。EU拠点の研究者はDSA第40条で相対的に強いアクセスを持つ
YouTubeYouTube Data API v3既定で1日10,000ユニット(検索は100ユニット=1日約100検索)。増枠は申請制で審査に数週間。報道専用枠は無い
RedditReddit API(非営利研究は無料)/Arctic Shift(Pushshiftの後継、無料)Pushshiftは2023年以降、認証モデレーター限定で報道用途には使えない。Arctic Shiftが現行の後継
BlueskyJetstream(WebSocket上のフィルタ済みJSON、認証不要)/生Firehose既定で公開。Jetstreamが報道向けの入口(フィルタ済みで約850MB/日)。生Firehoseは4〜8GB/時で自前のアーカイブ構築が必要
Threads(Meta)Threads API(投稿・埋め込み用)/Meta Content Library(研究用)一括の過去データ研究はMeta Content Library経由で、学術中心の制限は同じ
Mastodon/Misskey等の分散SNS各インスタンスの公開タイムラインAPI/横断検索(search.noc.social等)多くのインスタンスが未認証APIアクセスを禁止。管理者の設定次第。横断検索は断片的
TelegramBot API+MTProto+公開プレビュー(t.me/s/<channel>)/Bellingcatのツール群公開チャンネルの取得は多くの法域で適法。非公開グループは対象外
LINEMessaging API(自社公式アカウント運用向け)/公開アカウント・オープンチャットの閲覧個別トーク・グループ・通常のオープンチャットは取得・監視の対象外。原則クローズドとして扱う
note・ブログ公開ページの閲覧/各サービスの提供範囲専用の研究APIは一般に無い。長文の一次主張の発生源として個別に確認
5ch等の掲示板・まとめ公開スレッドの閲覧/各サイトの規約匿名。発生・増幅・転載の連鎖を追う対象。発信者特定は法・倫理に従い慎重に

💡 無料で拡散分析ができる現実解は Bluesky(日本版・検証済みメモ 2026-07)。 X は無料では拡散分析が事実上できない(開発者APIは従量課金、fxtwitter/vxtwitter は単一投稿の取得に限られる)。 一方 Bluesky は公開REST https://api.bsky.app(認証不要・無料) で検索〜スレッド取得ができ、時系列・発信者・協調・真正性の分析に十分使える。 実測メモ:app.bsky.feed.searchPosts 等の公開エンドポイントは api.bsky.app を使う(public.api.bsky.app はWAFで403になることがある)。 ただし Blueskyは日本の代表的世論とはズレる(利用者層が偏る)ため、Xでの現象の代理として使うときは代表性の限界を明記する。 なお、ここで出る協調・ボット・真正性のスコアは手がかりであって結論ではない(例:通信社アカウントが「高頻度投稿」で真正性フラグに触れることがある=高頻度≠ボット)。

EU DSA第40条によるアクセス

EUのデジタルサービス法(DSA)は、EU拠点の研究者にTikTokやMetaプラットフォームへの強いアクセス権を与えている。日本にはこれと同等の研究者アクセス制度は現状ない(要確認)。日本の記者がResearch API層を使うには、EUの研究機関・認証研究NPOといった連携先が必要になる場合がある。これは抜け道ではなく正規の経路。日本国内では、各社の所属・学術連携・データ事業者契約を通じてアクセスを確保するのが現実的。

OSINTツール群

上のPython製ヒューリスティックは、監視とパターンの当たりをつけるための出発点。本格的なOSINT実務では、次の定番外部ツールを使う(多くは国際的に利用可能。利用前に稼働状況と利用規約を確認)。

アカウント真正性・協調検知

  • Botometer X(osome.iu.edu)— Twitterのボット度合いの学術標準。2023年6月以降はアーカイブモードのみで、2023年5月31日以降に作成・活動したアカウントはスコア化できない
  • Hoaxy / Hoaxy2(osome.iu.edu)— 稼働中。Hoaxy2はMastodon検索・Blueskyのリアルタイム監視・Facebook News Bridgeを追加。Twitter API終了でボットスコアは削除
  • OSoMe Coordiscope — 協調ネットワークの可視化、無料
  • CooRTweet(Rパッケージ)— 協調的非真正行動分析のCooRnet後継。CooRnetはCrowdTangleと同時に2024年8月に終了
  • Bot Sentinel(botsentinel.com)— 一時停止と再開アナウンスの経緯あり。報道で引用する前に稼働状況を確認

逆画像検索・画像フォレンジック

  • TinEye — 完全一致と初出特定の定番逆画像検索
  • Yandex Images — 顔・地域マッチに強い(特にロシア・東欧)
  • Google レンズ — 旧Google逆画像検索の後継
  • Forensically(29a.ch)と FotoForensics — 画像改変の解析(エラーレベル解析、クローン検出、メタデータ)
  • InVID-WeVerify プラグイン — EUのvera.aiプロジェクト下で稼働。合成画像・音声クローン検出のベータ機能を追加。Twitter時代の一部機能はAPI変更で停止

ネットワーク分析

  • Gephi — 無料・オープンソース。ICIJのパナマ文書で使われた定番
  • Maltego CE — Community Editionは登録で無料。有料層あり
  • NodeXL — Excelアドオン。基本無料/Pro有料。レガシーだが機能する

手法のよりどころ(2026年時点)

  • Bellingcat Online Investigation Toolkit(bellingcat.gitbook.io/toolkit)— コミュニティ維持の「Toolkit 2.0」、継続更新
  • Information Futures Lab(ブラウン大公衆衛生大学院)— 2022年6月閉鎖のFirst Draft Newsの後継。クレア・ワードルが共同ディレクター
  • WITNESS Deepfakes Rapid Response Force(gen-ai.witness.org)— 締切に追われる記者と映像フォレンジック専門家を結ぶ
  • CJR Tow Center — 記者向けの非技術的なディープフェイク検出ガイド(2025年)
  • 国内の参考:日本ファクトチェックセンター(JFC)、FIJ(ファクトチェック・イニシアティブ)、総務省「インターネット上の誹謗中傷・偽情報」関連の資料。手法はBellingcat等の国際リソースを基盤に、国内事情に合わせて運用する(各リソースの最新状況は要確認)

注:米国の選挙偽情報研究を担っていた一部機関は2024年に再編された。研究の所在は流動的なので、引用前に各機関の現況を確認すること。

法令・倫理上の注意(日本)

SNS分析は、対象者の権利と取材の自由の両方に関わる。次を踏まえる(いずれも一般的な留意点であり、具体の判断は法務・デスクと相談する)。

## 押さえるべき主な論点(要確認・法的助言ではない)

### 個人情報の保護
- 個人情報保護法:SNSから個人情報を収集・蓄積・分析する行為は、報道目的なら適用除外の余地があるが、
  自社の規程と必要最小限の原則に従う。収集の目的・範囲・保管期間を決めておく
- 公開情報でも、名寄せ・プロファイリングで「特定の個人」を浮かび上がらせる場合は配慮が必要

### 名誉毀損・信用毀損・侮辱
- 投稿の引用・要約で個人や法人の社会的評価を下げる表現は、名誉毀損(刑法230条・民法709条)等のリスク。
  真実性・公益性・公共性の要件、相当性(裏取り)を満たすかをデスク・法務と確認
- 「ボット」「工作員」「ステマ」と断定的にラベリングしない。スコアは仮説であり、断定は別途の立証が要る

### プライバシー・肖像
- 一般私人の私生活上の事実・写真の扱いは慎重に。公人/私人、公共の関心事かを区別する
- 未成年・被害者・遺族・病歴等の機微情報は特に配慮

### 発信者の特定
- 匿名アカウントの発信者特定は、プロバイダ責任制限法の発信者情報開示など法的手続の領域。
  推測での特定・暴露(個人情報の晒し)はしない

### 著作権・利用規約
- 投稿本文・画像・動画の転載は著作権法に触れうる。引用の要件(必然性・主従関係・出所明示)を守る
- 各プラットフォームの利用規約(特にスクレイピング禁止)を守る。規約違反の取得は証拠価値も損なう

### 取材倫理
- なりすましアカウントや偽装での潜入はしない(後述の倫理ガイドライン)
- 取材源がSNS内容を提供した場合、その人を危険に晒さない

倫理ガイドライン

  • 公開コンテンツのみをアーカイブする。 非公開・限定公開はアーカイブ対象外(取材源同意のもとの保全は例外で、公開アーカイブには送らない)。
  • 監視のために偽アカウント・なりすましを作らない。
  • プラットフォームの利用規約を守る。 Xの2023年以降の規約は公開投稿でもスクレイピングを禁止。APIかライセンス契約のデータ事業者を使う。研究用トークンを研究外の用途に流用しない(MetaやTikTokは規約逸脱でアクセスを停止する)。
  • SNS内容を提供した取材源を守る。
  • 協調を公表する前に検証する。 協調スコアは仮説であって結論ではない。
  • 有害コンテンツを増幅しないよう文脈を考える。 拡散の片棒を担がない。
  • 人を断定的にラベリングしない。 ボット・工作員・ステマといった評価は、十分な根拠と裏取りを伴ってから。

関連スキル

  • source-verification(情報源の検証) — SNS上で見つけたアカウント・主張を裏取りする
  • web-archiving(ウェブアーカイブ) — 本書のラッパーを超える本格的なアーカイブ手順
  • web-scraping(ウェブ取得) — 公開コンテンツのプログラム的収集(規約遵守前提)
  • data-journalism(データジャーナリズム) — SNSデータのパターン分析とチャート化
  • crisis-communications(危機・速報対応) — 速報時の時間圧下での検証

スキル・メタデータ

項目値
version1.1.0(日本語版)
原典 version1.1.0
原典 created2025-12-26
原典 updated2026-06-18
authorJoe Amditis(原典)/日本語ローカライズ版
domainjournalism, osint
complexityadvanced

出典・ライセンス(帰属表示)

本スキルは claude-skills-journalism(by Joe Amditis, MIT License, © 2025 Joe Amditis, https://github.com/jamditis/claude-skills-journalism )の journalism-core/skills/social-media-intelligence を 原典の構成(リアルタイム監視・アカウント分析・言説追跡・証拠保全・協調検知・プラットフォーム別取得手段・OSINTツール・倫理)に沿って日本語化し、 日本語プラットフォーム環境と個人情報保護法・名誉毀損・プライバシー・取材倫理に再構成したものです。 米国・EU固有の制度(EU DSA第40条等)はその旨を注記し、日本に同等制度が無いものはその旨を明記しています。 コードはコメントのみ日本語化し機能は保持しています。Botometer・Hoaxy・Bellingcat等の外部ツール名は事実情報として参照しているだけで、各ツールの本文・著作物は転載していません。 原典の著作物本文は転載していません。原作者の公認・推奨を受けたものではありません。 MIT ライセンス全文は同梱の LICENSE-MIT.txt を参照。

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

AIが生成しがちな「いかにもAIっぽい」日本語の定型表現・水増し・空疎な形容を取り除き、読者の信頼を損なわない自然な文章に直す。記事・原稿・プレスリリース・告知・SNS投稿など、AIの支援を受けて書いた文章を公開前に点検するときに使う。「AI臭を消したい」「文章が機械っぽい」「人が書いたように直したい」などの依頼で使う。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

sgbms1/ja-news-skills32026년 7월 14일 업데이트

取材・調査・データ報道で使える無料データ源(API)のカタログ。政府統計(e-Stat)、地域経済(RESAS)、気象(気象庁)、法令(e-Gov)、企業開示(EDINET)、書誌・国会会議録(国立国会図書館)、地理空間(国土地理院)、汎用の国際API(GDELT・Open-Meteo・Wayback等)を用途別に整理し、登録要否・利用条件・選び方を示す。「無料のデータ源が欲しい」「このテーマのAPIは?」「公的データをプログラムで取りたい」といった依頼で使う。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

sgbms1/ja-news-skills32026년 7월 14일 업데이트

data-hodo

무료

データ報道(データジャーナリズム)の手順を支援する。データセットの分析、グラフや地図の作成、雑然としたデータのクリーニング、統計の算出、データに基づく記事の構築に使う。「e-Statのデータを分析したい」「人口あたりで比較したい」「グラフが誤解を招かないか確認したい」「統計から記事を組み立てたい」などの依頼で使う。記者・編集者・調査担当向け。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

sgbms1/ja-news-skills32026년 7월 14일 업데이트

取材で集めた資料(開示文書・記事・SNS・反訳など)をデジタルアーカイブ化し、AIで要約・分類・エンティティ抽出・関係づけ(知識グラフ化)して横断検索できる調査基盤を作るための手引き。「取材資料を整理したい」「大量の開示文書をデータベース化したい」「人物・組織・出来事の関係を可視化したい」「調査報道のアーカイブを作りたい」などの依頼で使う。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

sgbms1/ja-news-skills32026년 7월 14일 업데이트

発言・記事・SNS投稿などの主張を体系的にファクトチェックする。主張の抽出、検証可能な命題への分解、根拠の収集、当事者への確認、判定(レーティング)、記録、訂正対応までを支援する。掲載前の裏取り、ファクトチェック記事の作成、編集部の検証手順づくりに使う。「ファクトチェックして」「この発言は本当か」「裏取りの手順を」などの依頼で使う。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

sgbms1/ja-news-skills32026년 7월 14일 업데이트

テキスト・記事・ウェブページの内容を、厳密な検証方法論にもとづいてファクトチェックする。検証可能な主張を抽出し、信頼できる一次情報・公的データ・検証機関と突き合わせ、各主張の真偽と確からしさ(確信度)、情報源の信頼度スコアを評価し、根拠つきの検証レポートにまとめる。「この主張を検証して」「記事をファクトチェックして」「情報源の信頼性を見て」「真偽を判定して」「誤情報か確かめて」などの依頼で使う。付属の references(信頼できる情報源/検証方法論)を参照する検証重視版。

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

sgbms1/ja-news-skills32026년 7월 14일 업데이트

sgbms1의 스킬 모두 보기

이 스킬의 문제 신고