Nemo2011 / bilibili-api

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

[建议] 从底层 request 重构项目 #760

Open Drelf2018 opened 3 months ago

Drelf2018 commented 3 months ago

v0.1.0

import asyncio

import httpx
from bilibili_api import HEADERS

class Api:
    def __init__(self, path: str):
        # 假装导入了接口信息
        self.url = "https://api.bilibili.com/x/web-interface/view"
        self.method = "GET"
        # 导入一些默认值
        self.query = {}
        self.data = {}

    def __call__(self, fn):
        return self.request

    async def request(self, **kwargs):
        data = self.data.copy()
        if "data" in kwargs:
            data.update(kwargs.pop("data"))

        query = self.query.copy()
        query.update(kwargs)

        return httpx.request(self.method, self.url, params=query, data=self.data, headers=HEADERS.copy()).json()["data"]

@Api("path.to.api")
async def get_video_info(bvid: str) -> dict: ...

async def main():
    info = await get_video_info(bvid="BV1Xw4m1C7K4")
    print(info.get("title"))

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

image

这是一段我想实现的代码,是能获取到结果的,最妙的是能识别到原来函数上标注的返回值类型。

函数体内部不需要写任何代码,只要在参数那里标注好类型,剩下的就会直接从数据文件中读取。

调用时必须要通过指名的方式,这样才能把值放在正确的 key 里面。

z0z0r4 commented 3 months ago

基本上我是没空了,围观

直接从数据文件中读取

这个信息量有点大啊,接口定义有示例吗?

这个 class Api: 看的比较迷糊,自动导入 URL 和 method ?

最妙的是能识别到原来函数上标注的返回值类型

现在的函数不能拿到返回值类型吗...?没太看懂,还是说包装了装饰器之后

没看出啥意义,期待讲解,以 https://github.com/Nemo2011/bilibili-api/blob/48e6350ccd9596dadf65a6443927c9ccfcdfd6c6/bilibili_api/user.py#L529-L547 为例

顶多把三行的接口调用变成了一行(假如 class Api: 是通用的...

Drelf2018 commented 3 months ago

v0.1.1

import asyncio
from inspect import Parameter, signature

import httpx
from bilibili_api import HEADERS

class Api:
    def __init__(self, method: str, url: str):
        self.method = method
        self.url = url
        self.params = {}
        self.data = {}
        self.headers = HEADERS.copy()

    def __call__(self, fn):
        for key, param in signature(fn).parameters.items():
            val = param.default
            if val is Parameter.empty:
                continue
            if key == "data":
                self.data = val
            elif key == "headers":
                self.headers.update(val)
            else:
                self.params[key] = val

        return self.request

    async def request(self, **kwargs):
        data = self.data.copy()
        if "data" in kwargs:
            data.update(kwargs.pop("data"))

        headers = self.headers.copy()
        if "headers" in kwargs:
            headers.update(kwargs.pop("headers"))

        params = self.params.copy()
        params.update(kwargs)

        return httpx.request(self.method, self.url, params=params, data=data, headers=headers).json()["data"]

class Get(Api):
    def __init__(self, url: str):
        return super().__init__("Get", url)

@Api("GET", "https://api.bilibili.com/x/web-interface/view")
async def get_video_info(*, bvid: str) -> dict: ...

@Get("https://api.bilibili.com/x/v2/reply")
async def get_replies(*, oid: str, type: int = 11) -> dict: ...

async def main():
    bvid = "BV1gz421m7mk"
    info = await get_video_info(bvid=bvid)
    print(f'{bvid} title: {info.get("title")}')

    data = await get_replies(oid="315871473")
    for reply in data["replies"]:
        print(reply["member"]["uname"]+": "+reply["content"]["message"])
        break

    data = await get_replies(oid="909825870340816915", type=17)
    for reply in data["replies"]:
        print(reply["member"]["uname"]+": "+reply["content"]["message"])
        break

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

控制台

BV1gz421m7mk title: 小羊🐧振翅【Animelo Summer Live 2023 MyGO!!!!!部分】
-Dashstar-: ( ゜- ゜)つロ
死鱼眼八幡: 急急急急急急
Drelf2018 commented 3 months ago

v0.2.0

import asyncio
import json
import re
from enum import Enum, IntFlag
from functools import partial
from inspect import Parameter, signature

from bilibili_api import (HEADERS, Credential, ResponseCodeException, enc_wbi, get_mixin_key,
                          get_session)
from bilibili_api.comment import CommentResourceType, OrderType
from bilibili_api.user import AudioOrder, VideoOrder

class Flag(IntFlag):
    WBI         = 1 << 0
    VERIFY      = 1 << 1
    NO_CSRF     = 1 << 2
    JSON_BODY   = 1 << 3
    IGNORE_CODE = 1 << 4

class Api:
    def __init__(self, url: str, flag: Flag = 0):
        self.method = self.__class__.__name__.upper()
        self.url = url
        self.flag= flag
        self.params = {}
        self.data = {}
        self.headers = HEADERS.copy()
        self.credential = Credential()

    @property
    def wbi(self):
        return self.flag & Flag.WBI != 0

    @property
    def verify(self):
        return self.flag & Flag.VERIFY != 0

    @property
    def no_csrf(self):
        return self.flag & Flag.NO_CSRF != 0

    @property
    def json_body(self):
        return self.flag & Flag.JSON_BODY != 0

    @property
    def ignore_code(self):
        return self.flag & Flag.IGNORE_CODE != 0

    def __call__(self, fn):
        for key, param in signature(fn).parameters.items():
            val = param.default
            if val is Parameter.empty:
                continue
            if key == "data":
                self.data = val
            elif key == "headers":
                self.headers.update(val)
            elif key == "credential":
                if val is not None:
                    self.credential = val
            else:
                if isinstance(val, Enum):
                    val = val.value
                self.params[key] = val

        return self.wrapper

    async def wrapper(self, **kwargs):
        data = self.data.copy()
        if "data" in kwargs:
            data.update(kwargs.pop("data"))

        headers = self.headers.copy()
        if "headers" in kwargs:
            headers.update(kwargs.pop("headers"))

        if "credential" in kwargs:
            self.credential = kwargs.pop("credential")

        params = self.params.copy()
        for key, val in kwargs.items():
            if isinstance(val, Enum):
                val = val.value
            params[key] = val

        return await self.request(params=params, data=data, headers=headers)

    async def request(self, params: dict, data: dict, headers: dict):
        """
        向接口发送请求。

        Returns:
            接口未返回数据时,返回 None,否则返回该接口提供的 data 或 result 字段的数据。
        """
        # 如果接口需要 Credential 且未传入则报错 (默认值为 Credential())
        if self.verify:
            self.credential.raise_for_no_sessdata()

        # 请求为非 GET 且 no_csrf 不为 True 时要求 bili_jct
        if self.method != "GET" and not self.no_csrf:
            self.credential.raise_for_no_bili_jct()

        # if settings.request_log:
        #     settings.logger.info(self)

        # jsonp
        if params.get("jsonp") == "jsonp":
            params["callback"] = "callback"

        if self.wbi:
            # global wbi_mixin_key
            # if wbi_mixin_key == "":
            #     wbi_mixin_key = await get_mixin_key()
            # enc_wbi(params, wbi_mixin_key)
            enc_wbi(params, await get_mixin_key())

        # 自动添加 csrf
        if not self.no_csrf and self.method in ["POST", "DELETE", "PATCH"]:
            data["csrf"] = self.credential.bili_jct
            data["csrf_token"] = self.credential.bili_jct

        cookies = self.credential.get_cookies()

        # if self.credential.buvid3 is None:
        #     global buvid3
        #     if buvid3 == "" and self.url != API["info"]["spi"]["url"]:
        #         buvid3 = (await get_spi_buvid())["b_3"]
        #     cookies["buvid3"] = buvid3
        # else:
        #     cookies["buvid3"] = self.credential.buvid3
        cookies["Domain"] = ".bilibili.com"

        config = {
            "url": self.url,
            "method": self.method,
            "data": data,
            "params": params,
            # "files": self.files,
            "cookies": cookies,
            "headers": headers,
        }
        # config.update(kwargs)
        # print(config)
        if self.json_body:
            config["headers"]["Content-Type"] = "application/json"
            config["data"] = json.dumps(config["data"])

        session = get_session()
        resp = await session.request(**config)

        # 检查响应头 Content-Length
        content_length = resp.headers.get("content-length")
        if content_length and int(content_length) == 0:
            return None

        if "callback" in params:
            # JSONP 请求
            resp_data: dict = json.loads(re.match("^.*?({.*}).*$", resp.text, re.S).group(1))
        else:
            # JSON
            resp_data: dict = json.loads(resp.text)

        # 检查 code
        if not self.ignore_code:
            code = resp_data.get("code")

            if code is None:
                raise ResponseCodeException(-1, "API 返回数据未含 code 字段", resp_data)
            if code != 0:
                msg = resp_data.get("msg")
                if msg is None:
                    msg = resp_data.get("message")
                if msg is None:
                    msg = "接口未返回错误信息"
                raise ResponseCodeException(code, msg, resp_data)
        # elif settings.request_log:
        #     settings.logger.info(resp_data)

        real_data = resp_data.get("data")
        if real_data is None:
            real_data = resp_data.get("result")
        return real_data

class Get(Api):
    """
    Get method api
    """

@Get("https://api.bilibili.com/x/web-interface/view")
async def get_info(*, bvid: str) -> dict:
    """
    获取视频信息。

    Returns:
        dict: 调用 API 返回的结果。
    """

@Get("https://api.bilibili.com/x/v2/reply")
async def get_comments(
    *,
    oid: int,
    type: CommentResourceType,
    pn: int = 1,
    order: OrderType = OrderType.TIME,
    credential: Credential = None
) -> dict:
    """
    获取资源评论列表。

    第二页以及往后需要提供 `credential` 参数。

    Args:
        oid (int)                  : 资源 ID。

        type (CommentsResourceType) : 资源类枚举。

        pn (int, optional)        : 页码. Defaults to 1.

        order (OrderType, optional)  : 排序方式枚举. Defaults to OrderType.TIME.

        credential (Credential, optional) : 凭据。Defaults to None.

    Returns:
        dict: 调用 API 返回的结果
    """

class User:
    def __init__(self, uid: int, credential: Credential = None):
        self.get_audios = partial(self.get_audios, uid=uid, credential=credential)
        self.get_videos = partial(self.get_videos, mid=uid, credential=credential)

    @Get("https://api.bilibili.com/audio/music-service/web/song/upper")
    async def get_audios(self, *, pn: int = 1, ps: int = 30, order: AudioOrder = AudioOrder.PUBDATE) -> dict:
        """ 
        获取用户投稿音频。 

        Args: 
            order (AudioOrder, optional): 排序方式. Defaults to AudioOrder.PUBDATE. 
            pn    (int, optional)       : 页码数,从 1 开始。 Defaults to 1. 
            ps      (int, optional)       : 每一页的视频数. Defaults to 30. 

        Returns: 
            dict: 调用接口返回的内容。 
        """

    @Get("https://api.bilibili.com/x/space/wbi/arc/search", Flag.WBI)
    async def get_videos(self, *,
        tid: int = 0,
        pn: int = 1,
        ps: int = 30, 
        keyword: str = "", 
        order: VideoOrder = VideoOrder.PUBDATE,
        web_location: int = 1550101,
        dm_img_list: str = "[]",
        dm_img_str: str = "V2ViR0wgMS4wIChPcGVuR0wgRVMgMi4wIENocm9taXVtKQ",
        dm_cover_img_str: str = "QU5HTEUgKEludGVsLCBJbnRlbChSKSBVSEQgR3JhcGhpY3MgNjMwICgweDAwMDAzRTlCKSBEaXJlY3QzRDExIHZzXzVfMCBwc181XzAsIEQzRDExKUdvb2dsZSBJbmMuIChJbnRlbC",
    ) -> dict:
        """
        获取用户投稿视频信息。

        Args:
            tid (int, optional)       : 分区 ID. Defaults to 0(全部).

            pn (int, optional)       : 页码,从 1 开始. Defaults to 1.

            ps (int, optional)       : 每一页的视频数. Defaults to 30.

            keyword (str, optional)       : 搜索关键词. Defaults to "".

            order (VideoOrder, optional): 排序方式. Defaults to VideoOrder.PUBDATE

        Returns:
            dict: 调用接口返回的内容。
        """

async def main():
    credential = Credential(...)

    user = User(12344667, credential=credential)
    data = await user.get_videos()
    for v in data["list"]["vlist"]:
        print("【"+v["title"]+"】封面:", v["pic"])
        break

    data = await get_comments(oid=909825870340816915, type=CommentResourceType.DYNAMIC)
    for reply in data["replies"]:
        print(reply["member"]["uname"]+":", reply["content"]["message"])
        break

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

控制台

【手术前一天的神楽mea】封面: http://i1.hdslb.com/bfs/archive/f302d499a075067eb5a90ff7ddd498348dbac278.jpg
死鱼眼八幡: 急急急急急急