> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sorsa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# ターゲット層の発見

> プロフィール検索、フォロワー、コミュニティ、ツイートへの反応から、関連性の高いXアカウントを見つけます。

6つの方法でX（Twitter）の関連ユーザーを探します。プロフィールのキーワード、フォロワー、コミュニティへの参加、最近の投稿、認証状態、特定投稿への反応という異なる手がかりを使います。ユーザーIDで結果を統合し、重複のないオーディエンスを作成してください。

詳しい流れは[APIでTwitterのターゲット層を見つける方法](https://api.sorsa.io/blog/twitter-audience-discovery)を参照してください。

## 方法を選ぶ

| 調べたいこと                   | エンドポイント                           | 結果                        |
| :----------------------- | :-------------------------------- | :------------------------ |
| 関連する職種やキーワードで自己紹介している人は？ | `POST /search-users`              | ユーザープロフィール                |
| 自分の分野のアカウントをフォローしている人は？  | `GET /followers`                  | 1ページ最大200プロフィール           |
| 対象の話題のコミュニティに参加した人は？     | `POST /community-members`         | 簡略化されたメンバープロフィール          |
| 対象の話題について投稿している人は？       | `POST /search-tweets`             | 1ページ最大20ツイートと投稿者プロフィール    |
| 対象アカウントの認証済みフォロワーは？      | `GET /verified-followers`         | 1ページ最大200プロフィール           |
| 特定の投稿を広めている人は？           | `POST /retweeters`、`POST /quotes` | リツイートした人のプロフィール、または引用ツイート |

ページの件数は変動します。`next_cursor`で続きを取得し、少ない件数だけを理由に終了しないでください。

## 準備と共通のページネーション

すべての例は`https://api.sorsa.io/v3`を使い、`ApiKey`ヘッダーが必要です。Pythonの例は次の準備コードに続け、同じスクリプトで実行してください。`python -m pip install requests`で`requests`をインストールし、環境変数`SORSA_API_KEY`を設定します。JavaScriptの例にはNode.js 18以降など、`fetch`が使えるサーバー側の実行環境が必要です。

```python theme={null}
import os
import time
import requests

API_KEY = os.environ["SORSA_API_KEY"]
BASE_URL = "https://api.sorsa.io/v3"

def fetch_pages(method, endpoint, payload, result_key, max_pages=10):
    """Fetch a bounded number of pages; raise on HTTP errors."""
    items = []
    cursor = None
    seen_cursors = set()

    for _ in range(max_pages):
        values = dict(payload)
        if cursor:
            values["next_cursor"] = cursor
        options = {"params": values} if method == "GET" else {"json": values}
        response = requests.request(
            method, f"{BASE_URL}{endpoint}",
            headers={"ApiKey": API_KEY}, timeout=30, **options,
        )
        response.raise_for_status()
        data = response.json()
        items.extend(data.get(result_key) or [])
        cursor = data.get("next_cursor")
        if not cursor:
            break
        if cursor in seen_cursors:
            raise RuntimeError("Pagination returned a repeated cursor")
        seen_cursors.add(cursor)
        time.sleep(0.1)

    return items
```

`max_pages`はリクエストの消費を制限します。上限に達しても未取得の結果が残る場合があります。これらの例はHTTPエラーで停止します。本番では[エラーコード](https://docs.sorsa.io/ja/error-codes)を参考に、`429`と一時的なサーバーエラーに回数を制限した再試行を追加し、同じキーを使う全ワーカーを[レート制限](https://docs.sorsa.io/ja/rate-limits)内に調整してください。共通の仕組みは[認証](https://docs.sorsa.io/ja/authentication)と[ページネーション](https://docs.sorsa.io/ja/pagination)を参照してください。

## 方法1：自己紹介のキーワード検索

**エンドポイント：** `POST /v3/search-users`

職種、肩書き、関心などのキーワードやフレーズでアカウントを検索します。返された自己紹介、表示名、ユーザー名を確認し、対象の層に合うか判断してください。

```json theme={null}
{
  "query": "Product Manager"
}
```

| パラメーター        | 型      | 必須  | 説明                  |
| :------------ | :----- | :-- | :------------------ |
| `query`       | string | はい  | 検索キーワードまたはフレーズ。     |
| `next_cursor` | string | いいえ | 前のレスポンスのカーソル。初回は省略。 |

### Python

```python theme={null}
def find_users_by_bio(query, max_pages=10):
    return fetch_pages("POST", "/search-users", {"query": query}, "users", max_pages)

bio_results = find_users_by_bio("machine learning engineer")
qualified = [
    u for u in bio_results
    if (u.get("followers_count") or 0) >= 1000
    and (u.get("tweets_count") or 0) >= 100
    and not u.get("protected", False)
]
```

### JavaScript

```javascript theme={null}
const API_KEY = process.env.SORSA_API_KEY;
if (!API_KEY) throw new Error("Set SORSA_API_KEY before running this example");

async function findUsersByBio(query, maxPages = 10) {
  const users = [];
  const seenCursors = new Set();
  let cursor = null;

  for (let i = 0; i < maxPages; i++) {
    const body = { query };
    if (cursor) body.next_cursor = cursor;
    const response = await fetch("https://api.sorsa.io/v3/search-users", {
      method: "POST",
      headers: { ApiKey: API_KEY, "Content-Type": "application/json" },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(30000),
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();
    users.push(...(data.users || []));
    cursor = data.next_cursor;
    if (!cursor) break;
    if (seenCursors.has(cursor)) throw new Error("Repeated pagination cursor");
    seenCursors.add(cursor);
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
  return users;
}
```

## 方法2：競合のフォロワーを取得する

**エンドポイント：** `GET /v3/followers`

関連する公開アカウントのフォロワーを1リクエスト最大200人取得します。`username`（@なし）、`user_id`（文字列）、`user_link`（完全なプロフィールURL）のいずれかを指定します。続きを取得するには`next_cursor`を渡します。

```text theme={null}
GET https://api.sorsa.io/v3/followers?username=competitor_handle
```

```python theme={null}
def get_followers(username, max_pages=10):
    return fetch_pages("GET", "/followers", {"username": username}, "users", max_pages)

followers = get_followers("competitor_handle", max_pages=20)
```

### 複数の起点アカウント間での重複

各起点アカウントについて、同じユーザーは1回だけ数えます。実行前に仮のユーザー名を置き換えてください。

```python theme={null}
from collections import Counter

competitors = ["competitor_a", "competitor_b", "competitor_c"]
follower_sets = {
    handle: {u["id"] for u in get_followers(handle, max_pages=10)}
    for handle in competitors
}
counts = Counter(uid for ids in follower_sets.values() for uid in ids)
overlap = {uid for uid, count in counts.items() if count >= 2}
```

これは取得したページ内での重複であり、全フォロワー一覧の重複とは限りません。詳細は[フォロワーとフォロー中ユーザー](https://docs.sorsa.io/ja/followers-and-following)を参照してください。

## 方法3：コミュニティのメンバーを見つける

> **事前に提供状況を確認：** この節はコミュニティのリクエスト形式を説明します。新しいワークフローへ追加する前に、現在のデータ利用可否を[サポート](https://docs.sorsa.io/ja/support)へ確認してください。[リストとコミュニティ](https://docs.sorsa.io/ja/lists-and-communities)も参照してください。

**エンドポイント：** `POST /v3/community-members`

Xコミュニティのメンバーを取得します。参加は関心を示す手がかりですが、最近の活動や購入意向を証明するものではありません。

```json theme={null}
{
  "community_link": "1966045657589813686"
}
```

`community_link`には、文字列の数値IDまたは完全なコミュニティURLを指定できます。

```python theme={null}
def get_community_members(community_id, max_pages=20):
    return fetch_pages(
        "POST", "/community-members",
        {"community_link": community_id}, "users", max_pages,
    )
```

結果は`id`、`username`、`display_name`、`profile_image_url`、`verified`、`protected`を持つ簡略プロフィールです。自己紹介やフォロワー数で絞る前に、[ユーザープロフィールの一括取得](https://docs.sorsa.io/ja/api-reference/users-data/user-profile-batch)へIDを渡して補完します（1回最大100ID）。関連する機能は[リストとコミュニティ](https://docs.sorsa.io/ja/lists-and-communities)を参照してください。

## 方法4：意向を示すツイートから探す

**エンドポイント：** `POST /v3/search-tweets`

最近の議論を検索して投稿者を重複なく抽出します。後でプロフィール検索やフォロワーの結果と統合できるよう、完全なUserオブジェクトを保持してください。

```python theme={null}
def find_active_voices(query, min_followers=100, max_pages=10):
    tweets = fetch_pages(
        "POST", "/search-tweets",
        {"query": query, "order": "latest"}, "tweets", max_pages,
    )
    voices = {}
    for tweet in tweets:
        user = tweet.get("user")
        if not user or not user.get("id"):
            continue
        if (user.get("followers_count") or 0) < min_followers:
            continue
        if user["id"] not in voices:
            voices[user["id"]] = {
                **user,
                "sample_tweet": (tweet.get("full_text") or "")[:160],
            }
    return list(voices.values())

intent_voices = find_active_voices(
    '("need a CRM" OR "looking for a CRM") lang:en -filter:retweets',
)
```

### よく使う検索条件

角括弧の仮の値を、対象カテゴリー、ユーザー名、ツール、話題に置き換えてください。丸括弧により、共通フィルターが`OR`の両側に適用されます。

| 目的         | 検索条件                                                                           |
| :--------- | :----------------------------------------------------------------------------- |
| 購入意向       | `("need a [category]" OR "looking for [category]") lang:en -filter:retweets`   |
| 競合製品への不満   | `"[competitor]" (frustrated OR broken OR "switching from") -from:[competitor]` |
| 移行の意向      | `("migrating from [tool]" OR "switching from [tool]") lang:en`                 |
| おすすめを求める投稿 | `("any recommendation" OR "anyone use") [topic] lang:en`                       |
| 課題についての議論  | `("struggling with" OR "how do you handle") [topic] lang:en`                   |

観測期間を決める場合は`since:`と`until:`を加えます。キーワードへの一致を購入意向と判断する前に、実際の投稿を確認してください。[検索演算子](https://docs.sorsa.io/ja/search-operators)と[ツイート検索](https://docs.sorsa.io/ja/search-tweets)も参照してください。

## 方法5：認証済みフォロワーの分析

**エンドポイント：** `GET /v3/verified-followers`

`/followers`と同じ識別子とページネーションで取得します。認証状態は分類用の属性であり、対象との関連性は別に評価してください。

```python theme={null}
def get_verified_followers(username, max_pages=10):
    return fetch_pages(
        "GET", "/verified-followers", {"username": username}, "users", max_pages,
    )

verified = get_verified_followers("openai")
verified.sort(key=lambda u: u.get("followers_count") or 0, reverse=True)
```

## 方法6：リツイート・引用したユーザー

**エンドポイント：** `POST /v3/retweeters`、`POST /v3/quotes`

`/retweeters`はユーザープロフィールを返します。`/quotes`は引用ツイートを返し、`user`が引用した人、`full_text`がそのコメントです。

```python theme={null}
def get_retweeters(tweet_link, max_pages=10):
    return fetch_pages(
        "POST", "/retweeters", {"tweet_link": tweet_link}, "users", max_pages,
    )

def get_quoters(tweet_link, max_pages=10):
    quote_tweets = fetch_pages(
        "POST", "/quotes", {"tweet_link": tweet_link}, "tweets", max_pages,
    )
    return list({
        tweet["user"]["id"]: tweet["user"]
        for tweet in quote_tweets if tweet.get("user")
    }.values())
```

以下の処理で使うため、ヘルパー`get_quoters`は引用ツイートを重複のないプロフィール一覧に変換します。コメントも必要なら、変換前の`quote_tweets`を保持して`full_text`を分析してください。

## 複数の方法を組み合わせる

文字列のユーザーIDで一覧を統合し、各アカウントがどの情報源に現れたかも保持します。情報源の数が多いことは、選択した入力に多く現れたことを示します。優先順位付けの目安であり、信頼度のスコアではありません。

```python theme={null}
def score_by_source(by_source):
    index = {}
    for source, users in by_source.items():
        for user in users:
            uid = user["id"]
            if uid not in index:
                index[uid] = {"user": dict(user), "sources": set()}
            else:
                # Fill gaps when one source returns a compact profile.
                for field, value in user.items():
                    if index[uid]["user"].get(field) is None and value is not None:
                        index[uid]["user"][field] = value
            index[uid]["sources"].add(source)

    result = [
        {**entry["user"], "source_count": len(entry["sources"]),
         "sources": sorted(entry["sources"])}
        for entry in index.values()
    ]
    return sorted(result, key=lambda u: (-u["source_count"], -(u.get("followers_count") or 0)))

# Uses the results from Techniques 1, 2, and 4 above.
combined = score_by_source({
    "profile_search": bio_results,
    "competitor_followers": followers,
    "topic_discussion": intent_voices,
})
```

コミュニティのメンバーは簡略プロフィールを補完してから加えます。`get_retweeters`と`get_quoters`のユーザー一覧も追加できます。

## 品質の条件で絞り込む

プロジェクトに合う明確な選択基準を使ってください。このフィルターはプロフィールの充実度、アカウント年齢、基本的な件数を確認します。ボットや最近の活動を判定するものではありません。活動状況が重要なら最近の投稿を調べます。

```python theme={null}
from datetime import datetime, timezone, timedelta

def is_quality_account(user, min_followers=500, min_tweets=100, max_following_ratio=10):
    if user.get("protected", False):
        return False
    followers = user.get("followers_count") or 0
    if followers < min_followers or (user.get("tweets_count") or 0) < min_tweets:
        return False
    if (user.get("followings_count") or 0) > followers * max_following_ratio:
        return False
    if not (user.get("description") or "").strip():
        return False

    created = user.get("created_at")
    if not created:
        return False
    try:
        dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
        if dt.tzinfo is None:
            return False
    except (TypeError, ValueError):
        return False
    return dt <= datetime.now(timezone.utc) - timedelta(days=30)

qualified = [user for user in combined if is_quality_account(user)]
```

この例は、作成日がない、または解釈できないアカウントを除外します。その方針としきい値は用途に合わせて調整してください。

## CSVに出力する

重複排除とフィルタリング後のユーザーを出力します。ツイートの結果は先に`user`へ変換してください。コミュニティの簡略プロフィールで不足する項目が必要なら、先に補完します。

```python theme={null}
import csv

def export_users_to_csv(users, output_file="audience.csv"):
    fields = [
        "user_id", "username", "display_name", "description",
        "followers_count", "followings_count", "tweets_count",
        "location", "verified", "created_at",
    ]
    with open(output_file, "w", newline="", encoding="utf-8") as file:
        writer = csv.DictWriter(file, fieldnames=fields)
        writer.writeheader()
        for user in users:
            row = {field: user.get(field, "") for field in fields}
            row["user_id"] = user["id"]
            row["description"] = (user.get("description") or "").replace("\n", " ")
            writer.writerow(row)

export_users_to_csv(qualified)
```

欠けた値は0にせず空欄のままにします。表計算ソフトへ取り込む際は、IDを正確に保持するため`user_id`列を文字列に設定してください。

## 次のステップ

* [ツイート検索](https://docs.sorsa.io/ja/search-tweets)：パラメーターと検索例。
* [検索演算子](https://docs.sorsa.io/ja/search-operators)：論理条件とフィルター。
* [フォロワーとフォロー中ユーザー](https://docs.sorsa.io/ja/followers-and-following)：フォロワー分析。
* [リストとコミュニティ](https://docs.sorsa.io/ja/lists-and-communities)：メンバーとフィードの取得。
* [競合分析](https://docs.sorsa.io/ja/Competitor-Analysis)：競合調査のワークフロー。
* [リアルタイム監視](https://docs.sorsa.io/ja/real-time-monitoring)：ポーリングと重複排除。
* [メンションの追跡](https://docs.sorsa.io/ja/search-mentions)：ブランドと競合への言及。
* [API利用の最適化](https://docs.sorsa.io/ja/optimizing-api-usage)：バッチ処理とリクエストの予算。
* [APIリファレンス](https://docs.sorsa.io/ja/api-reference-guide)：エンドポイントの仕様。
