十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python在线运行实战:十六进制字符串解析与调试技巧

Python在线运行实战:十六进制字符串解析与调试技巧 前阵子调试一个蓝牙温湿度计设备端的协议文档拿过来一看返回报文是一串十六进制字符串类似aa 01 64 02 1f 90...这种。我手头那台电脑没装完整的 Python 环境就顺手打开浏览器里的一个 Python 在线运行器把解析脚本粘贴进去改了两轮校验逻辑十分钟就把温湿度数据解出来了。这段经历其实很典型日常开发里凡是跟串口、蓝牙、网络抓包、二进制日志打交道的人几乎都会碰到十六进制字符串解析而 Python 在线运行这类免安装的调试环境恰好是验证这类代码最短的那条路径。这篇文章就围绕“Python 在线运行 十六进制字符串解析”这个组合展开从为什么需要解析十六进制、怎么拆字段、怎么处理脏数据到常见的坑和排查思路我会把能落地的代码和判断逻辑都放在里面。适合串口调试、蓝牙协议分析、网络包解析、嵌入式日志处理这几类场景的开发者参考尤其是刚接触二进制协议、想快速验证解析逻辑的新手。1. 为什么要跟十六进制字符串较劲1.1 十六进制字符串在真实世界里的存在感很多刚接触硬件通信或者网络协议的开发者第一次看到协议文档里的报文都会愣一下为什么设备不直接返回“温度 25.6 度”这种人类友好的数据非要在串口助手里显示成01 03 02 10 00 80 5A原因很简单设备端的单片机要传输的是二进制数据但二进制在调试、存储、日志、中间件转发这些环节里既不好看也不方便于是大家约定俗成用十六进制来表示一个字节的内容。一个字节是 8 个 bit用十进制表示需要 0 到 255 这 256 种可能不够直观用十六进制正好是两位0x00到0xFF每个字节对应刚好两个字符。所以你会发现串口调试助手、网络抓包工具、嵌入式日志系统、甚至很多传感器厂商的文档给出的原始数据一律是十六进制字符串。我见过最典型的几类场景串口或 USB 设备返回的协议帧比如温湿度传感器、扫码枪、工业控制板。蓝牙 BLE 广播包和 GATT 通知返回值很多自定义服务的数据都按十六进制字符串透传。网络报文的 payload 抓包展示尤其是在 Wireshark 里看原始字节流。日志系统里打印出来的二进制内容不少服务端框架会把 bytes 直接.hex()后落盘。这些场景里我们拿到的不是现成的结构化字段而是“一串字符”。字符串只是载体真正有价值的温度值、湿度值、电量值、状态位都被藏在特定位置的字节里。解析十六进制字符串本质上就是把这个字符串按协议拆开还原成一个个有业务含义的数值。1.2 解析的本质让字符串变回“机器认识的字节”这里要先理清一个概念十六进制字符串不是二进制数据本身它是二进制数据的一种“书面表达”。举个例子报文里出现一个字节内容是二进制的0001 0010也就是十进制的 18。为了让人能读设备厂商会在协议里把它写成12于是你看到的字符串就是12。但当你把它当字符串去处理时12是两个字符字符1和字符2它们各自有各自的 ASCII 码并不是一个字节的 18。所以解析的第一步一定是“翻译”把字符串表示还原成真实的字节流。在 Python 里这对应着str到bytes的转换。很多新手在这里会踩第一脚坑直接对字符串做切片、取[0:2]、然后int()最后发现拿到的数值和协议文档里对不上。根源就在于你切的是“字符”不是“字节”。如果你做过数据处理可以把它类比成“拆快递”。快递面单上写着“手机 1 台充电器 1 个”这是给人看的描述真正装东西的是箱子里的实物。十六进制字符串就是那张面单bytes字节流就是箱子而你要解析的数据是箱子里的实物。协议解析就是先按面单找到箱子再打开箱子按清单取货。Python 在线运行能做的就是让你不用在本地折腾环境直接拿一段报文反复试这个“拆箱”逻辑。1.3 在线运行为什么它是调试这类代码的最快路径我知道现在很多开发者习惯本地装个 PyCharm 或者 VSCode配好环境再写代码。但十六进制字符串解析这种任务大多数时候是一个“一次性验证”的活儿拿到一段报文跑个脚本看看解析结果对不对改几个参数再跑一次确认没问题后要么把代码粘回正式工程要么就直接用了。这种场景下你为它专门建虚拟环境、配 IDE 项目、管理依赖其实是有点重了。在线运行器解决的就是这个矛盾。不需要安装任何东西只要有个浏览器打开一个支持 Python 3 的在线解释器页面把代码粘贴进去点运行立刻看到输出。这整个流程通常不到一分钟。而且十六进制字符串解析这个任务基本不需要第三方库Python 标准库里的bytes、binascii、struct就足够覆盖绝大多数需求所以在线环境完全够用。我个人的习惯是在正式工程的代码里写一个大而全的解析模块但每次调协议、对字段、验证校验算法时都在在线运行器里先用最小脚本跑通再回填到项目里。这样可以避免在主项目里反复打印调试、污染日志也能快速对比不同协议的解析结果。这篇文章后面给的所有代码你都可以直接复制到在线运行器里跑。2. 核心思路字符串、字节与十六进制之间的三角关系2.1 Python 里与二进制数据打交道的三种形态在写解析代码之前必须把 Python 中和二进制相关的三种数据形态分清楚str字符串、bytes字节串、int整数。它们之间不是随便互转的转错了类型轻则报错重则解析出完全错误的结果还不报错。形态说明常见来源常见下场str文本字符串比如aa0102串口助手复制、日志、协议文档需要转成 bytes 或 int 才有业务意义bytes字节串比如b\xaa\x01\x02bytes.fromhex()、网络接收、文件读取可以按索引取字节也可以.hex()转回字符串int整数比如170int(..., 16)、b[0]参与算术运算、拼装字段、校验计算我见过很多解析代码把这三个混在一起比如直接对字符串做len()然后当成字节长度用结果长度总是翻倍又比如把bytes直接拼到字符串里触发TypeError。正确的心法是十六进制字符串只是输入格式解析过程中尽快把它转换成bytes后续字段拆解和数值计算都以bytes和int为主。之所以推荐这种思路是因为bytes是按字节索引的b[0]返回的就是一个 0 到 255 的整数天然适合做协议字段解析。你在字符串上做s[0]拿到的却是一个字符还得再转一次 int中间多出不少出错的机会。2.2 两个必须刻进脑子的方法bytes.fromhex()和hex()Python 内置的bytes.fromhex()是我处理十六进制字符串时最常用的入口方法。它可以直接把一个十六进制字符串转成bytes并且能自动忽略字符串中的空格s 01 03 02 12 34 b bytes.fromhex(s) print(b) # b\x01\x03\x02\x124 print(len(b)) # 5 print(b[0]) # 1这段代码在在线运行器里直接跑就能看到效果。bytes.fromhex()的输入不需要带0x前缀也不需要每个字节都补成两位但如果某个字节只有一位比如a它也会按0a处理。这个方法实在太好用了以至于我基本不写手动的“先去空格再切割”的代码。反向操作则是bytes.hex()它把字节流重新表示成十六进制字符串b b\x01\x03\x02\x123 print(b.hex()) # 0103021233 print(b.hex( )) # 01 03 02 12 33Python 3.8 支持bytes.hex( )是 Python 3.8 引入的参数能让你以空格分隔每个字节在在线环境里如果看到TypeError: hex() takes no keyword arguments或者hex() argument 2 must be str, not int说明当前解释器版本太老后面我会在常见问题里展开。2.3 为什么在在线运行器里直接print(b)看到的是\x开头这一步很多人会莫名奇妙好不容易把字符串转成 bytes 了打印出来却是b\x01\x03\x02\x124感觉什么都没解析出来。其实这是bytes的 repr 表达方式Python 会把不可打印的字节统一显示成\xXX的形式可打印的 ASCII 字符比如数字、字母直接显示原字符。比如b\x01\x03\x02\x124里前三个字节是不可打印的控制字符所以显示成\x01、\x03、\x02最后一个字节0x34对应 ASCII 字符4所以直接显示成4。这并不代表数据有问题只是显示规则如此。解析十六进制字符串的关键不在于“打印出来好不好看”而在于“能不能按协议把字节拆到位”。你要时刻提醒自己看到b\xaa\x01\x0c...时心里应该把它还原成AA 01 0C...然后继续按字段切分。如果你需要更好看的输出可以在最后统一格式化成大写、带空格的十六进制字符串这个我在第 4 章会专门写一个格式化函数。3. 实操演练用在线运行器从零解析一段十六进制报文3.1 三分钟上手在线运行器如果你还没用过 Python 在线运行器操作流程极其简单打开搜索引擎搜“Python 在线运行”选一个支持 Python 3 的在线解释器页面通常页面上会有一个编辑框和一个“运行”按钮。把代码写进编辑框点运行下方会显示出程序的标准输出。我建议你创建一个最小模板之后每次解析都从这套模板开始# 粘贴到在线运行器 def main(): frame_hex AA 01 0C 02 12 34 56 78 9A BC 12 34 56 78 5A frame_bytes bytes.fromhex(frame_hex) print(字节长度:, len(frame_bytes)) print(前 4 字节:, frame_bytes[:4].hex( ).upper()) if __name__ __main__: main()这段代码的核心逻辑是把原始十六进制字符串转成bytes然后先看看总长度、前几个字节长什么样确认数据格式符合预期再一步步往下拆。十六进制解析最忌讳的就是不看原始长度、不看帧头直接蒙头切字段。3.2 第一步把带空格的 hex 字符串变成字节流实际从串口助手或日志里复制出来的数据格式五花八门有的是AA010C02...这种没有空格的有的是AA 01 0C 02...这种每个字节空一格还有可能是AA,01,0C这种带逗号的。好在bytes.fromhex()本身就能处理空格所以常见的第一种变形最简单s1 AA 01 0C 02 12 34 56 78 9A BC 12 34 56 78 5A s2 AA010C02123456789ABC123456785A b1 bytes.fromhex(s1) b2 bytes.fromhex(s2) print(b1 b2) # True如果你拿到的是带逗号、带0x前缀的变体先做一次清洗再交给fromhex会更安全。不要直接对原始字符串切片否则遇到不同分隔符就会崩。3.3 第二步按帧格式拆解字段为了演示我设计一个简化版的通信协议帧。假设这是一个蓝牙温湿度设备的上报帧格式如下帧头2 字节固定为AA 01数据长度1 字节表示后面数据区的字节数数据区若干字节具体业务含义见下校验和1 字节从帧头开始到数据区结束所有字节累加取低 8 位示例报文AA 01 0C 02 12 34 56 78 9A BC 12 34 56 78 5A。这里的0C是十六进制的 12表示数据区 12 个字节。数据区我按场景再拆一层设备类型1 字节0x02温度值2 字节有符号整数单位 0.1 摄氏度湿度值2 字节无符号整数单位 0.1%电量1 字节百分比预留6 字节解析代码def parse_frame(frame_hex): data bytes.fromhex(frame_hex) if data[0:2] ! b\xaa\x01: raise ValueError(帧头错误) length data[2] if len(data) ! 3 length 1: raise ValueError(长度字段与实际数据不匹配) payload data[3:3 length] checksum data[-1] device_type payload[0] raw_temp int.from_bytes(payload[1:3], byteorderbig, signedTrue) temp raw_temp / 10.0 raw_humi int.from_bytes(payload[3:5], byteorderbig) humi raw_humi / 10.0 battery payload[5] reserved payload[6:] print(设备类型:, hex(device_type)) print(温度:, temp, ℃) print(湿度:, humi, %) print(电量:, battery, %) print(校验和:, hex(checksum)) parse_frame(AA 01 0C 02 12 34 56 78 9A BC 12 34 56 78 5A)这里用了int.from_bytes(payload[1:3], byteorderbig, signedTrue)来还原有符号整数。为什么不用struct因为在线运行器里能跑struct也能跑但int.from_bytes的可读性更好新手不容易把格式字符串写错。后面我会单独讲struct适合什么场景。3.4 第三步校验和计算并验证数据完整性解析出业务字段只是第一步更严谨的做法是把校验和也算一遍确认报文在传输过程中没有被改过。我设计这个协议的校验规则是从帧头开始到数据区结束所有字节累加取低 8 位。def calc_checksum(frame_hex): data bytes.fromhex(frame_hex) # 去掉最后一个校验和字节 body data[:-1] total sum(body) # 取低 8 位 return total 0xFF demo_hex AA 01 0C 02 12 34 56 78 9A BC 12 34 56 78 5A calc calc_checksum(demo_hex) reported bytes.fromhex(demo_hex)[-1] print(f计算校验和: {calc:02X}, 报文里的校验和: {reported:02X}) print(校验通过 if calc reported else 校验失败)如果你把某个报文里的字节改错一位校验和一般就对不上这时候第一反应应该是数据传输出错而不是解析代码写错。这套思路在做真实协议的在线调试时非常管用先验证帧头再看长度最后对校验和三个条件都满足解析出的业务字段才能放心用。4. 进阶玩法常见变形解析与格式化输出4.1 处理带0x、带逗号、带空格的脏输入真实环境里的十六进制字符串经常不是干干净净的。有的设备日志会打印成0xAA, 0x01, 0x0C有的网络框架会输出[aa, 01, 0c]这种列表字符串。如果直接丢给bytes.fromhex()大概率会报ValueError: non-hexadecimal number found。我建议写一个统一的清洗函数把所有非十六进制字符全部过滤掉再看情况import re def clean_hex_string(raw): # 去掉 0x 前缀、逗号、空格、换行等干扰符 cleaned re.sub(r0x, , raw, flagsre.IGNORECASE) cleaned re.sub(r[^0-9a-fA-F], , cleaned) if len(cleaned) % 2 ! 0: # 奇数长度说明最后一个字节不完整 cleaned 0 cleaned return cleaned samples [ 0xAA, 0x01, 0x0C, 0x02, [aa, 01, 0c], AA 01 0C\r\n02 ] for s in samples: print(s, , bytes.fromhex(clean_hex_string(s)).hex( ).upper())这个函数允许我突然看到一段日志、来不及手动清洗的时候直接粘进去跑。注意re.sub(r0x, , raw)要放在去除非十六进制字符之前因为x本身不是十六进制字符如果先执行第二行过滤0x会被过滤成0导致字节错位。这个顺序错了我踩过一次肉眼很难发现。4.2 字节流转回十六进制字符串并统一格式解析完之后经常需要把某一段字节重新格式化成可读的形式比如在日志里统一打印成大写且以空格分隔。Python 3.8 可以直接用bytes.hex( )加.upper()def format_hex_bytes(data: bytes) - str: return data.hex( ).upper() frame_hex AA 01 0C 02 12 34 56 78 9A BC 12 34 56 78 5A data bytes.fromhex(frame_hex) print(format_hex_bytes(data[:4])) # AA 01 0C 02如果在线运行器版本低于 3.8bytes.hex( )不可用可以用生成器表达式手动拼def format_hex_bytes_compat(data: bytes) - str: return .join(f{b:02X} for b in data)推荐在在线运行器里先试一下原生的bytes.hex( )如果报错再换兼容写法。这两段代码我会都贴在工程模板里作为格式化工具函数使用。4.3 综合案例解码温度、湿度、电量的传感器数据最后做一个完整的综合案例把前面讲的字段拆解、校验和、格式化输出串起来。假设一个设备上报的协议如下帧头FA 552 字节长度1 字节数据区总字节数数据区共 6 字节温度 2 字节有符号整数0.1℃、湿度 2 字节无符号整数0.1%、电量 1 字节、状态位 1 字节校验和1 字节帧头到数据区结束累加取低 8 位示例报文FA 55 06 FF CE 02 2B 5A 01 9F。其中FF CE是有符号的-50除以 10 就是-5.0℃湿度02 2B是555也就是55.5%电量5A是90%状态01表示在线。完整解码代码def decode_sensor_report(hex_str): body bytes.fromhex(hex_str) if body[:2] ! b\xfa\x55: raise ValueError(帧头错误) length body[2] payload body[3:3 length] data body[:3 length] checksum body[-1] calc sum(data[:-1]) 0xFF if calc ! checksum: print(f警告: 校验和不匹配, 期望 {checksum:02X}, 实际 {calc:02X}) temp_raw int.from_bytes(payload[0:2], big, signedTrue) temp temp_raw / 10 humi_raw int.from_bytes(payload[2:4], big) humi humi_raw / 10 battery payload[4] status payload[5] print(f温度: {temp:.1f} ℃) print(f湿度: {humi:.1f} %) print(f电量: {battery} %) print(f状态: {在线 if status 0x01 else 离线}) return temp, humi, battery decode_sensor_report(FA 55 06 FF CE 02 2B 5A 01 9F)这个案例基本覆盖了日常设备调试中 80% 的解析需求定长数据、有符号数、无符号数、字节序、校验和。你完全可以把这段代码当成一个模板换成你自己的协议字段即可。如果遇到变长数据或者嵌套结构就在payload的基础上继续按偏移量切。5. 常见问题与排查技巧5.1 遇到报错怎么办在线运行器里写完代码最常见的几个错误我在下面整理成了一张速查表。你解析十六进制字符串时看到类似报错直接对照原因调代码报错信息常见原因解决办法ValueError: non-hexadecimal number found字符串里有0x、逗号、中文符号等非十六进制字符先调用clean_hex_string()清洗再bytes.fromhex()TypeError: string argument expected, got bytes把bytes和str直接拼接比如数据: b\x01先b.decode(utf-8, errorsignore)或者用format()格式化ValueError: odd-length string十六进制字符串长度是奇数少了一个字符检查输入是否漏了某位或在开头补0IndexError/struct.error切片越界或struct.unpack的长度和格式不匹配先输出len(data)和data.hex()确认报文长度AttributeError: bytes object has no attribute hexPython 版本太低老环境改用binascii.hexlify(b).decode()或手工拼接我调试时有个习惯如果代码报错第一件事不是改代码而是先打印三样东西——原始字符串、清洗后的字符串、bytes.fromhex()转出的bytes。只要这三样是连续的、符合预期的后续字段解析基本不会出大问题。这比你盯着报错信息猜更快。5.2 在线运行时的版本与输入输出问题在线解释器鱼龙混杂有的默认跑 Python 2有的虽然写 Python 3 但版本可能偏老。这会导致几个典型问题bytes.hex( )需要 Python 3.8低版本直接报错建议统一用兼容写法。print在 Python 2 里是语句不是函数如果你把 Python 3 代码粘到 Python 2 环境会立刻语法报错。所以选在线环境时认准“Python 3”字样。input()在 Python 3 里返回字符串在 Python 2 里返回的是原始输入可能需要raw_input()。如果你写的代码涉及键盘输入注意当前运行器的版本。如果你从串口助手拷贝的数据带着\r\n或者中间有换行bytes.fromhex()其实能忽略空格和换行但逗号、方括号这类字符不行必须先清洗。另外在线运行器的输出区一般只能看文本没有断点调试。我的做法是在关键位置临时加print()把每个中间变量打出来。比如我想确认切片是否取对就打印payload[:2].hex()看到ffce就知道数据进来了。确认逻辑无误后再把临时print删掉这比试图用断点调试更快。5.3 我的避坑清单最后分享几条从实际调试中总结出来的经验每一条都是用踩坑换来的先判断字节序再定 read 还是 big。温度、湿度这种多字节数值一定要查协议文档里写的是大端高字节在前还是小端低字节在前。同一个12 34大端是0x12344660小端是0x341213330差出去三倍。不确定时把两个字节组合出来算一下哪个数值更符合常识。比如湿度 55.5%大端解出来是 55.5小端可能是 133.3物理常识立刻暴露问题。负数不要自己写补码转换。温度可能为负所以int.from_bytes(..., signedTrue)或者struct.unpack(h, ...)一定要带signed参数。千万别自己做“取反加一”这种手工补码计算标准库已经处理好了。报文复杂时用struct替代手写切片。如果你的协议是一长串固定格式字段比如“1 字节类型 2 字节温度 2 字节湿度 4 字节时间戳”用struct.unpack_from更省心import struct payload b\x02\x12\x34\x56\x78\x12\x34\x56\x78 device_type, temp_raw, humi_raw, timestamp struct.unpack_from(BHH L, 0)注意格式串里表示大端B是无符号字节H是无符号 2 字节L是 4 字节。如果字节序是反的就改成。struct的好处是一行代码就能拆出所有字段不需要手动计算偏移量但代价是你必须对格式串非常熟悉写错时排错成本也不低。在线运行器别放太大报文。虽然解析代码本身不重但如果你把几百 KB 的十六进制字符串直接粘贴到在线编辑框页面可能卡顿。真遇到大报文先在本地把数据切割成若干小片段用在线运行器验证最关键的几个片段再回本地做全量解析。解析函数尽量做成“输入字符串、输出结构化结果”的纯函数。不要在里面写print一堆临时调试信息也不要依赖外部的全局变量。这样你既可以在在线运行器里测试也可以直接复制回工程使用两边代码保持一致减少来回改动带来的低级错误。我在实际使用中最大的体会是十六进制字符串解析的逻辑并不难难的是把协议文档里的“字段表”准确无误地翻译成代码。在线运行器最大的价值是让你能在几秒钟内完成“改写代码、看结果、再改写”的循环而不用在本地环境和工程代码之间来回切换。等到解析逻辑稳定了再把它整理成函数或类放回正式工程这时候你就拥有了一份经过快速验证、可复用的解析工具。后续遇到不同的协议也能套用这套思路很快拆解出想要的数据。
返回列表