06.拉取操作事件¶
115 操作事件就是你网盘中发生的文件和目录的变更事件,主要应该包括(部分可能未被实现)
增加:创建目录、上传、复制、离线下载、接收分享、从回收站还原
删除:删除到回收站、从回收站永久删除
改动:改名、移动、隐藏、显示、星标、标签、备注、打分等
查看:打开目录、打开文件、影音文件的播放进度等
我根据 P115Client.life_behavior_detail_app 接口的返回值分析,目前所支持的操作类型如下:
操作类型 |
类型代码 |
解释说明 |
|---|---|---|
upload_image_file |
1 |
上传图片 |
upload_file |
2 |
上传文件或目录(不包括图片) |
star_image |
3 |
给图片设置星标 |
star_file |
4 |
给文件或目录设置星标(不包括图片) |
move_image_file |
5 |
移动图片 |
move_file |
6 |
移动文件或目录(不包括图片) |
browse_image |
7 |
浏览图片 |
browse_video |
8 |
浏览视频 |
browse_audio |
9 |
浏览音频 |
browse_document |
10 |
浏览文档 |
receive_files |
14 |
接收文件 |
new_folder |
17 |
新增目录 |
copy_folder |
18 |
复制目录 |
folder_label |
19 |
目录设置标签 |
folder_rename |
20 |
目录改名 |
delete_file |
22 |
删除文件或目录 |
copy_file |
23 |
复制文件 |
file_rename |
24 |
文件改名 |
操作事件的粗略列表¶
目前有两个接口可以获取最近的事件列表,并提供了一定的统计信息
P115Client.life_list:老接口,支持的查询参数多且复杂,并不推荐使用P115Client.life_recent_operations:新接口,推荐使用,虽然可能支持的事件类型较少
操作事件的明细列表¶
P115Client.life_behavior_detail_app:老接口,推荐使用,如果省略查询的日期和类型,可以直接按发生事件逆序拉取各种类型的事件P115Client.life_recent_operation_items:新接口,能力远弱于上一个,需要指定查询的日期和类型,且部分类型可能尚不支持
对操作事件的周期轮询策略¶
当你需要以一定的时间间隔去查询自上一次查询(或者自某个时间点)以来,究竟发生了哪些事件,你其实需要的是对 P115Client.life_behavior_detail_app 接口的某种专门使用。
我们不妨设想这样一个情况,假设你要监测某个目录内的文件变化,更确切的讲,你需要监测以下 4 类事件
新增
操作类型 |
类型代码 |
解释说明 |
|---|---|---|
upload_image_file |
1 |
上传图片 |
upload_file |
2 |
上传文件或目录(不包括图片) |
receive_files |
14 |
接收文件 |
new_folder |
17 |
新增目录 |
copy_folder |
18 |
复制目录 |
copy_file |
23 |
复制文件 |
删除
操作类型 |
类型代码 |
解释说明 |
|---|---|---|
delete_file |
22 |
删除文件或目录 |
移动
操作类型 |
类型代码 |
解释说明 |
|---|---|---|
move_image_file |
5 |
移动图片 |
move_file |
6 |
移动文件或目录(不包括图片) |
改名
操作类型 |
类型代码 |
解释说明 |
|---|---|---|
folder_rename |
20 |
目录改名 |
file_rename |
24 |
文件改名 |
那么你只需要用 P115Client.life_behavior_detail_app 分页拉取从现在开始,直到以前某个时间点或事件 id 为止的所有事件,然后排除掉你所不需要的事件即可。只要你合理设定查询的时间间隔,或者根据某种策略动态调整时间间隔(甚至可经由某种事件驱动机制触发),你就不用害怕被服务端暂禁接口。
我在 p115client.tool.life 模块中其实已经封装了一些工具函数,但出于理解的需要,我在下面给出一个简单的版本
from collections.abc import Iterator
from time import sleep, time
from p115client import P115Client, check_response
def iter_recent_operations(
client: P115Client,
from_id: int = 0,
from_time: int = 0,
filter_types: None | tuple[int, ...] = (1, 2, 5, 6, 14, 17, 18, 20, 24, 22, 23),
cooldown: float = 0,
pagesize: int = 1000,
) -> Iterator[dict]:
"""迭代获取最近的操作事件
:param client: 115 的客户端对象
:param from_id: 从此 id 开始(不含)
:param from_time: 从此发生的时间点开始(含)
:param filter_types: 需要输出的操作类型(默认只输出更改类,即增删改)
:param cooldown: 两次接口调用之间的冷却时间
:param pagesize: 分页大小,即每次查询的数据条数
:return: 事件的迭代器,按发生时间逆序
"""
last_called = 0
payload = {"offset": 0, "limit": pagesize}
# NOTE: 对于同一个文件或目录,只输出其最近的一条操作事件
seen_file_ids = set()
while True:
# NOTE: 冷却时间不够的,用睡眠补足
if last_called and cooldown > 0 and (sleep_t := last_called + cooldown - time()) > 0:
sleep(sleep_t)
resp = client.life_behavior_detail_app(payload)
last_called = time()
check_response(resp)
data = resp["data"]
for item in data["list"]:
if (from_id and int(item["id"]) <= from_id or
from_time and item["create_time"] < from_time
):
break
elif item["file_id"] in seen_file_ids:
continue
elif filter_types and item["type"] in filter_types:
yield item
seen_file_ids.add(item["file_id"])
if not data["next_page"]:
break
payload["offset"] += len(data["list"])
一种基于数据库的运用妙招¶
某些应用,会把 115 网盘上某些目录内的文件信息保存到数据库,往后再见机更新。这里我介绍一种基于数据库的运用办法
首先全量拉取某个目录内的文件列表到数据库
之后,按一定周期,比如 1 秒,增量拉取事件,并把变更更新到数据库
基于数据库,提供查询目录结构变更的接口,建议至少提供下面这些信息
更新类型:增加、删除、改名、移动
更新前:路径
更新后:路径
可以把查询接口做成 webhook 等协议,实现由服务器端主动推送,而应用端基于来信,事件驱动地执行相应操作
在更新数据库的时候,有一些点需要注意:
为了高效地获知路径信息,你最好批量先去获取整个网盘的目录信息,这个可以用
p115client.tool.iter_dirs获取只需要全量获取目录内的文件列表,而不用管目录,因为通过上一条提示,你已经获取了所有目录信息
不建议在数据表中存储路径字段,而应该借由查询时再去构建路径,这样对于移动和改名只需要改一条数据即可,不然的话,以后对一个目录进行改名或移动,你需要把归在它之下的所有文件和目录的路径全部改一遍
查询某个 id 对应的路径,可以借助 SQL 的 公共表表达式(Common Table Expression),以 SQLite 数据库为例
WITH id(id) AS (
-- :id 是你所要查询的目录 id
SELECT :id
), path_raw AS (
SELECT id, parent_id, name, 0 AS _n FROM data JOIN id USING(id)
UNION ALL
SELECT data.id, data.parent_id, data.name, _n+1
FROM data JOIN path_raw ON (data.id=path_raw.parent_id)
)
SELECT concat('/', path) AS path FROM (
SELECT id, group_concat(name, '/') OVER (
ORDER BY _n DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS path, _n
FROM path_raw
) WHERE _n = 0
UNION ALL
SELECT '/' FROM id WHERE id=0
相应的,也可以递归查询某个目录下的所有文件和目录列表,并且附带路径,再以 SQLite 数据库为例
WITH id(id) AS (
-- :id 是你所要查询的目录 id
SELECT :id
), path_raw AS (
SELECT id, parent_id, name, 0 AS _n FROM data JOIN id USING(id)
UNION ALL
SELECT data.id, data.parent_id, data.name, _n+1
FROM data JOIN path_raw ON (data.id=path_raw.parent_id)
), path AS (
SELECT id, concat('/', path) AS path FROM (
SELECT id, group_concat(name, '/') OVER (
ORDER BY _n DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS path, _n
FROM path_raw
) WHERE _n = 0
UNION ALL
SELECT 0, '' FROM id WHERE id=0
), data2 AS (
SELECT data.*, concat(path, '/', name) AS path
FROM data JOIN path WHERE parent_id=path.id
UNION ALL
SELECT data.*, concat(data2.path, '/', data.name)
FROM data JOIN data2 ON (data.parent_id=data2.id)
)
SELECT * FROM data2
为查询目录结构变更的需要,你最好专门建一张表,然后当原始数据表更新时,由触发器来插入这张查询表。而你在插入或更新数据时,必须保证,被插入数据的祖先 id 所对应的目录数据已经在数据库中了,不然就无法构建完整的路径
开箱即用的数据库更新¶
我在 p115client.tool 模块提供了一些工具函数,其中 p115client.tool.updatedb 实现了全量更新,updatedb_life_iter 则实现了基于操作事件的增量更新。
由于我的实现完全是为了快速更新数据库,所以根本没有考虑其他的应用场景,甚至故意忽略,因此这个数据库可能缺少你需要的各种字段。针对你的具体应用,你可以参照我的源代码,自己写一个更全面覆盖的,或者借助 AI 或独立思考,DIY 一个能实际派上用场的。
最后的一点浅见¶
就目前来讲,115 网盘的操作事件差不多该有的全有了
有的:文件和目录的增(上传文件,创建目录,复制)删(删除)改(改名,移动)
尚缺:离线下载、回收站还原、隐藏和显示、接收文件列表(目前只能得到最顶层的信息,需要你自己去拉取完整列表)等
对于还尚缺的,只要你先限制好使用方式,其实是不会造成什么问题的。还有一个比较怪的地方,就是创建 Office 文档,事件为 “browse_document”,可查看它时,也是这个事件,所以哪怕是类型名带 browse_ 前缀的事件,也可能会产生新的文件。
总的来说,目前的事件已经足够覆盖所需,你在一次全量更新后,以后只需要拉取增量即可,也并不需要往后靠偶尔的全量来消除误差。对于增量更新,完全也可以做成出触发式的,在需要的时候,手动触发一次。自动化虽然方便,但是根据奥卡姆剃刀原理,如果没有必要,不用过度设计。当然追求实时更新也是不难的,按照我的评估,把两次接口调用的冷却时间设为 0.1 秒,也不怎么会引起封禁,如果胆子小,就设为 1 秒,其实也差不多够了,绝对实时是物理上不可能的,切记!