> ## 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 账号。

通过六种方法在 X（Twitter）发现相关用户。每种方法从不同信号出发：资料关键词、粉丝、社群成员身份、近期推文、认证状态或特定帖子的互动。按用户 ID 合并结果，构建去重后的受众列表。

详细教程见[如何通过 API 在 Twitter 找到目标受众](https://api.sorsa.io/blog/twitter-audience-discovery)。

## 选择方法

| 问题              | 端点                                | 结果                |
| :-------------- | :-------------------------------- | :---------------- |
| 谁用相关职位或关键词描述自己？ | `POST /search-users`              | 用户资料              |
| 谁关注了我所在领域的账号？   | `GET /followers`                  | 每页最多 200 份资料      |
| 谁加入了相关主题的社群？    | `POST /community-members`         | 精简成员资料            |
| 谁正在讨论我的话题？      | `POST /search-tweets`             | 每页最多 20 条推文，含作者资料 |
| 哪些认证账号关注了目标？    | `GET /verified-followers`         | 每页最多 200 份资料      |
| 谁在传播特定帖子？       | `POST /retweeters`、`POST /quotes` | 转推者资料或引用推文        |

每页数量可能变化。使用 `next_cursor` 继续读取，不要把较短的页面视为末尾。

## 环境设置与通用分页

所有示例使用 `https://api.sorsa.io/v3`，并需要 `ApiKey` 请求头。Python 示例应放在下方初始化代码之后，在同一脚本中运行。使用 `python -m pip install requests` 安装 `requests`，然后设置 `SORSA_API_KEY` 环境变量。JavaScript 示例需要支持 `fetch` 的服务端环境，例如 Node.js 18 或更高版本。

```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/zh-Hans/error-codes)，对 `429` 和临时服务器错误加入有限重试，并根据[速率限制](https://docs.sorsa.io/zh-Hans/rate-limits)统一协调共享密钥的工作进程。通用机制见[身份验证](https://docs.sorsa.io/zh-Hans/authentication)和[分页](https://docs.sorsa.io/zh-Hans/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`

获取相关公开账号的粉丝，每次最多 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)
```

### 多个种子账号的受众重叠

对于每个种子账号，每位用户只计数一次。运行前替换示例中的占位用户名：

```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/zh-Hans/followers-and-following)。

## 方法 3：发现社群成员

> **先检查可用性：** 本节介绍社群请求格式。加入新工作流前，请向[支持团队](https://docs.sorsa.io/zh-Hans/support)确认当前数据可用性，详情见[列表与社群](https://docs.sorsa.io/zh-Hans/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/zh-Hans/api-reference/users-data/user-profile-batch)补全这些 ID，每次最多 100 个。相关端点见[列表与社群](https://docs.sorsa.io/zh-Hans/lists-and-communities)。

## 方法 4：根据意图挖掘推文

**端点：**`POST /v3/search-tweets`

搜索近期讨论，提取去重后的作者。保留完整用户对象，便于随后与资料和粉丝结果合并。

```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/zh-Hans/search-operators)和[搜索推文](https://docs.sorsa.io/zh-Hans/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)
```

缺失值保留为空，而不是变为零。导入电子表格时，将 `user_id` 列设为文本，以保留完整 ID。

## 后续步骤

* [搜索推文](https://docs.sorsa.io/zh-Hans/search-tweets)：请求参数与搜索示例
* [搜索运算符](https://docs.sorsa.io/zh-Hans/search-operators)：布尔逻辑与筛选
* [粉丝与关注](https://docs.sorsa.io/zh-Hans/followers-and-following)：粉丝分析
* [列表与社群](https://docs.sorsa.io/zh-Hans/lists-and-communities)：成员与信息流端点
* [竞品分析](https://docs.sorsa.io/zh-Hans/Competitor-Analysis)：竞品情报工作流
* [实时监测](https://docs.sorsa.io/zh-Hans/real-time-monitoring)：轮询与去重
* [追踪提及](https://docs.sorsa.io/zh-Hans/search-mentions)：品牌和竞争对手的提及
* [优化 API 使用](https://docs.sorsa.io/zh-Hans/optimizing-api-usage)：批量和请求预算
* [API 参考](https://docs.sorsa.io/zh-Hans/api-reference-guide)：端点规范
