返回教程列表
教程2026年10月11日4 分钟阅读

微博数据怎么采集?按时间搜索帖子、获取详情与评论

通过 Just One API 按关键词和日期小时范围搜索微博,获取帖子详情,并使用 MID 与 maxId 游标读取评论。包含分页去重和保存 JSON 的 Python 示例。

按一个话题和时间段组织采集

研究微博上的产品讨论或活动反馈,可以先限定关键词与时间范围,找到相关帖子,再读取详情和评论。Just One API 提供这三个步骤对应的接口,便于把帖子和讨论内容关联保存。

本教程以“露营”为关键词,读取最多两页搜索结果,选择三条帖子获取详情,并为每条帖子读取最多两页评论。你可以替换关键词和日期,用少量请求验证自己的话题需求。

获取 Token 并先测试一页搜索

注册 Just One API 后获取 API Token,在微博关键词搜索 V2的在线文档中免费测试。一个 Token 可用于多个平台,后续按量调用;接口权限及免费测试次数以账户显示为准。

本文三个接口都使用 GET,并通过 token 参数鉴权。响应体业务 code=0 后,才继续解析数据。Python 示例从环境变量读取 Token,不打印完整请求地址。

第一步:按日期和小时搜索微博

搜索路径为 /api/weibo/search-all/v2,下列条件需要明确传入:

  • q:搜索关键词,例如“露营”。
  • startDay、endDay:开始与结束日期,格式为 YYYY-MM-DD。
  • startHour、endHour:小时,取值为 0 到 23;本例分别设为 0 和 23。
  • page:搜索页码,从 1 开始。

日期和小时都是这个接口的必填参数。示例使用 hotSort=false 和 contains=ALL;如果只关注特定媒体类型,可按文档选择 PICTURE、VIDEO、MUSIC 或 LINK。

当前公开样例把帖子放在 data.weibo_list,其中 id 用于获取详情,data.has_next_page 用于判断是否继续搜索。修改关键词或日期后,应重新开始分页。平台搜索结果适合形成研究样本,不应被当作指定时期所有相关微博的完整清单。

第二步:获取详情并读取 MID

将搜索返回的帖子 id 传给帖子详情 V1,路径为 /api/weibo/get-weibo-detail/v1。

当前公开样例中的帖子对象位于 data.status,包含 id、mid、text 和 created_at 等字段。下一步评论请求明确使用这个对象返回的 mid。搜索结果中也可能有 bid 等标识,不要假定不同标识在所有接口中都能直接互换。

脚本会先保存详情,再取 mid。若没有找到 MID,会停止并提示检查已保存的响应,而不是猜一个值继续发送评论请求。文本中可能带有链接或格式标记,清洗时保留原文,方便后续核对。

第三步:用 maxId 游标获取评论

调用帖子评论 V1,路径为 /api/weibo/get-post-comments/v1,传入 mid 和 sort=TIME。

评论接口按游标翻页。首次请求不传 maxId;下一次把响应中的 data.max_id 作为请求参数 maxId。它与搜索的 page 页码是不同机制,不要把评论游标当作页数递增。

当前公开样例中,评论结果还带有内层 data.ok 状态,成功时为 1;评论数组位于 data.data。脚本在检查状态后保存评论,按评论 ID 去重,并在以下情况停止:空列表、游标为零或空值、游标重复,或达到每帖两页的上限。

评论中附带的部分回复不等于完整评论树;同样,少量页面也不能代表所有参与讨论的人。做主题归纳时保留帖子 ID、评论 ID 和采集范围,方便说明分析依据。

Python 示例:保存帖子与评论

在 macOS 或 Linux 终端安装依赖并设置环境变量。把 Token 换成自己的值,日期改成你要研究的时间段。

bash
python3 -m pip install requests
export JUSTONEAPI_TOKEN='your_token'
export WEIBO_START_DAY='2026-10-01'
export WEIBO_END_DAY='2026-10-07'

保存代码为 weibo_sample.py,运行 python3 weibo_sample.py。最多两次搜索,加上三条帖子的详情与每帖两次评论请求,共不超过 11 次 API 请求。结果结束较早时会减少调用。

python
import json
import os
from datetime import datetime, timezone
from pathlib import Path

import requests

BASE_URL = "https://api.justoneapi.com"
TOKEN = os.environ.get("JUSTONEAPI_TOKEN", "")


def call_api(path, **params):
    try:
        response = requests.get(
            BASE_URL + path,
            params={"token": TOKEN, **params},
            timeout=120, allow_redirects=False,
        )
    except requests.RequestException:
        raise RuntimeError("API network error or timeout") from None
    if response.status_code != 200:
        raise RuntimeError(f"API HTTP {response.status_code}")
    try:
        result = response.json()
    except ValueError:
        raise RuntimeError("API response is not JSON") from None
    if not isinstance(result, dict) or result.get("code") != 0:
        code = result.get("code") if isinstance(result, dict) else None
        raise RuntimeError(f"API failed: code={code}; check the docs")
    data = result.get("data")
    if not isinstance(data, dict):
        raise RuntimeError("Unexpected data shape; inspect the docs")
    inner = data.get("base_resp")
    if isinstance(inner, dict) and inner.get("status_code") not in (0, "0"):
        raise RuntimeError("Platform response was unsuccessful")
    return result


def save_json(folder, name, value):
    (folder / name).write_text(
        json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8"
    )


def require_list(data, key):
    value = data.get(key)
    if not isinstance(value, list):
        raise RuntimeError(f"Expected a list at {key}; inspect saved JSON")
    return value


def output_folder(topic):
    if not TOKEN or TOKEN == "your_token":
        raise RuntimeError("Set JUSTONEAPI_TOKEN before running")
    run_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
    folder = Path(topic + "-output") / run_id
    folder.mkdir(parents=True, exist_ok=False)
    return folder


KEYWORD = "露营"
START_DAY = os.environ.get("WEIBO_START_DAY", "")
END_DAY = os.environ.get("WEIBO_END_DAY", "")
MAX_SEARCH_PAGES = 2
MAX_POSTS = 3
MAX_COMMENT_PAGES = 2


def main():
    try:
        start = datetime.strptime(START_DAY, "%Y-%m-%d")
        end = datetime.strptime(END_DAY, "%Y-%m-%d")
    except ValueError:
        raise RuntimeError("Set WEIBO_START_DAY and WEIBO_END_DAY: YYYY-MM-DD") from None
    if start > end:
        raise RuntimeError("Start date must not be after end date")
    folder = output_folder("weibo")
    ids, seen = [], set()
    for page in range(1, MAX_SEARCH_PAGES + 1):
        result = call_api(
            "/api/weibo/search-all/v2", q=KEYWORD,
            startDay=START_DAY, startHour=0, endDay=END_DAY, endHour=23,
            hotSort="false", contains="ALL", page=page,
        )
        save_json(folder, f"search-{page}.json", result)
        data = result["data"]
        rows = require_list(data, "weibo_list")
        if not rows:
            break
        previous_count = len(ids)
        for row in rows:
            post_id = row.get("id") if isinstance(row, dict) else None
            if post_id and str(post_id) not in seen:
                seen.add(str(post_id))
                ids.append(str(post_id))
        if len(ids) == previous_count:
            break
        if data.get("has_next_page") not in (True, 1, "1"):
            break

    selected = ids[:MAX_POSTS]
    save_json(folder, "selected-posts.json", selected)
    for index, post_id in enumerate(selected, start=1):
        detail = call_api("/api/weibo/get-weibo-detail/v1", id=post_id)
        save_json(folder, f"post-{index}-detail.json", detail)
        status = detail["data"].get("status")
        mid = status.get("mid") if isinstance(status, dict) else None
        if not mid:
            raise RuntimeError("Missing post MID; inspect the saved detail")
        cursor, cursors, comments, comment_ids = "", set(), [], set()
        for page in range(1, MAX_COMMENT_PAGES + 1):
            params = {"mid": str(mid), "sort": "TIME"}
            if cursor:
                params["maxId"] = cursor
            result = call_api("/api/weibo/get-post-comments/v1", **params)
            save_json(folder, f"post-{index}-comments-{page}.json", result)
            data = result["data"]
            if data.get("ok") not in (1, "1"):
                raise RuntimeError("Comment response was unsuccessful")
            rows = require_list(data, "data")
            if not rows:
                break
            for row in rows:
                if not isinstance(row, dict):
                    raise RuntimeError("Unexpected comment item")
                cid = row.get("idstr") or row.get("id")
                if cid is None:
                    raise RuntimeError("Missing comment ID; inspect saved JSON")
                if str(cid) not in comment_ids:
                    comment_ids.add(str(cid))
                    comments.append(row)
            next_cursor = data.get("max_id")
            if next_cursor in (None, "", 0, "0"):
                break
            cursor = str(next_cursor)
            if cursor in cursors:
                break
            cursors.add(cursor)
        save_json(folder, f"post-{index}-comments.json", comments)
    print(f"Saved {len(selected)} post samples to {folder}")


if __name__ == "__main__":
    try:
        main()
    except RuntimeError as error:
        raise SystemExit(str(error)) from None

如何查看和继续分析

结果保存在 weibo-output 的独立时间目录中。selected-posts.json 记录选中的帖子,post-*-detail.json 保存详情,post-*-comments-*.json 保存原始评论页,post-*-comments.json 汇总该帖去重后的评论。

可以把评论按帖子、时间或主题整理,筛选用户关心的功能与问题。先保留原始文本,再添加自己的清洗文本、分类标签或情绪判断;把自动分类视作待复核结果,尤其要检查反讽、引用和上下文。

常见问题

能否持续跟踪一个话题?

可以在此流程上定时查询,但要保存每次关键词、时间范围和采集时间,跨批次按帖子与评论 ID 去重。搜索和评论可用范围受实际响应影响,持续运行也不等于全量舆情覆盖。

下一页重复或没有更多结果怎么办?

搜索根据 has_next_page 与新增 ID 判断;评论根据 max_id 游标判断。保留原始响应后结束当前分页,不要无限重复同一请求。脚本中的上限用于小规模测试,可在验证结果后按需求调整。

请求失败会自动重试吗?

示例不会自动重试,避免把失败变成持续请求。先区分参数、权限、限流和瞬时故障,再参考接口失败后的处理策略添加有限重试。重跑脚本会创建新目录,不覆盖旧结果。

打开微博搜索在线文档,选一个较短时间段验证搜索结果,再连接详情与评论采集。

继续使用 Just One API

登录 Dashboard 获取 Token,查看完整 API 文档,或打开 MCP GitHub 项目了解最新配置。