Nemo2011 / bilibili-api

哔哩哔哩常用API调用。支持视频、番剧、用户、频道、音频等功能。原仓库地址:https://github.com/MoyuScript/bilibili-api
https://nemo2011.github.io/bilibili-api/
GNU General Public License v3.0
2.14k stars 203 forks source link

[提问] 获取视频的评论列表 API 有变,求助怎么翻页 #462

Closed FunnySaltyFish closed 1 year ago

FunnySaltyFish commented 1 year ago

Python 版本: 3.10.4

模块版本: 15.5.5

运行环境: Windows


如题,现在的获取视频评论接口有了非常大的变化。开启 F12 抓取到的一个请求如下:

https://api.bilibili.com/x/v2/reply/main?type=1&oid=275261884&mode=3&pagination_str={"type":1,"direction":1,"session_id":"1734042864723068","data":{}}

# 上面的对应视频 https://www.bilibili.com/video/av275261884/

唯一与分页相关的似乎只有 pagination_str,而且返回的请求中也确实有 pagination_reply 字段。但有意思的来了,我发现这个字段在页数不同时也不会变化

下面展示了两页的请求,唯一有变化的只有 w_rip 字段(和 wts,看样子是时间戳),甚至连 cookie 都一样。 Snipaste_2023-08-30_21-20-54

Snipaste_2023-08-30_21-21-11

我已经尝试了目前代码里内置的算 w_rip 的方法,代码类似于

 params = {
    "type": 1,
    "oid": oid,
    "mode": 2,
    "pagination_str": pagination_str,
    "plat": 1,
    "web_location": 1315875,
    "crsf": credential.bili_jct
}
# pagination_str: {"offset":"{\"type\":1,\"direction\":1,\"session_id\":\"1733963713068881\",\"data\":{}}"}
url = "https://api.bilibili.com/x/v2/reply/wbi/main"
print("-- url: ", url + "?" + urlencode(params))
enc_wbi(params, await get_mixin_key())
print("-- wbi: ", params["w_rid"])
text = await get_html(url, params, COMMON_HEADERS, cookies=credential.get_cookies())

但是一直获取的是同一页的内容。所以我现在很懵逼,不知道怎么翻页,问问各位大佬有什么办法吗?

z0z0r4 commented 1 year ago

如果是 api 变了的话我明天修

z0z0r4 commented 1 year ago

这是Web端底下的评论区分页吗?

z0z0r4 commented 1 year ago

首先网页端姑且用的是 wbi 接口,你这个 https://api.bilibili.com/x/v2/reply/main?type=1&oid=275261884&mode=3&pagination_str={"type":1,"direction":1,"session_id":"1734042864723068","data":{}} 是哪来的…我记得是已经没在用的

image

https://api.bilibili.com/x/v2/reply/wbi/main

FunnySaltyFish commented 1 year ago

是的,用的是 wbi 的,我上面给的 url 漏了这个,下面的代码里有。带 wbi 的和不带的我都试过,就是不知道怎么翻页

z0z0r4 commented 1 year ago

你直接反复请求就是翻页,根据 session_id 后端处理,不用前端翻页了

z0z0r4 commented 1 year ago

旧版还能跑其实也没必要,第一次请求是 {"offset":""} 剩下的填 data.cursor.pagination_reply.next_offset

FunnySaltyFish commented 1 year ago

您介意给个例子吗?我刚刚改用了同一个 httpx.AsyncClient 做请求,多次请求同一个 URL,得到的结果一直相同。

用来测试的代码如下:

import asyncio
import json
from traceback import format_exc, print_exc
from typing import Any, Dict, List, Optional, TypeAlias
from urllib.parse import urlencode

import httpx
from async_pool import AsyncPool
from bilibili_api.credential import Credential
from bilibili_api.utils.network_httpx import request, enc_wbi, get_mixin_key
# config 中包含了 BILI_JCT, SESSDATA, BUVID3, DEDE_USER_ID, AT_TIME_VALUE
from config import *

JSON_TYPE: TypeAlias = Dict[str, Any]

COMMON_HEADERS = {
    "Origin": "https://www.bilibili.com",
    "Authority": "api.bilibili.com",
    "Sec-Ch-Ua": '"Chromium";v="116", "Not)A;Brand";v="24", "Microsoft Edge";v="116"',
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
}

# https://nemo2011.github.io/bilibili-api/#/get-credential
if not (SESSDATA and BILI_JCT and BUVID3 and DEDE_USER_ID and AT_TIME_VALUE):
    raise ValueError(
        "请在 .env 中填写 SESSDATA, BILI_JCT, BUVID3, DEDE_USER_ID, AT_TIME_VALUE")

credential = Credential(sessdata=SESSDATA, bili_jct=BILI_JCT,
                        buvid3=BUVID3, dedeuserid=DEDE_USER_ID, ac_time_value=AT_TIME_VALUE)
print("credential: ", credential.get_cookies())
pool = AsyncPool(maxsize=16)

async def get_html(url: str, params: Dict = None, headers: Dict = None, cookies: Dict = None, timeout: int = 30, client: httpx.AsyncClient = None):
    m_client = client
    try:
        if client is None:
            m_client = httpx.AsyncClient()
        print("当前发送请求的 client ID: ", id(m_client))
        r = await m_client.get(url, timeout=timeout, params=params, headers=headers, cookies=cookies)
        r.raise_for_status()  # 如果状态不是200,引发HTTPError异常
        return r.text
    except Exception as e:
        print_exc()
        return "产生异常"
    finally:
        if client is None and m_client is not None:
            await m_client.aclose()

async def get_one_page(oid: int, pagination_str: str, client: httpx.AsyncClient = None):
    """获取范围:一个回复页"""
    params = {
        "type": 1,
        "oid": oid,
        "mode": 2,
        "pagination_str": pagination_str,
        "plat": 1,
        "web_location": 1315875,
        "crsf": credential.bili_jct
    }
    # pagination_str: {"offset":"{\"type\":1,\"direction\":1,\"session_id\":\"1733963713068881\",\"data\":{}}"}
    url = "https://api.bilibili.com/x/v2/reply/wbi/main"
    # print("-- url: ", url + "?" + urlencode(params))
    enc_wbi(params, await get_mixin_key())
    text = await get_html(url, params, COMMON_HEADERS, cookies=credential.get_cookies(), client=client)
    obj = json.loads(text)
    return obj

async def crawl_one_page_video(oid: int, page: int, pagination_str: str, client: httpx.AsyncClient) -> Optional[str]:
    """
    爬取一个视频一页的评论,返回下一页的 pagination_str
    """

    print("-- 开始爬取视频 {} 的第 {} 页评论".format(oid, page))
    obj = await get_one_page(oid, pagination_str, client)

    video_replies = obj["data"]["replies"]

    print("爬取到的第 {} 页,第一条评论是 {}".format(
        page, video_replies[0]["content"]["message"]))
    return obj["data"]["cursor"]["pagination_reply"].get("next_offset")

async def crawl_one_video(oid: int):
    """
    爬取一个视频的所有评论
    """
    print("- 开始爬取视频 {} 的评论".format(oid))
    url = "https://api.bilibili.com/x/v2/reply/count"
    params = {
        "type": 1,
        "oid": oid
    }
    text = await get_html(url, params, COMMON_HEADERS)
    obj = json.loads(text)
    total_page: int = obj["data"]["count"] // 20 + 1
    print("- 视频 {} 一共有 {} 页评论".format(oid, total_page))
    pagination = '{"offset":""}'
    async with httpx.AsyncClient() as client:
        for page in range(1, total_page + 1):
            next_page = await crawl_one_page_video(oid, page, pagination_str=pagination, client=client)
            print("-- 爬取视频 {} 的第 {} 页评论完毕".format(oid, page))
            if next_page is None:
                print("- 视频 {} 的评论爬取完毕".format(oid))
                break
            await asyncio.sleep(0.1)
            pagination = next_page

async def refresh_cookie_if_necessary():
    need_refresh = await credential.check_refresh()
    if need_refresh:
        print("cookie 已过期,正在刷新")
        await credential.refresh()
        print("cookie 刷新成功")
    else:
        print("cookie 未过期,无需刷新")

async def main():
    await refresh_cookie_if_necessary()
    await crawl_one_video(532664301)

if __name__ == "__main__":
    asyncio.run(main())

其中, config.py 如下:

from dotenv import load_dotenv
import os

load_dotenv()

env = os.environ

SESSDATA = env.get('SESSDATA', '')
BILI_JCT = env.get('BILI_JCT', '')
BUVID3 = env.get('BUVID3', '')
DEDE_USER_ID = env.get('DEDE_USER_ID', '')
AT_TIME_VALUE = env.get('AT_TIME_VALUE', '')

随便找一个视频,爬取结果如下:

当前发送请求的 client ID:  2028897443584
爬取到的第 2 页,第一条评论是 vedal是真的强啊[笑哭]
-- 爬取视频 532664301 的第 2 页评论完毕
-- 开始爬取视频 532664301 的第 3 页评论
当前发送请求的 client ID:  2028897443584
爬取到的第 3 页,第一条评论是 vedal是真的强啊[笑哭]
-- 爬取视频 532664301 的第 3 页评论完毕
-- 开始爬取视频 532664301 的第 4 页评论
当前发送请求的 client ID:  2028897443584
爬取到的第 4 页,第一条评论是 vedal是真的强啊[笑哭]
-- 爬取视频 532664301 的第 4 页评论完毕
-- 开始爬取视频 532664301 的第 5 页评论
当前发送请求的 client ID:  2028897443584
爬取到的第 5 页,第一条评论是 vedal是真的强啊[笑哭]
-- 爬取视频 532664301 的第 5 页评论完毕
-- 开始爬取视频 532664301 的第 6 页评论
当前发送请求的 client ID:  2028897443584
爬取到的第 6 页,第一条评论是 vedal是真的强啊[笑哭]
-- 爬取视频 532664301 的第 6 页评论完毕
-- 开始爬取视频 532664301 的第 7 页评论
当前发送请求的 client ID:  2028897443584
z0z0r4 commented 1 year ago

直接 F12 浏览器刷新就能发现 replies 会变

应该是 pagination_str 里面的 \ 被转义了,你看看 params,我也不知道咋整(

https://github.com/Nemo2011/bilibili-api/pull/453#issuecomment-1700443121

FunnySaltyFish commented 1 year ago

浏览器刷新会变,有可能是那个视频下面有人新评论了,因为按照时间排序,所以会变……好像也不是翻页。找一个没什么人评论的视频,浏览器里怎么刷新好像都不变 😢

z0z0r4 commented 1 year ago

就是刷新的xd...

https://api.bilibili.com/x/v2/reply/wbi/main?oid=659036522&type=1&mode=3&pagination_str=%7B%22offset%22:%22%7B%5C%22type%5C%22:1,%5C%22direction%5C%22:1,%5C%22session_id%5C%22:%5C%221734108348411924%5C%22,%5C%22data%5C%22:%7B%7D%7D%22%7D&plat=1&web_location=1315875&w_rid=586f3aa3f7299caa7e10372729134a54&wts=1693465188 你换个 session_id 刷第二次就没 replies

blyc commented 1 year ago

next参数,bac文档说从零开始,可我个人实测却是从1开始,具体自行测试

https://api.bilibili.com/x/v2/reply/wbi/main?type=1&oid=275261884&mode=3&next=1

https://api.bilibili.com/x/v2/reply/wbi/main?type=1&oid=275261884&mode=3&next=2

BAC链接:https://socialsisteryi.github.io/bilibili-API-collect/docs/comment/list.html#%E8%8E%B7%E5%8F%96%E8%AF%84%E8%AE%BA%E5%8C%BA%E6%98%8E%E7%BB%86-%E6%87%92%E5%8A%A0%E8%BD%BD


同时本库也有另一个获取评论的api的实现(和上文不是同一个api):https://nemo2011.github.io/bilibili-api/#/modules/comment?id=async-def-get_comments

def test(credential):
    for i in [1, 2, 3]:
        rep = sync(comment.get_comments(page_index=i,credential=credential, oid=275261884, type_=CommentResourceType.VIDEO,order=OrderType.LIKE))
        print(rep)

简单测试结论是也可以翻页

blyc commented 1 year ago

next参数,bac文档说从零开始,可我个人实测却是从1开始,具体自行测试

https://api.bilibili.com/x/v2/reply/wbi/main?type=1&oid=275261884&mode=3&next=1

https://api.bilibili.com/x/v2/reply/wbi/main?type=1&oid=275261884&mode=3&next=2

BAC链接:https://socialsisteryi.github.io/bilibili-API-collect/docs/comment/list.html#%E8%8E%B7%E5%8F%96%E8%AF%84%E8%AE%BA%E5%8C%BA%E6%98%8E%E7%BB%86-%E6%87%92%E5%8A%A0%E8%BD%BD

同时本库也有另一个获取评论的api的实现(和上文不是同一个api):https://nemo2011.github.io/bilibili-api/#/modules/comment?id=async-def-get_comments

def test(credential):
    for i in [1, 2, 3]:
        rep = sync(comment.get_comments(page_index=i,credential=credential, oid=275261884, type_=CommentResourceType.VIDEO,order=OrderType.LIKE))
        print(rep)

简单测试结论是也可以翻页

又去f12看了下不是很自信了,有无pagination_str影响了响应里pagination_reply的结构,所以有pagination_str怎么翻页我就没研究测试了

z0z0r4 commented 1 year ago

我想问问有人知道咋给 pagination_str 加 \ 吗?搞定这一步我就能直接 commit 了其实(

z0z0r4 commented 1 year ago

next参数,bac文档说从零开始,可我个人实测却是从1开始,具体自行测试

api.bilibili.com/x/v2/reply/wbi/main?type=1&oid=275261884&mode=3&next=1

api.bilibili.com/x/v2/reply/wbi/main?type=1&oid=275261884&mode=3&next=2

BAC链接:socialsisteryi.github.io/bilibili-API-collect/docs/comment/list.html#%E8%8E%B7%E5%8F%96%E8%AF%84%E8%AE%BA%E5%8C%BA%E6%98%8E%E7%BB%86-%E6%87%92%E5%8A%A0%E8%BD%BD

同时本库也有另一个获取评论的api的实现(和上文不是同一个api):nemo2011.github.io/bilibili-api/#/modules/comment?id=async-def-get_comments

def test(credential):
    for i in [1, 2, 3]:
        rep = sync(comment.get_comments(page_index=i,credential=credential, oid=275261884, type_=CommentResourceType.VIDEO,order=OrderType.LIKE))
        print(rep)

简单测试结论是也可以翻页

wbi 的接口好像有变动吧?BAC 那个不是 /x/v2/reply/main ,难道B站后端还会写参数兼容?

直接套用现有的参数是不行的吧...(?

z0z0r4 commented 1 year ago

这里讨论的是接口有变而非用已有的

blyc commented 1 year ago

next参数,bac文档说从零开始,可我个人实测却是从1开始,具体自行测试 api.bilibili.com/x/v2/reply/wbi/main?type=1&oid=275261884&mode=3&next=1 api.bilibili.com/x/v2/reply/wbi/main?type=1&oid=275261884&mode=3&next=2 BAC链接:socialsisteryi.github.io/bilibili-API-collect/docs/comment/list.html#%E8%8E%B7%E5%8F%96%E8%AF%84%E8%AE%BA%E5%8C%BA%E6%98%8E%E7%BB%86-%E6%87%92%E5%8A%A0%E8%BD%BD 同时本库也有另一个获取评论的api的实现(和上文不是同一个api):nemo2011.github.io/bilibili-api/#/modules/comment?id=async-def-get_comments

def test(credential):
    for i in [1, 2, 3]:
        rep = sync(comment.get_comments(page_index=i,credential=credential, oid=275261884, type_=CommentResourceType.VIDEO,order=OrderType.LIKE))
        print(rep)

简单测试结论是也可以翻页

wbi 的接口好像有变动吧?BAC 那个不是 /x/v2/reply/main ,难道B站后端还会写参数兼容?

直接套用现有的参数是不行的吧...(?

因为比对过响应数据基本没差别 (搞不懂阿b)

图片

https://api.bilibili.com/x/v2/reply/main?type=1&oid=275261884&mode=3&next=2 https://api.bilibili.com/x/v2/reply/wbi/main?type=1&oid=275261884&mode=3&next=2

z0z0r4 commented 1 year ago

我晚点 urlencode 看看提交了…这 \ 啥玩意,干脆直接撸编码

我还是不知道咋整这个 \,开摆

z0z0r4 commented 1 year ago

image

z0z0r4 commented 1 year ago

等 v16.0.0 或者自己拉取 dev 分支

feat: comment.get_comments_lazy

z0z0r4 commented 1 year ago

有bug 直接在这里提