当前位置:首页>python>我用 Python 扒了小米 15 底层日志,发现一个“44天不睡觉”的怪物

我用 Python 扒了小米 15 底层日志,发现一个“44天不睡觉”的怪物

  • 2026-10-11 07:38:29
我用 Python 扒了小米 15 底层日志,发现一个“44天不睡觉”的怪物

我用 Python 扒了一遍小米 15 的底层日志,结果发现手机里住着一个“44 天不睡觉”的怪物

有时候,手机比人更诚实。

我本来只是想做一件很简单的事:看一下自己今天到底在各个 App 上花了多少时间。

系统自带的“数字健康”当然能看,但它太像一个温柔的提醒工具:告诉你“今天用久了”,然后就没有然后了。第三方统计 App 也能看,但问题更多:常驻后台、耗电、被系统杀、数据断流,还要跟 HyperOS 的后台管理斗智斗勇。

所以我决定换一种方式。

不用第三方 App,不跑自动化测试框架,不模拟点击 UI。 直接从 Android 系统底层拿数据。

也就是:

adb shell dumpsys usagestats --daily

然后用 Python 清洗、聚合、可视化。

本来以为只是一次普通的手机使用时长统计,结果数据一出来,我沉默了。

因为它告诉我:

王者荣耀今天使用了 63135 分钟。

换算一下,大概是:

44 天。

一天里面打了 44 天王者。

这已经不是自律问题了,这是时空连续性问题。


01 为什么我不想用第三方统计 App?

很多人想统计手机使用时间,第一反应是去应用商店装一个工具。

但问题是,这类 App 本身也会变成“被统计对象”。

它要常驻后台,要拿权限,要持续监听,还要和系统的省电策略反复拉扯。尤其是小米这种后台管理比较强的系统环境里,今天能跑,明天可能就被冻住。

还有一种方案是 Appium 这类自动化测试框架。

但用它来统计使用时间,就有点像为了切一片面包,先开一台挖掘机。

Appium 更适合做 UI 自动化测试。它需要驱动环境,需要模拟操作,需要维持一个相对重的测试链路。只是为了读几个使用时长数字,实在没必要把事情搞这么重。

我想要的是一个更干净的方案:

电脑发起请求,手机返回底层日志,Python 负责清洗和展示。

它不依赖第三方统计 App,不需要手机端长期运行服务,也不会额外占用前台资源。

思路很朴素:

既然 Android 系统自己已经记录了使用行为,那我为什么不直接去读系统记录?


02 整条链路其实很简单

这次用的是局域网无线 ADB。

也就是说,电脑和手机在同一个 Wi-Fi 下,不用插数据线,也能直接和手机建立调试连接。

大致流程是这样:

  1. 在小米 15 上打开开发者选项;
  2. 开启 USB 调试相关权限;
  3. 开启无线调试;
  4. 复制手机上显示的局域网地址和端口;
  5. 电脑终端连接手机。

命令大概是这样:

adb connect 192.168.xx.xx:xxxxx

终端返回 connected 之后,电脑和手机之间的通道就打通了。

接下来,用 Python 调用 ADB:

adb shell dumpsys usagestats --daily

把系统吐出来的原始日志拿回来,再用正则把包名和 totalTimeUsed 字段抠出来,最后转成分钟数。

技术上没有什么玄学。

真正玄学的是数据本身。


03 数据出来那一刻,我人有点懵

清洗之后,Top 10 长这样:

排名
应用
使用时长
1
王者荣耀
63,135.0 分钟
2
微信
47,211.9 分钟
3
aweme
15,593.8 分钟
4
googlequicksearchbox
5,001.9 分钟
5
系统桌面
3,137.7 分钟
6
QQ
2,914.5 分钟
7
video
1,525.3 分钟
8
bili
1,513.1 分钟
9
browser
1,315.4 分钟
10
mall
1,241.2 分钟

如果只是看“分钟”这个单位,可能还没有那么刺激。

换算成现实时间之后,画风就变了:

应用
分钟
折算后大约相当于
王者荣耀
63,135
44 天连续在线
微信
47,212
33 天持续聊天
aweme
15,594
11 天不停刷短视频
googlequicksearchbox
5,002
3.5 天持续搜索
系统桌面
3,138
2.2 天一直划桌面
QQ
2,915
2 天在线怀旧
video
1,525
1 天看视频
bili
1,513
1 天看弹幕

看到这里,我第一反应不是“我怎么用这么多”。

而是:

这玩意儿肯定不是今天。

因为一天只有 1440 分钟。

王者荣耀一个应用就 63135 分钟,相当于把今天扩展成了 44 天。 微信也不甘示弱,直接给我算出了 33 天。

如果这些都是真的,那我不是手机重度用户。

我是时间管理局漏网之鱼。


04 这不是“今日使用时长”,更像是一次统计口径事故

这组数据最有意思的地方,不在于它多吓人,而在于它暴露了一个问题:

系统底层日志里的“daily”,不一定等于我们口语里的“今天”。

很多人看到 --daily,会自然理解成“当天数据”。

但在 Android 的 UsageStatsManager 里,事情没有这么简单。系统会维护不同粒度的统计桶,比如 daily、weekly、monthly、yearly。不同 ROM、不同系统版本、不同缓存策略下,最终输出的数据可能并不是一个干净的自然日切片。

尤其是在定制系统里,可能还会叠加几类影响:

第一,跨天结算不一定按我们想象的方式发生。 某些前台使用事件可能在统计桶里继续滚动,直到系统认为它该被归档或重算。

第二,重启、系统更新、缓存策略,都会影响日志聚合结果。 你看到的是“系统当前吐出来的统计值”,不一定是“今天零点到现在的真实使用值”。

第三,时区、系统时间、网络环境变化,也可能让时间窗口变得更复杂。 如果手机、电脑、网络代理、系统时间之间发生过明显切换,日志里的时间戳边界就更容易变得不直观。

所以,这组数据最合理的解释不是:

我今天真的打了 44 天王者。

而是:

我拿到的是一个被系统统计口径、缓存窗口、历史聚合共同影响过的使用时长结果。

这就很有意思了。

因为它提醒我们一件事:

数据看起来很客观,但数据口径并不天然诚实。


05 但这组“异常数据”依然有价值

虽然它不适合被当成严格的“今日使用时长”,但它不是废数据。

恰恰相反,它很有价值。

它至少说明了三个问题。

第一,娱乐应用确实占据了绝对优势

哪怕不把它理解成一天的数据,只要看排序,就能看出使用重心。

王者荣耀、微信、aweme、QQ、video、bili 都排在前面。

这说明手机真正被高频占用的场景非常清楚:

游戏、社交、短视频、视频内容。

这些应用不是偶尔打开一下,而是在系统统计里留下了明显的前台痕迹。

换句话说,手机不是简单的工具。

它更像一台随身携带的注意力分发机器。

第二,搜索行为很有意思

googlequicksearchbox 排到了第四。

这说明搜索框并不是摆设。

很多时候,我们以为自己打开手机是在“解决问题”:查资料、搜答案、找方法。 但搜索很容易变成另一种形式的缓冲区。

比如:

“如何减少手机使用时间” “怎么戒游戏” “怎么提高专注力” “睡前玩手机怎么办”

然后搜索完,收藏一下,关掉页面。

继续打开王者荣耀。

搜索不一定带来行动。

有时候,它只是让人短暂感觉:

我已经开始改变了。

第三,系统桌面也很能说明问题

“系统桌面”排在第五。

这类数据通常不显眼,但很真实。

很多时候我们并不是一打开手机就进入某个 App,而是在桌面上来回滑、找图标、看通知、切任务、犹豫下一步干什么。

这类时间很碎。

碎到你不会主动记得。

但系统会记得。

它说明手机使用不是一个个清晰的动作,而是一连串微小的注意力漂移。


06 我让大模型也看了一眼,它的反应很真实

我把清洗后的头部数据丢给大模型,让它做一个复盘。

它没有急着下结论,第一反应和我差不多:

这不可能是一天的数据。

然后它做了一个很朴素的换算:

王者荣耀 63135 分钟,约等于 43.8 天。 微信 47211 分钟,约等于 32.8 天。 aweme 15593 分钟,约等于 10.8 天。

这组数据如果被标记为“今日”,那就一定有统计口径问题。

但如果把它看成某种更长周期的累积痕迹,它又确实像一份数字生活切片:

游戏提供即时反馈,社交提供持续连接,短视频提供低成本刺激,搜索提供“我正在处理问题”的心理安慰。

这个判断很扎心。

因为它不像是在分析一台手机。

更像是在分析一个人如何分配自己的注意力。


07 代码其实不复杂,关键是思路

完整脚本分成几块:

  1. 通过 ADB 获取 dumpsys usagestats --daily;
  2. 用正则解析包名和使用时长字段;
  3. 将毫秒或时间格式转换成分钟;
  4. 按使用时长排序;
  5. 生成 Top 10 横向条形图;
  6. 可选:把数据交给大模型生成复盘。

核心解析逻辑大概是这样:

pattern = re.compile(r'(?:package|pkg)=([\w\.]+).*?totalTime(?:Used)?=["\']?([^"\'\s>]+)["\']?')

这行正则主要做两件事:

第一,匹配应用包名。 第二,抓取对应的 totalTime 或 totalTimeUsed 字段。

然后再把包名映射成更容易读的中文名,比如:

APP_MAP = {"com.tencent.mm": "微信","com.tencent.mobileqq": "QQ","com.tencent.tmgp.sgame": "王者荣耀","com.miui.home": "系统桌面",}

最后把所有应用按分钟数倒序排一下,就能得到一个比较直观的榜单。

这里需要提醒一句:

这个脚本适合做个人探索和数据观察,不建议直接把结果当作严格审计数据。

原因前面已经说过:系统日志的统计窗口和聚合口径,可能和我们日常理解的“今天”不完全一致。

如果你真的想做更严谨的统计,应该增加两层处理:

一层是时间窗口过滤。 比如明确只保留今天零点到当前时间之间的事件。

另一层是事件级别解析。 不要只看系统聚合后的 totalTimeUsed,而是进一步解析前台进入、前台退出等事件,再自己计算持续时间。

这样得到的结果,才更接近真正意义上的“今日使用时长”。


08 这次实验真正让我在意的,不是 44 天

说实话,44 天这个数字当然夸张。

它有标题效果,也有传播点。

但这次实验真正让我在意的,不是这个异常值本身。

而是我突然意识到:

我们平时对手机使用的感知,是非常不可靠的。

你以为只是刷了一会儿。 系统记下来的是几十次打开、切换、停留。

你以为只是搜个资料。 实际可能是在搜索、跳转、分心、返回桌面之间来回循环。

你以为自己还挺克制。 数据一摊开,才发现注意力早就被拆成了碎片。

手机最厉害的地方,不是让你一次性沉迷 10 个小时。

而是让你每次只多看 3 分钟。

多到你不觉得痛。 少到你不愿意管。 最后累积成一个你不太敢相信的数字。


09 如果你也想试,可以从这三个问题开始

不用一上来就写脚本,也不用立刻搞复杂自动化。

你可以先问自己三个问题:

第一,我手机里排名第一的 App 是什么?

不是你以为的那个,而是真实统计里的那个。

第二,我打开手机最多的理由是什么?

是沟通、娱乐、工作、学习,还是单纯不知道该干什么?

第三,我有没有把“搜索解决方案”当成了“已经开始改变”?

这个问题尤其重要。

因为搜索会给人一种非常隐蔽的满足感:

我查了。 我懂了。 我收藏了。 我下次一定改。

然后下次继续重复。


10 最后:数据不会教育你,但它会照出你

这次小实验,表面上是用 Python 扒了一次小米 15 的底层日志。

实际上更像是给自己的手机做了一次注意力体检。

结果当然不完美。 数据口径有坑,系统统计有误差,--daily 也不一定等于真正的今天。

但它依然有意义。

因为它把那些平时模糊的、碎片化的、不愿承认的使用习惯,变成了一组冷冰冰的数字。

代码不会安慰你。 日志不会给你找借口。 图表也不会替你自律。

它只会把事实摆出来。

至于要不要改变,那是另一回事。

但至少从这一刻开始,我很难再假装自己只是“随便玩一会儿”。


附:完整 Python 脚本

下面是这次用到的脚本版本,主要适配 Android / HyperOS 的 usagestats 输出,用来做个人数据探索。

import subprocessimport reimport jsonimport osimport requestsimport matplotlib.pyplot as pltAPP_MAP = {"com.tencent.mm": "微信","com.tencent.mobileqq": "QQ","com.tencent.tmgp.sgame": "王者荣耀","com.bilibili.app.in": "哔哩哔哩","com.miui.home": "系统桌面","com.android.settings": "系统设置","com.coolapk.market": "酷安","com.github.android": "GitHub","com.android.chrome": "Chrome浏览器"}defload_credentials():    possible_paths = ["api/credentials.json", "credentials.json"]for path in possible_paths:if os.path.exists(path):with open(path, "r", encoding="utf-8") as f:return json.load(f)raise FileNotFoundError("未找到 credentials.json 配置文件")defget_adb_usagestats():try:        check_device = subprocess.run(            ["adb", "devices"],            capture_output=True,            text=True        )        devices = [            line for line in check_device.stdout.strip().split("\n")[1:]if line.strip()        ]ifnot devices or"offline"in devices[0]:            print("未检测到在线设备,请先确认 adb connect 是否成功")returnNone        result = subprocess.run(            ["adb", "shell", "dumpsys", "usagestats", "--daily"],            capture_output=True,            text=True,            encoding="utf-8",            errors="ignore"        )return result.stdoutexcept Exception as e:        print(f"ADB 调用失败:{e}")returnNonedefparse_time_to_minutes(time_str):    time_str = time_str.strip()if":"in time_str:        parts = list(map(int, time_str.split(":")))if len(parts) == 3:return parts[0] * 60 + parts[1] + parts[2] / 60if len(parts) == 2:return parts[0] + parts[1] / 60try:        ms = int(time_str)if ms > 0:return ms / 1000 / 60except ValueError:passreturn0defparse_stats(raw_text):ifnot raw_text:return {}    pattern = re.compile(r'(?:package|pkg)=([\w\.]+).*?totalTime(?:Used)?=["\']?([^"\'\s>]+)["\']?'    )    app_minutes = {}for line in raw_text.split("\n"):        match = pattern.search(line)ifnot match:continue        pkg_name = match.group(1)        time_str = match.group(2)        minutes = parse_time_to_minutes(time_str)if minutes > 0.1:            readable_name = APP_MAP.get(pkg_name, pkg_name.split(".")[-1])            app_minutes[readable_name] = app_minutes.get(readable_name, 0) + minutesreturn dict(        sorted(app_minutes.items(), key=lambda x: x[1], reverse=True)    )defshow_visualization(data):ifnot data:return    top_data = dict(list(data.items())[:10])    apps = list(top_data.keys())    minutes = list(top_data.values())    plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "Arial"]    plt.rcParams["axes.unicode_minus"] = False    plt.figure(figsize=(10, 6))try:        colors = plt.colormaps.get_cmap("Set3")(range(len(apps)))except AttributeError:        colors = plt.cm.get_cmap("Set3")(range(len(apps)))    bars = plt.barh(        apps[::-1],        minutes[::-1],        color=colors[::-1],        edgecolor="gray",        height=0.5    )    plt.xlabel("当日累计使用时间(分钟)", fontsize=11)    plt.title("小米 15 今日应用使用时长 Top 10", fontsize=13, fontweight="bold")for bar in bars:        width = bar.get_width()        plt.text(            width + 0.3,            bar.get_y() + bar.get_height() / 2,f"{width:.1f} min",            va="center",            ha="left",            fontsize=9,            color="#333333"        )    plt.tight_layout()    plt.show()defask_ai(credentials, usage_data):    url = f"{credentials['BASE_URL']}/chat/completions"    headers = {"Authorization": f"Bearer {credentials['API_KEY']}","Content-Type": "application/json"    }    brief_data = dict(list(usage_data.items())[:8])    prompt = ("这是我的手机应用使用时长数据,单位是分钟:\n"f"{json.dumps(brief_data, ensure_ascii=False, indent=2)}\n""请从数据异常、使用习惯、注意力分配三个角度做一份简洁复盘。"    )    payload = {"model": credentials["MODEL"],"messages": [            {"role": "system", "content": "你是一个严谨但不说教的个人数据分析助手。"},            {"role": "user", "content": prompt}        ]    }try:        response = requests.post(url, json=payload, headers=headers)if response.status_code == 200:            print("\n================ AI 数字生活复盘 ================\n")            print(response.json()["choices"][0]["message"]["content"].strip())else:            print(f"AI 接口返回异常:{response.status_code}")except Exception as e:        print(f"调用大模型失败:{e}")if __name__ == "__main__":    config = load_credentials()    raw_log = get_adb_usagestats()    cleaned_data = parse_stats(raw_log)    print(json.dumps(cleaned_data, indent=4, ensure_ascii=False))if cleaned_data:        ask_ai(config, cleaned_data)        show_visualization(cleaned_data)

后台回复「底层追踪」,获取完整无线 ADB 配置步骤和 Python 清洗脚本。

最新文章

随机文章