Nemo2011 / bilibili-api

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

【建议】可以在仓库里哪个不起眼的小角落加上我这个解析器吗 #184

Closed Drelf2018 closed 1 year ago

Drelf2018 commented 1 year ago

很久很久之前,我刚来到这个仓库,当时有个 Issue #31 他说:

这个包可以在脚手架里用吗,我在vue-cli开发模式下启动直接提示跨域

解决办法是在后端写请求,例如使用 fastapi + uvicorn 开一个后端,自己写接口。

开始我不懂啥意思,直到后来我也写了点 vue ,用到了 bilibili 的接口发现跨域,我就打算按照那个方法写后端。

但是一个个重新写接口名再找对应函数确实很累,所以我写了这个解析器:

import re
from enum import Enum
from inspect import iscoroutinefunction as isAsync, isfunction as isFn, isclass
from typing import List, Tuple

import bilibili_api
import uvicorn
from fastapi import FastAPI, Response

pattern = re.compile(r'(?:([:\$\w]+(?:=\w+)?),?)')
app = FastAPI()

class Parser:
    def __init__(self, var: str):
        self.valid = True
        self.varDict = dict(v.split("<-") for v in var.split(";")) if var else dict()

    async def __aenter__(self):
        for key, val in self.varDict.items():
            obj, err = await self.parse(val)
            if not err:
                if isinstance(obj, bilibili_api.Credential):
                    self.valid = await obj.check_valid()
                self.varDict[key] = obj
        return self

    async def __aexit__(self, type, value, trace): ...

    async def parse(self, path: str) -> Tuple[any, bool]:
        "分析指令"

        sentences = path.split(".")  # 指令列表
        position: any = bilibili_api  # 起始点

        async def inner() -> bool:
            "递归取值"

            nonlocal position
            if len(sentences) == 0:
                return position is None  # 判断是否取得具体对象

            sentence = sentences.pop(0)
            # 分解执行的函数名、参数、指名参数
            flags: List[str] = pattern.findall(sentence)
            func = flags.pop(0)
            args, kwargs = list(), dict()

            for flag in flags:
                # 假设分为键值形式 利用列表特性从 -1 读取值
                # 即使没有键也能读到值
                arg = flag.split("=")
                # 类型装换
                if arg[-1].endswith(":int"):
                    arg[-1] = int(arg[-1][:-4])
                # 将值与储存的变量替换
                arg[-1] = self.varDict.get(arg[-1], arg[-1])
                # 存入对应的参数、指名参数
                if len(arg) == 1:
                    args.append(arg[0])
                else:
                    kwargs[arg[0]] = arg[1]

            # 开始转移
            if isinstance(position, dict):
                position = position.get(func, None)
            else:
                position = getattr(position, func, None)

            # 赋值参数
            if isAsync(position):
                position = await position(*args, **kwargs)
            elif isFn(position):
                position = position(*args, **kwargs)
            elif isclass(position) and not issubclass(position, Enum):
                position = position(*args, **kwargs)

            # 递归
            return await inner()

        err = await inner()
        return position, err

@app.get("/{path}")
async def bilibili_api_web(response: Response, path: str, var: str = "", max_age: int = -1):
    # 返回头设置
    response.headers["Access-Control-Allow-Origin"] = "*"
    if max_age != -1:
        response.headers["Cache-Control"] = f"max-age={max_age}"

    # 先判断是否有效 再分析
    async with Parser(var) as parser:
        if not parser.valid:
            return {"code": 1, "error": "Cookies Error"}
        try:
            obj, err = await parser.parse(path)  # 什么 golang 写法
            if not err:
                return {"code": 0, "data": obj}
            else:
                return {"code": 2, "error": "Path Error"}
        except Exception as e:
            return {"code": 3, "error": str(e)} 

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=9000)

用法

这段代码我已经部署在阿里云的函数计算里了,域名:https://aliyun.nana7mi.link

from bilibili_api import user, sync

async def main():
    return await user.User(uid=2).get_user_info()

print(sync(main()))

上述代码现在只需要一个链接:[https://aliyun.nana7mi.link/user.User(2).get_user_info()](https://aliyun.nana7mi.link/user.User(2).get_user_info()) 就能实现。

属于是从接口来回接口去了。

类似的还有 [https://aliyun.nana7mi.link/live.LiveRoom(21452505).get_room_info()](https://aliyun.nana7mi.link/live.LiveRoom(21452505).get_room_info())


FAQ

Q1. 这个有什么用呢?

前端访问不跨域了。

Q2. 为什么要解析器,直接用 eval() 不好吗?

有安全隐患,用解析器这样一步一步调用比较安全。


进阶用法

https://aliyun.nana7mi.link/comment.get_comments(708326075350908930,type,1:int)?var=type<-comment.CommentResourceType.DYNAMIC

在网址后使用 var 参数用于储存变量,变量名与值用 <- 连接,多个变量用 ; 分割。

这个变量是另一个需要被解析的文本,为什么不直接放在网址里呢?因为放前面会被当做字符串传进去。

同时为了不让所有参数都以字符串传入,还加了类型标注,在变量后使用类似 :int 的方式来强制转换,目前只写了 int 的。


再高级一点呢

使用 ?max_age=86400 参数设置为期 86400 秒的缓存。

在获取的字典结果后再使用 .key 的方式获得更精细数据,节省带宽,例如:

[https://aliyun.nana7mi.link/user.User(2).get_user_info().face?max_age=86400](https://aliyun.nana7mi.link/user.User(2).get_user_info().face?max_age=86400)

Nemo2011 commented 1 year ago

我的想法是这样子的:把解析器代码加进模块的 tools 包里面或者是整理成另一个模块发布在 pypi (如果选第二种的话那么你就自己建一个仓库吧),然后在模块的文档(或者是整理之后的另一个模块的 README)中用一个简单的 vue + python 后端的示例展示一下这个 API 接口怎么用,同时提供一下部署后端的文档。