MoreRSS

site iconRaye Peng修改

一个想成为黑客的文人,ENFJ-A 
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Raye Peng的 RSS 预览

当我在跑步的时候我在想什么

2026-05-01 09:59:23

越来越觉得,身处AI时代,更有必要坚持遵从本心去写作。我特别反感贩卖焦虑的自媒体,创作本就该发自内心,不应该被外界焦虑裹挟,被迫生出“我必须要做点什么”的念头。

从2025年11月真正开始跑步以来,我收获了太多,也慢慢笃定:自己应该好好记录、好好分享,把这段心路历程留存下来。

其实我一直都很喜欢跑步。学生时代常见的乒乓球、羽毛球、篮球这类球类运动,我都玩不好,所以只剩下跑步了。但如果你在初中体育课一个人独自绕操场跑步,会被你的同学或者朋友认为是一个很奇怪的事情,再加上我又是一个很在意他人评价的人,于是就不敢一个人大大方方地去跑步

真正和跑步结缘,并且想坚持跑步的想法,诞生在高中。当时我在实验班,寒暑假需要补课,但早上不用早读。于是我习惯每天七点起床,先去操场跑上一两圈,就这样坚持了整整一个月.

而真正重新拾起、正式开启跑步训练,是去年11月。因为公司每年都有司庆跑步活动,第一年我不知道有这个活动,知道后却发现名额满了;第二年报了名,又因为其他事情遗憾错过;直到去年,才终于成功参加了。

当时我和另外两位同事一起报名27公里团队跑,我和我的朋友hh负责跑10公里,另一位女生跑7公里。为了磨合状态,我跟hh约好早起拉练10公里。结果两个人一开始只顾着疯狂提速,配速直接冲到5分50多秒,跑完5公里后,后半程彻底跑崩。

好不容易熬完10公里回到起点,我们俩都扶着墙,虚脱到说不出话,整个人疲惫到恍惚。也正是从这次拉练开始,我自觉才算真正踏入跑步的行列。

其实之前我也试过跑步,但10公里对我而言,一直是一道遥不可及的坎。我也曾在跑步机上坚持过每天5公里,足足跑了一个月,但是跑步机实在是太过枯燥,很难投入进去,让我对跑步产生发自内心的喜欢。

后来尝试过第一次挑战10公里,是舜子哥带着我。那一次跑到七八公里时跑崩了。但这一次的拉练证明我是可以做到跑完10公里的,虽然疲惫,但是让我认识到,原来这件事,我真的可以做到。

我一直记得陈铭在辩论赛里说过一段话,非常有意思,他说:洞穴是黑暗的,但只要有光打进去,黑暗就不再是它的宿命;沙漠是隔绝的,但只要有人穿越过去,隔绝就不再是它的宿命。

曾经10公里于我,就是触不可及的目标;可当我真正第一次完成,才发觉到,原来也没那么难,我真的是可以完成的。

那次司庆跑步,我顺利跑完10公里,最终计时一小时零六分钟。放在当时还算勉强完成,但如今回头看已经很慢了——我的10公里已经能跑进52分钟,这要是放在几个月前,我完全无法想象...

从去年11月起,我决心开始系统训练,一方面想提升跑步水平、强化心肺,另一方面也是弥补自身短板:我从小心肺能力偏弱,肺活量一直偏低,Apple Watch测出的有氧水平也长期低于平均值,心里一直很介意。

说不清当时是哪来的一股劲头,就想认真试一试,就这样开启了跑步训练。如今再回头,反倒记不清当初出发的具体心境,甚至说不清最初的初心到底是什么。 或许正因为没有预设的初心,反而少了“不忘初心、方得始终”的束缚。别人要怕走得太远、忘了为什么出发,而我本就不知为何出发,反倒能纯粹享受奔跑本身。

翻看过往的运动记录,后续我又参加了好几场跑步活动,也慢慢解锁了深圳的跑步路线,才发现自己居住的地方环境非常棒,沿着深圳湾就能一路畅跑。

解锁这条路线后,我越跑越投入。也就在某个瞬间,心底突然冒出一个念头:我是不是可以试着挑战马拉松?

我一直有种执念:想要真正融入一项运动、一个圈子,就必须全身心投入进去。 就像当时用微信读书,我觉得只有主动发评论、写见解,和书友交流互动,而不只是单纯用来看书,才算深度参与,才算真正用好这款产品。

跑步也是同理,我开始想,我能不能挑战下跑者的最高殿堂——马拉松。

还记得那天我兴奋得彻夜难眠,凌晨四五点就醒了,四点四十九分从家里出门,只想单纯试一试,看自己能不能跑完一场半马。

没想到真的做到了,21公里的半马,最终用时2小时22分钟。那一刻满心震撼,从来没想过自己能跑完这样长度的距离。

既然能跑完,那为什么不正式报名参赛呢? 我开始研究马拉松报名规则,才发现并没有自己想象中那么复杂,也没有遥不可及的高门槛。比如第一赛道app,只需完成15公里线上测试就能获得报名资格,于是我在12月报了广州、梅州两场半马。

广州遗憾没能中签,于是去了梅州半马,这也是我人生第一场线下半马。当时的目标是破2小时,但还是准备不足,最终时间定格在2小时4分钟

虽然遗憾,但也刷新了自己的最快配速——平均5分55秒跑完半马。放在以前,我日常慢跑也只是6分30、6分40的配速,根本不敢想象能跑出这个节奏。

跑完这场线下半马,像为我的人生推开了一扇全新的大门,听起来有点俗套,却是我最真实的感受。

然后就一发不可收拾,报了一堆马拉松比赛,最终顺利报上两场:浙江宁波奉化马拉松、青岛马拉松。

上半年还参加了深圳某公园的公益马拉松,赛道爬升182米,跑得格外煎熬,但也是我冬训最好的检验。没想到我成功实现半马破2,时间定格在1小时56分钟。

带着这份底气来到奉化马拉松,我跑出了个人半马最佳成绩:平均配速5分17秒,完赛1小时51分钟,顺利达成大众二级马拉松运动员的目标,原来自己真的可以做到!

回看这一路成长,配速从最初的6分40、甚至7分开外,到如今能以160左右的心率,稳住5分30秒配速,我已经历许多。

再想起当初10公里要跑1小时6分钟的自己,真的忍不住感慨:短短几个月,人真的可以完成质的飞跃。

有氧训练从来不会骗人,坚持练就一定会有进步,一旦停下,状态就会快速回落。

那么我跑步的时候,我都在想些什么呢?

最开始在跑步机上慢跑,满脑子只盼着这半小时赶紧结束,盯着计时器一分一秒煎熬,度秒如年。我没办法像别人那样追剧刷视频转移注意力,只能硬熬,非常痛苦。

但置身户外奔跑时,思绪反倒变得很松弛。

会胡思乱想,也会留意身边跑步的人,看别人的跑步体态(运动的小姐姐腿都很好看);会观察别人身上的穿搭logo、运动装备。

跑在深圳湾的时候,常会看见栏杆上立着的大白鹭,一动不动宛如雕塑,走近才发现是真的大白鹭,非常有意思。看周围路边的草木,有时也会去想吴王钱镠写下“陌上坏开,可缓缓归矣”是一种什么样的情景。跑步的沿途总会有各种各样的细碎美好,足以填满奔跑的时光。

我记得我刚开始训练不就,有一次脑海中突然冒出一个想法,我已经跑了五公里了,离这次的目标就只剩5公里了,我突然觉得不知足了!我想跑更多,我第一次冒出这种想法的时候都惊呆了。

原来身体也能给出反馈,能承受的距离越来越长,心底的渴望也越来越多,就像《阿甘正传》里那句简单的台词:Run,Forest,Run,只管义无反顾,向前奔跑就好。

尤其偏爱夜晚在深圳湾蛇口海边跑步。 沿着海浪边前行,听潮水涨落、浪花拍打礁石,恰好应和那句“海风吹过澎湖湾,白浪逐沙滩”。远处城市灯火辉煌,霓虹光影倒映在海面,海风拂面,那一刻多巴胺与肾上腺素开始上涌,只想一直往前奔跑,贪恋此刻的晚风、海浪与夜色,我有时候觉得,哪怕我就在这一刻死去,也是值得的。

我的2025

2026-01-01 01:48:34

12月居然没有更新一篇文章,看来真的是太忙了。

12月我做了三次分享——草坪《重读鲁迅》、拜年祭《那不勒斯四部曲》以及部门年会上的分享,月中还有一次给老板的下半年汇报,耗去我不少精力。

但是不是真的没有时间来写周报和技术文章呢?我扪心自问,工作日晚上回家打一小时三国杀+看一小时书就睡了,写作阅读游戏总是不可兼得了。

原定的12月31日写一篇年度总结,但因为拖延症的关系又拖到了今天,已经是26年的1月1日了,不管怎么说这两个日子都有特殊的意义。

前段时间看到和菜头的一篇文章,他说你们的年度总结,本质上就是对12月的总结,因为前几个月发生的事情根本就不记得了,不得不说一语中的了。

于是我尝试去列了一下每个月所发生的一些重要事情,果真如此,年初的几个月还能记起一些,但是年中的7、8、9月已经是全都忘记了,不得不借助相册和为数不多pyq来回顾。

那么对于我26年的flag来说,唯一重要的一件事,就是尝试把每天发生的事情+自己的一些感受记录到flomo中,只要把这一件事做好就行了。

2025年我做了哪些改变

最大的改变就是健身和跑步,但是难以想象的是健身是我从9月底开始的,跑步则是11月开始的。“做一件事,最好是十年前,其次是现在”

健身和跑步对人的改善真的非常大,我曾经非常容易失眠,但现在基本倒头就睡,工作精力开始重新变得充沛,身体形态的改善也是肉眼可见(尽管还没有腹肌,但相信坚持下去总会有的)

每次的健身都让我充满了挑战欲,当我高位下拉的时候,最初的35kg还比较吃力,到现在45kg、52kg,你能持续感受到身体在不断的进步。

肌肉的恢复速度也不断加快,以往健身一次能疼一周,但现在一天后就恢复了,此刻对于痛苦已经不是惧怕,而是渴望。

跑步则是更让我欣喜的,我也是今年才懂得户外跑相比室内跑步机,对膝盖更好的道理。户外跑是一件非常上瘾的事情,尤其在深圳这么好的条件下,当我早上5点半沿着大沙河一路跑到日出剧场,此时刚好6点半,冬日的太阳逐渐升起,运气好的话还能看到火烧云,那种感觉是无与伦比的,恐怕也只有“悠然见南山”能形容了。

今年第一次参加了半马,来到梅州,早上7点半的梅州温度在6、7度左右,热身后却感觉不到寒冷,发令枪响起的一刻,跟随者大部队的脚步便开始了。

果然寒冷的天气,对于肌肉的酸痛有抑制作用,第一公里的配速就来到了5分28秒,心率也直接跳到160吗,让我有点害怕。但后续的也基本维持在5分50秒左右的配速,从来没想到过自己能跑这么快,毕竟以前训练的时候,我都是按照6分20左右的配速来跑。

我的心肺一直不太行,从小肺活量就很低,跑步也没怎么练上去。21公里最后的几公里已经让我几乎

周报42 十公里跑步、鬼灭、敦煌展

2025-11-20 13:36:59

距离上一次的疯狂更新已经过了两周,中间的一个周末也曾想要维持更新频率,但人又开始变得有点懒惰,不过想要停下来写写的心总是在跳动着的,于是就开始写吧。

如果你想留住时间,那就看书吧,因为你以为你看了好久,实际上才不过几分钟

最近突然有了一个感悟,想要留住时间最快的办法就是阅读,阅读的时候时间流逝似乎非常慢。最近又在沉迷于《海伯利安》这本书,虽然去年曾经读过前两册,但我自认为并没有读懂,于是在微信读书上随便打开一页,刚好看到侦探拉米亚的故事,于是就一直往下看了。

海伯利安并不是一个硬核的科幻故事,如果喜欢硬核的科幻,我推荐《拯救计划》,但《海伯利安》自有它独特的魅力——它仿佛把一个从欧洲中世纪到现在的故事,放在太空的画卷下徐徐展开 。

这也就是为什么,当你读到基督教、读到神父的时候,你会有疑惑,怎么科幻的故事里还残留着上个世纪的故事?超光速飞船的时代还会有人信奉基督教吗?星际航行的时代还有活人献祭的仪式吗?正是这看似不寻常的吊诡之处,吸引了我不断地重温。

等我有机会再写点这本书的读后感吧,不过我其实也不知道写啥,都是一些零零碎碎的想法。

跑步

和两个公司的朋友(非同事)报名了公司的司庆跑,三人小组,跑步总和27公里即可,我和另一个男生十公里,女生跑7公里。

我以往都是在公司的跑步机跑,十公里也不是没跑过,对于我来说,前面的3公里时一个小坎,随着逐渐进入状态,心率和呼吸开始加速。紧接着就是五公里,那是我平常跑的最远距离,跑完五公里之后,就只有靠毅力支撑下去了。

在跑前5公里的过程中,你会因为每跑完1公里就喜笑颜开,然而5公里之后,每增加的一公里,都要克服极大的身体和心理困难。

和朋友约好提前一天早上先在户外跑10公里熟悉路线,大早上的7点我们就碰面了,然后开始跑。

不得不说长跑控制配速是非常重要的,在空腹的情况下,两个人前5公里的配速基本都在6分钟以内,导致我们后半程人都虚脱了,最后跑完十公里,感觉人都站不稳了(所以跑步前吃点东西还是有必要的)

不过这也是我开始逐渐喜欢上户外跑步了,喜欢这种在城市间用脚步丈量空间的感觉,喜欢那在空旷的街道上撒开腿丫子飞奔,仿佛又回到了乡下,回到了自己的童年时候

哦对,训练跑当天还是我生日,也算是为自己庆生了吧!

0bca9e68f62e36780649de482c590fc7

鬼灭

舜子哥邀请我一起去看鬼灭之刃的路演,虽然我不是纯粹的鬼灭迷,但是也基本追完了系列的动漫,非常喜欢里面的角色。

电影其实非常棒,制作上似乎也找不出什么瑕疵,蝴蝶忍开场就没了,感觉纯粹是剧情杀。善逸直接单杀上弦六,节奏稍微过快了点。

最后就是打上弦三猗窝座了,看得我真是,为什么还不死!给我大哥报仇啊!

不过,上弦三的设定还是非常感人的,不过这个故事情节略显老套,因为大部分人看到开头就能猜到结局,但还是不影响3哥的形象,这种顶天立地的男子汉,誓死守护自己所爱的人。

image-20251120215908496

image-20251120215921463

image-20251120215929517

敦煌展

南山博物馆的大成敦煌展非常值得,第一次是在周六晚上去的,走马观花看了后印象非常深刻,刚好看展群里有人要卖早鸟票的,于是就买了一张又去看了一遍

拍了一些有意思的照片:

巧摄使用与平安金融中心月升

2025-11-05 13:48:35

巧摄

今年第一次接触到巧摄这个软件,契机已经忘了,只知道摄影圈的朋友都有用到这个软件,于是下载后付费,巧摄的基础功能是买断制的,只有机位探索与3d模型需要订阅,我直接选择all in

于是有朋友问了,巧摄究竟能做什么?我这里以最简单的功能来告诉你,巧摄是多么的牛逼无敌

悬月悬日计算

何谓悬月和悬日呢?即我们选择一个地标建筑物,如深圳的平安金融中心,春笋,这些地标建筑都是鹤立鸡群的,如果太阳或者月亮,刚好和它尖端重叠,此时就称之为悬

如果我们没有软件的计算,那就只能依赖经验判断,太阳可能还好说,但是月亮的升起和落下是一天一个位置的,这时候巧摄的第一个功能就出现了,它能帮我们计算出太阳或者月亮什么时候与建筑物重叠

如下是一个巧摄预测的经典截图,在这个截图中,可以看到有几个关键要素:

  • 目标高度,即平安金融中心的高度,这个是非常关键的,巧摄是通过3D建模来模拟该建筑物的
  • 月亮位置,你可以制定月亮出现在建筑物的什么位置,巧摄会按照你的要求进行搜索
  • 高度角、方位角,这是指摄影机位的朝向
  • 焦距:非常关键,可以看到下图是400mm,巧摄可以模拟出镜头焦距拍摄效果,如果你恰好有400mm镜头,你就可以百分之百地拍摄出如下画面,如果你手头没有这么长的焦距,那可以选择你拥有的焦距,巧摄也可以模拟你相机中取景框拍摄到的效果
  • 中间就是取景框
  • 下方则是时间,巧摄可以精确的计算出具体时间
  • 当然左侧还有一些小图标,如显示更多地形、云层的,一般用的不是很多

Screenshot_2025_11_05_11_41_57_85_67defcee5f9754ce81d50c9d495861ba

巧摄使用思路

拍摄的时候无非是机位和被摄物体,被摄物体很容易定位,但是机位不太好确定,这时候有几种办法:

  • 已知机位找日期:经典机位,即已经很明确的,如果你订阅了巧摄的机位探索功能,是可以看到别人共享出来的机位,这时候确定机位后,就可以直接搜索日期,什么时候会出现悬日悬月
  • 已知日期找机位:如果你想知道今天或者明天,哪里可以拍摄,此时可以通过巧摄的对齐机位功能,巧摄会自动计算出一系列的机位,但这些机位只是理论上可行,需要实地考察

巧摄学习

建议按照巧摄的官方入门教程学习,耐心一路看过去即可,对于摄影的提升非常有用

<iframe width="100%" height="468" src="//player.bilibili.com/player.html?bvid=BV1oG41147Rc&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>

11.5 平安金融中心月升

到达机位

很早之前就已经通过计算得知深圳湾公园可以拍摄到平安金融中心的月升,并且还刚好是满月,到达现场后发现已经有好多人到达了

和摄影群的朋友们也面基了,好多人都是第一次见😊

月升照片分享

天气还是不够通透,就这样吧哈哈,400焦距尽力了

IMG_20251105_194107

mmexport1762340508099

oppo findx8u 使用体验分享

2025-11-03 13:54:53

第一次写测评类的文章,仅分享个人主观使用体验

起因

我的iPhone13pro,已经服役3年多了,由于我作死般地升级了ios16,导致使用上越来越卡顿,微信月球界面一天要见无数次,于是多方对比下,9月份换了oppo findx8u

从iPhone迁移到安卓,最开始是犹豫的,因为有好些个ios付费app,只能在iPhone使用,比如足迹记录软件Rond,还有索尼的导出软件,因为已和相机做了关联,如果换新手机后,导出相机未免有些麻烦,总结下犹豫的理由吧:

  • 付费app,iPhone专属
  • 相机app,手机与相机做了绑定
  • 部分app 安卓功能缺失,如dj mimo,只有iPhone版支持live三拼

当然安卓也不是没有优势,巧摄软件的最新天气功能,就只能在安卓上使用,这也是促使我换安卓的原因

所以我的解决方案还是双持,反正iPhone体积小巧,重量可接受,主力机还是用安卓,特定情况下才会使用iPhone,目前两个多月使用下来,自我感觉良好。

为什么是oppo findx8u

曾经的我认为oppo、vivo这类手机就是智商税,毕竟相同价位下的小米配置参数完全碾压吊打ov,但今时不同往日,ov两家在摄影上的进步是有目共睹的,反而上半年的小米15ultra,我体验下来,觉得拍摄得很一般,莱卡的色彩并没有让我很惊艳,vivo x200ultra曾经是我非常想换的手机,毕竟底是最大的,但综合对比下来:

  • 小米,体验过朋友的手机,拍出来的照片也还不错,但不知道为啥不太想用小米
  • vivo,饱和度战士😂,但是照片很讨喜
  • oppo,购买之前我认为是色彩还原度比较好的,并且和哈苏联名让我很是心动(所有摄影人的终极梦想都是想买一台哈苏吧)

当然oppo findx8u还有很多其他因素,比如操作习惯基本和苹果保持一致,能抄的基本都抄了,这就让我基本不用改平常的操作习惯,无缝迁移,完全可以认为是优化过后的ios

唯一吐槽的就是购买后发现oppo的开机键、音量控制键在同一侧,让我特别不适应

购买

由于是上半年发布的手机,9月左右入手,虽然比不过双十一,但价格也比较稳定了,拼多多百步补贴挺香的,但我这次尝试了下咸鱼,找了一个二手卖家,当天晚上就送过来了,感觉上应该是一个职业卖家,稍微检查了下没有任何瑕疵,目前使用下来感觉也还不错,看来走咸鱼面交买手机也是一个不错的选择,价格也更优惠

哈苏色彩

大师模式自动

哈苏色彩是我最喜欢oppo的一点,开启大师模式后,色彩的还原度让我觉得非常惊艳

大师模式的自动是我最开始比较常用的,最开始给我的感觉是照片的暗部会更暗一点, 照片有一定的对比度但是又不过分

e60ec219b3819e7293afd599c22be59d

aafbc33b1884cd2ce90bf875b584f00a

大师模式手动

如果你了解曝光三要素,大师模式手动可能是最常用的,在这个模式下可以调三个大的参数:

  • 曝光三要素:主要是调整快门和iso,比如拍日出的时候iso可以调到100,快门可以选择自动,也可以手动调节看效果(iso一般往低了调,毕竟底低iso拍出来的照片更纯净)

  • 柔光、影调、饱和度、色温、青品这几个参数

    • 柔光,这个主要是增加朦胧的效果,人像摄影比较常用
    • 影调,控制照片整体明暗对比关系,调高让照片光影感更强,调低让照片更平淡
    • 饱和度,主要控制色彩的鲜艳程度,建议别太高,不然就成为饱和度战士了
    • 色温,控制冷暖
    • 青品,调高增加品红色,画面偏暖,调低增加青绿色,画面偏森系
  • 滤镜:这个太多了,我一般默认标准,但是复古和童话也挺不错的,具体可以参照小红书上大佬们总结出来的调色参数

放一些大师模式拍出来照片,更多可以查看 我的照片博客

拍视频

目前博客似乎还不支持嵌入视频,但我要说的是,find x8u 拍烟花视频真的是一绝!等我后续把视频传上来

protobuf编码规则分析

2025-11-03 13:35:10

分析protobuf二进制编码

比如一个最简单的pb:

syntax = "proto3";

message ResourceModify {
    int32 type = 1;
    int32 amount = 2;
}

我们假设数据是:

{
  "type": 1,
  "amount": 26526
}

如何得到其对应的二进制编码呢?

tag + value

可以简单的把pb的二进制编码看作是 tag + value ,首先看tag怎么计算

公式:

tag = (field_number << 3) | wire_type

field_num 即是序号, 左移三位,然后和wire_type计算或

这个wire_type即代表了该字段的类型,定义如下

image-20251103210638320

因此,第一个字段 type = 1 对应的key就是:

1 << 3 | 0 = 0x8

接着再拼接上它的value,最后就是0x0801

msb +小端序编码

接着看第二字段

"amount": 26526

计算tag:2 <<3 |0 = 0x10

对应的value,则是:9ecf01 ,你是不是很好奇怎么来的,为什么不直接是26526的十六进制编码,这是因为pb采用了msb +小端序的规则

msb即最高位置作为符号位信息

我们将其转为对应的二进制,第一位为msb符号位直接去掉,然后按照小端序来拼接

9e = 10011110
cf = 11001111
01 = 00000001

拼接过程, 先取01 -> 00000001 (注意MSB,第一位直接丢掉
然后取 cf -> 1001111(同样第一位被丢掉了)
再取 9e -> 0011110 
拼接起来就成了  1 1001111 0011110
用py代码转一下就有了
int('1 1001111 0011110'.replace(' ', ''), 2) # 26526

vibe coding一下

def int_to_varint(n):
    """
    将一个大于 255 的整数转换为 Protobuf varint 编码。

    :param n: 需要转换的整数。
    :return: varint 编码后的字节数组(以 list 的形式返回)。
    """
    result = []
    while n > 0x7F:  # 0x7F = 127
        # 取低 7 位,并将高位设为 1 表示后续还有字节
        result.append((n & 0x7F) | 0x80)
        n >>= 7  # 将 n 右移 7 位
    result.append(n & 0x7F)  # 最后一个字节,最高位为 0
    return result

# 示例:将一
# 个大于 255 的数转换为 varint 编码
number = 26581
varint_encoded = int_to_varint(number)
print(f"Varint encoding for {number}: {[hex(b) for b in varint_encoded]}")

嵌套消息结构

如果遇到了这段编码

0A 06 08 01 10 9E CF 01

可以看到就是多了前两个字节: 0A 06

其中 0A 是第一个字段,并且代表后面是一个字符串或者嵌套消息,06代表长度,接着后面刚好6个字节, 08 01 10 9E CF 01,其实就回到前面了,所以其消息结构应该是:

{
  "1": {
    "1": 1,
    "2": 26526
  }
}

一键解码pb二进制

可以直接调用 protoc --decode_raw 来解码,不过pb编码后会丢失字段名,所以解码后也是看不到字段名的

❯ echo "0A 06 08 01 10 9E CF 01" | xxd -r -p | protoc --decode_raw

1 {
  1: 1
  2: 26526
}

或者这个例子:

❯ echo "0a1074645f6f70656e65775f64616d656e67120864616d656e6777781a08313139373230363922a0017b226f70656e6964223a226f33396867347a5155484b47376f414b57316655384b39425674426f222c22746f6b656e223a226a6434747165564664484b4c65366b31576d4a346550427875565137564e6e724c6c55413847442f6a5a2b4e3636535464655039534c37556936666a6176676a67714b735378646d61396d62644a6d2f6634615352517646415a414f50545578414c6b49664961513776513d227d2a00320864616d656e6777783a140a0012001a0022002a0032003a0631342e322e304202636e4804" | xxd -r -p  | protoc --decode_raw
1: "td_openew_dameng"
2: "damengwx"
3: "11972069"
4: "{\"openid\":\"o39hg4zQUHKG7oAKW1fU8K9BVtBo\",\"token\":\"jd4tqeVFdHKLe6k1WmJ4ePBxuVQ7VNnrLlUA8GD/jZ+N66STdeP9SL7Ui6fjavgjgqKsSxdma9mbdJm/f4aSRQvFAZAOPTUxALkIfIaQ7vQ=\"}"
5: ""
6: "damengwx"
7 {
  1: ""
  2: ""
  3: ""
  4: ""
  5: ""
  6: ""
  7: "14.2.0"
}
8: "cn"
9: 4

一键编码pb二进制

protoc 提供了解码,但是并没有提供编码,这个让我百思不得其解,实战中经常会有需要解码后修改数据再编码回去的,这个过程是完全可逆的,不会损失任何信息

于是干脆自己vibe coding了一个,如下,非常方便

'''
python pb_test.py # 解码,默认输出到output.txt
python pb_test.py encode # 编码,默认读取output.txt
'''

from google.protobuf.internal import decoder, encoder



# 在这里粘贴base64编码的pb二进制数据
a = '''CA8SyQEIARABGAAgACgAMAA4AEAASABQAFgAYABoAHC+T3gAgAEAiAEAkAEAmAEAoAEAqAEDsAEAuAEAwgEIAQg2ONAP0Q/IAQDQAdvctKeiM9gBAOABAOgBAPABAPgBAYACAIgC0A+SAgCYAgKgAgCoAgywAgC4AgDAAgDIAgDYAgDgAgDoAgDyAgoIBhAAGAAgASgA+AIAgAMAigMICJvbgMgGIAGSAxUKCAiAi/nHBhAACgkIgK7+xwYQryuaAwYoADABOGSiAwCoAwAaCAix2wYQABgAGggIstsGEAAYABoICLPbBhAAGAAaCAi02wYQABgAIgYIwZoMEAAiCgihjQYQgNKTrQMiCgiijQYQgNKTrQMqvQcKIwgBEAEYASABKgIBBTC7gMHGojM4AEAASABQAGAAaABwAHgBEgMI8AcaAghlKgwIoY0GEGUYACAAKAA6DQiRThBlGAEgACgAMAA6DQiSThBkGAAgACgAMAM6DQiTThBkGAAgACgAMANCDAihnAEQZBgAIAAoAEIMCKKcARBkGAAgACgAQgwIo5wBEGQYACAAKABCDAiknAEQZBgAIAAoAEIMCKWcARBkGAAgACgAQgwIppwBEGQYACAAKABCDAipnAEQZRgBIAAoAEoMCLHqARBlGAEgACgAUg4IwbgCEGQYACAAKAAwBFoICNGGAxBlGABiCAjh1AMQZRgBaggI8aIEEGUYAXIMCIHxBBBlGAEgACgAegoIkb8FEGQYACAAggEECAEQAIIBBAgCEACCAQQIAxAAggEECAQQAIIBBAgFEACCAQQIBhAAggEECAcQAIIBBAgIEACCAQQICRAAigEMCLHbBhBkGAAgACgAigEMCLLbBhBkGAAgACgAigEMCLPbBhBkGAAgACgAkgEMCMGpBxBkGAAgACgAmgEMCOHFCBBkGAAgACgAmgEMCOLFCBBkGAAgACgAmgEMCOPFCBBkGAAgACgAmgEMCOTFCBBkGAAgACgAmgEMCOXFCBBkGAAgACgAmgEMCObFCBBkGAAgACgAmgEMCOfFCBBkGAAgACgAmgEMCOjFCBBkGAAgACgAmgEMCOnFCBBkGAAgACgAqgEMCIHiCRBkGAAgACgAqgEMCILiCRBkGAAgACgAqgEMCIPiCRBkGAAgACgAsgEOCJOwChBkGAAgACgAMAKyAQ4IkrAKEGQYACAAKAAwArIBDgiRsAoQZBgAIAAoADACugEICKH+ChBlGADCAQwIscwLEGQYACAAKADCAQwIsswLEGQYACAAKADCAQwIs8wLEGQYACAAKADCAQwItMwLEGQYACAAKADKAQgIwZoMEGQYANIBDAjU6AwQZBgAIAAoANIBDAjR6AwQZBgAIAAoANIBDAjS6AwQZBgAIAAoANIBDAjT6AwQZBgAIAAoAOIBDAjxhA4QZBgAIAAoAOIBDAjyhA4QZBgAIAAoAOIBDAjzhA4QZBgAIAAoAOIBDAj0hA4QZBgAIAAoAOoBDgiB0w4QZBgAIAAoADAD6gEOCILTDhBkGAAgACgAMAPyAQwI4ZgXEGQYACAAKAD6AQwIgbUYEGQYACAAKACCAgwI8eYXEGQYACAAKACKAh4IARACGAAgACgAMgA6AEIASABQAFgAYABoAHAAeAEyADoKCAEQy+qAyAYqAEIA'''

# base64解码
data = base64.b64decode(a)

def is_valid_utf8(data):
    """检查数据是否为有效的UTF-8字符串"""
    try:
        data.decode('utf-8')
        return True
    except:
        return False

def parse_message(data, depth=0):
    """
    完整解析 protobuf message,支持所有wire types
    返回 list of (field_number, value),保留重复字段
    """
    pos = 0
    result = []
    while pos < len(data):
        try:
            key, pos = decoder._DecodeVarint(data, pos)
        except:
            break
            
        field_number = key >> 3
        wire_type = key & 0x7

        if wire_type == 0:  # varint
            value, pos = decoder._DecodeVarint(data, pos)
            result.append((field_number, value))
            
        elif wire_type == 1:  # 64-bit (fixed64, double)
            if pos + 8 > len(data):
                break
            value = struct.unpack('<Q', data[pos:pos+8])[0]
            result.append((field_number, value))
            pos += 8
            
        elif wire_type == 2:  # length-delimited (字符串/bytes/嵌套消息)
            length, pos = decoder._DecodeVarint(data, pos)
            if pos + length > len(data):
                break
            value = data[pos:pos+length]
            
            # 尝试解析为嵌套消息
            try:
                nested = parse_message(value, depth+1)
                if nested and len(nested) > 0:
                    # 如果成功解析出字段,认为是嵌套消息
                    result.append((field_number, nested))
                else:
                    # 否则作为bytes/string
                    result.append((field_number, value))
            except:
                # 解析失败,作为bytes/string
                result.append((field_number, value))
            pos += length
            
        elif wire_type == 5:  # 32-bit (fixed32, float)
            if pos + 4 > len(data):
                break
            value = struct.unpack('<I', data[pos:pos+4])[0]
            result.append((field_number, value))
            pos += 4
            
        else:
            # 不支持的wire type,跳过
            break
            
    return result

def format_bytes(b):
    """格式化bytes为带引号和转义的字符串"""
    result = []
    for byte in b:
        if 32 <= byte <= 126 and byte != ord('\\') and byte != ord('"'):
            # 可打印ASCII字符
            result.append(chr(byte))
        else:
            # 转义表示
            if byte < 8:
                result.append(f'\\{byte:03o}')
            else:
                result.append(f'\\{byte:03o}')
    return '"' + ''.join(result) + '"'

def format_value(value, indent=0):
    """格式化单个值"""
    if isinstance(value, list):
        # 嵌套消息
        lines = []
        lines.append(' ' * indent + '{')
        for field_num, field_val in value:
            field_lines = format_field(field_num, field_val, indent + 2)
            lines.extend(field_lines)
        lines.append(' ' * indent + '}')
        return lines
    elif isinstance(value, bytes):
        # bytes类型,判断是否为空字符串或二进制数据
        if len(value) == 0:
            return [' ' * indent + '""']
        # 检查是否全是可打印字符
        try:
            decoded = value.decode('utf-8')
            # 如果只包含数字字母和一些常见字符,直接显示
            if all(32 <= b <= 126 for b in value):
                return [' ' * indent + f'"{decoded}"']
        except:
            pass
        # 否则使用转义格式
        return [' ' * indent + format_bytes(value)]
    else:
        # 整数
        return [' ' * indent + str(value)]

def format_field(field_num, value, indent=0):
    """格式化字段"""
    lines = []
    if isinstance(value, list):
        # 嵌套消息
        lines.append(' ' * indent + f'{field_num} {{')
        for sub_field_num, sub_value in value:
            sub_lines = format_field(sub_field_num, sub_value, indent + 2)
            lines.extend(sub_lines)
        lines.append(' ' * indent + '}')
    elif isinstance(value, bytes):
        # bytes类型
        if len(value) == 0:
            lines.append(' ' * indent + f'{field_num}: ""')
        else:
            # 尝试解码
            try:
                decoded = value.decode('utf-8')
                if all(32 <= b <= 126 for b in value):
                    lines.append(' ' * indent + f'{field_num}: "{decoded}"')
                else:
                    lines.append(' ' * indent + f'{field_num}: {format_bytes(value)}')
            except:
                lines.append(' ' * indent + f'{field_num}: {format_bytes(value)}')
    else:
        # 整数
        lines.append(' ' * indent + f'{field_num}: {value}')
    return lines

def format_message(parsed_list):
    """格式化整个消息为res1.txt的格式"""
    lines = []
    for field_num, value in parsed_list:
        field_lines = format_field(field_num, value, 0)
        lines.extend(field_lines)
    return '\n'.join(lines)

def parse_escaped_string(s):
    """解析带转义的字符串,返回bytes"""
    if not s.startswith('"') or not s.endswith('"'):
        raise ValueError(f"字符串必须以引号包围: {s}")
    
    s = s[1:-1]  # 去掉首尾引号
    result = []
    i = 0
    while i < len(s):
        if s[i] == '\\' and i + 3 < len(s):
            # 八进制转义 \xxx
            try:
                octal = s[i+1:i+4]
                byte_val = int(octal, 8)
                result.append(byte_val)
                i += 4
            except:
                result.append(ord(s[i]))
                i += 1
        else:
            result.append(ord(s[i]))
            i += 1
    return bytes(result)

def parse_text_format(lines):
    """
    从文本格式解析回数据结构
    返回 list of (field_number, value)
    """
    result = []
    i = 0
    
    while i < len(lines):
        line = lines[i].rstrip()
        if not line or line.isspace():
            i += 1
            continue
        
        # 计算缩进
        indent = len(line) - len(line.lstrip())
        line = line.strip()
        
        if line == '}':
            # 消息结束
            break
        elif ':' in line and not line.endswith('{'):
            # 字段: 值
            field_str, value_str = line.split(':', 1)
            field_num = int(field_str.strip())
            value_str = value_str.strip()
            
            if value_str.startswith('"'):
                # 字符串/bytes值
                value = parse_escaped_string(value_str)
            else:
                # 整数值
                value = int(value_str)
            
            result.append((field_num, value))
            i += 1
        elif line.endswith('{'):
            # 嵌套消息开始
            field_str = line[:-1].strip()
            field_num = int(field_str)
            
            # 找到匹配的结束括号
            bracket_count = 1
            j = i + 1
            start_line = j
            while j < len(lines) and bracket_count > 0:
                sub_line = lines[j].strip()
                if sub_line.endswith('{'):
                    bracket_count += 1
                elif sub_line == '}':
                    bracket_count -= 1
                j += 1
            
            # 递归解析嵌套消息
            nested_lines = lines[start_line:j-1]
            nested_value = parse_text_format(nested_lines)
            result.append((field_num, nested_value))
            i = j
        else:
            i += 1
    
    return result

def encode_message(parsed_list):
    """
    将解析后的list编码回protobuf二进制
    """
    parts = []
    for field_number, value in parsed_list:
        if isinstance(value, int):  # varint
            key = (field_number << 3) | 0
            parts.append(encoder._VarintBytes(key))
            parts.append(encoder._VarintBytes(value))
        elif isinstance(value, list):  # 嵌套消息
            key = (field_number << 3) | 2
            nested_bytes = encode_message(value)
            parts.append(encoder._VarintBytes(key))
            parts.append(encoder._VarintBytes(len(nested_bytes)))
            parts.append(nested_bytes)
        elif isinstance(value, bytes):
            key = (field_number << 3) | 2
            parts.append(encoder._VarintBytes(key))
            parts.append(encoder._VarintBytes(len(value)))
            parts.append(value)
        else:
            raise TypeError(f"不支持的类型: {type(value)}")
    return b''.join(parts)

if __name__ == "__main__":
    import sys
    
    if len(sys.argv) > 1 and sys.argv[1] == 'encode':
        # 反向模式:从文本文件编码回base64
        input_file = sys.argv[2] if len(sys.argv) > 2 else 'output.txt'
        
        print(f"正在从 {input_file} 读取并编码...")
        with open(input_file, 'r', encoding='utf-8') as f:
            lines = f.readlines()
        
        # 解析文本格式
        parsed = parse_text_format(lines)
        
        # 编码回二进制
        encoded_data = encode_message(parsed)
        
        # base64编码
        b64_result = base64.b64encode(encoded_data).decode('ascii')
        
        print("\n编码结果(base64):")
        print(b64_result)
        print(f"\n原始长度: {len(data)} bytes")
        print(f"编码长度: {len(encoded_data)} bytes")
        print(f"是否匹配: {encoded_data == data}")
        
        # 保存到文件
        with open('encoded_result.txt', 'w') as f:
            f.write(b64_result)
        print(f"\n已保存到 encoded_result.txt")
        
    else:
        # 正常模式:解析base64并输出文本
        print("正在解析protobuf数据...")
        msg = parse_message(data)
        
        # 格式化输出
        formatted = format_message(msg)
        print(formatted)
        
        # 保存到文件
        with open('output1.txt', 'w', encoding='utf-8') as f:
            f.write(formatted + '\n')
        print("\n已保存到 output.txt")
        
        # 验证:编码回去看是否一致
        print("\n验证:重新编码...")
        re_encoded = encode_message(msg)
        print(f"原始数据: {len(data)} bytes")
        print(f"重新编码: {len(re_encoded)} bytes")
        print(f"数据一致: {re_encoded == data}")
        
        if re_encoded == data:
            print("✅ 编码解码完全可逆!")
        else:
            print("❌ 数据不一致")
            # 找出差异
            for i, (a, b) in enumerate(zip(data, re_encoded)):
                if a != b:
                    print(f"第一个差异在位置 {i}: 原始={a:02x}, 重编码={b:02x}")
                    break