第一次看到“在Notepad++里给十六进制数加0x前缀”这个问题时,我第一反应是“这还用问?查找替换嘛”。真正做的时候才发现,十六进制数的边界、0x重复前缀、纯数字的十六进制怎么处理,每个细节都能让你白改一堆文件。这是一个很典型的“正则批量改文本”场景,适用人群特别广:写单片机固件、看串口日志、整理内存数据、做CTF题,都会碰到。下面我把完整思路和实测过的正则写出来,照着抄就行。
1. 先想清楚:到底要把什么改成什么
1.1 数据的“身份”决定了正则怎么写
给十六进制数加0x前缀,看起来是“无脑加前缀”,但真正动手前要问自己三个问题:这些数字里是不是全是十六进制?有没有已经带0x前缀的?有没有普通英文单词混在中间?
我举个最常见的例子。串口日志里导出的数据是:
1A 2B 3C 4D 5E 6F 10 20 30这里的1A、2B、3C明显是十六进制,但10、20、30到底是十六进制的0x10、0x20、0x30,还是十进制的10、20、30?如果不先确认这一点,正则一替换下去,该加的、不该加的全都加了,后面返工更痛苦。
所以在动手指之前,先给数据“定性”:所有数字串是否都来自十六进制采集设备,还是既有十进制又有十六进制的混合文本。我处理过的大部分场景是前者,串口打印、设备寄存器值、抓包数据,基本都是“见到数字全当十六进制看”。但一旦到了代码文件里,情况就复杂了,变量名、注释、字符串里都有可能混进看似十六进制的字符串,这时候就需要更严格的边界条件。
第一条经验:正则写得再好,也替代不了对数据格式的判断。先花一分钟确认数据来源,能省后面一小时。
1.2 批量处理的三种思路对比
除了手动一个一个改,常见思路有下面三种,我按实用性排个序:
第一种:Notepad++自带的查找替换 + 正则。这是最常用、也是性价比最高的方式,不需要装任何插件,只要查找框里写对正则,点一下“全部替换”就完成。适合纯文本、格式化日志、代码片段等绝大多数场景。缺点是正则本身有学习成本,边界条件写不对容易误伤。
第二种:TextFX、Python Script等插件脚本。适合要反复处理、或者替换逻辑比较复杂的情况。比如你每天都要把某个格式的数据加前缀,用脚本可以一键完成,还能顺带做大小写转换、排序、去重等操作。缺点是需要安装插件,而且新版Notepad++对某些插件支持得不太好。
第三种:用Excel、PowerShell等外部工具处理。如果数据只有一小段,复制到Excel用公式也能做,比如="0x" & A1。批量改几十个数字挺方便,但数据量大了、或者要直接在文件里改回原处,就绕远了。我一般只在数据是表格结构时才这么干。
1.3 为什么是Notepad++而不是其他编辑器
有人可能会说,VS Code、Sublime也都有正则替换,为什么非要守着Notepad++?这话没错,但Notepad++在某些场景确实有不可替代的方便:启动快、打开大日志不卡、对老机器友好,而且正则替换入口就在最顺手的“替换”按钮里,不用来回切面板。尤其是处理嵌入式开发里的日志文件时,我习惯先把文件拖进Notepad++,随手一个正则替换,比打开VS Code等插件加载快多了。
另一个理由是Notepad++的PCRE正则能力其实不弱。虽然不像专业编辑器那样支持所有高级语法,但咱们要用的“向后断言”“捕获分组”“非捕获分组”,它都能跑通。下面要讲的所有正则,我在Notepad++ 8.x上全部实测过,可以直接用。
2. 手把手:用Notepad++正则替换加0x前缀
2.1 最直接的写法:\b[0-9A-Fa-f]+\b
先看最直观的版本。按Ctrl + H打开替换窗口,在“查找内容”里填:
\b[0-9A-Fa-f]+\b在“替换为”里填:
0x$0然后勾选左下角的“正则表达式”,点“全部替换”。
这个正则的意思是:用单词边界\b圈住一段由十六进制字符(0-9、A-F、a-f)组成的连续字符串,$0代表整个匹配到的内容,替换时在前面加上0x。
比如文本:
1A 2B C3 4D替换后得到:
0x1A 0x2B 0xC3 0x4D这个版本简单粗暴,在纯十六进制数据里很好用。但它有两个明显的坑:第一,如果数据里已有0x1A这种带前缀的,0x里的0是十六进制字符,会被单独匹配出来,替换成0x0x1A;第二,像face、be、dead这种本身只由 A-F 组成的英文单词,也会被加上前缀。后面我会专门讲怎么避开这两个坑。
2.2 更稳妥的写法:用负向断言,避开已带0x的数
如果你面对的数据里既有已经带0x的,又有没带的,直接用上面的表达式会出大问题。举例:
0x1A 2B AB用\b[0-9A-Fa-f]+\b匹配时,0x1A中的开头的0会被单独匹配到,因为它满足“单词边界 + 十六进制字符”,结果就变成0x0x1A 0x2B 0xAB,你还要再花一轮去修复0x0x。
更好的写法是把“可选前缀”也纳入匹配范围,确保重复替换也不会破坏已有前缀。查找内容用:
(?<![0-9A-Fa-fxX])(?:0[xX])?([0-9A-Fa-f]+)替换为:
0x$1这个正则拆开看是两部分:
(?<![0-9A-Fa-fxX]):负向后行断言,匹配位置前面不能是十六进制字符,也不能是x或X。这一下就把x1A、0x1A里的1A排除掉了,因为1A前面是x。(?:0[xX])?:表示前面的0x或0X可有可无。如果本来带前缀,它会把前缀一起匹配进去;如果没带前缀,直接匹配数字部分。([0-9A-Fa-f]+):捕获真正的十六进制数字部分,替换成0x$1。
实际替换时,0x1A会被整体匹配,捕获组拿到1A,替换成0x1A,等于没变化;而2B没有前缀,捕获组拿到2B,替换成0x2B。重复执行多少次都不会出现0x0x。
这也说明了一个重要原则:替换时不是只匹配“要加前缀的数字”,而是把“已经是目标格式的内容”也包进来,统一通过捕获组重写,这样才安全。
2.3 只给“包含字母”的十六进制数加前缀
另一个让人头疼的场景,是数据里既有纯数字如10、20,又有含 A-F 的十六进制数如1A、C3。如果你只希望给含字母的十六进制数加0x,而纯数字保持原样,那就得在正则里加一个条件:匹配串里必须出现至少一个 A-F 字母。
查找内容写成:
(?<![0-9A-Fa-fxX])(?=[0-9A-Fa-f]*[A-Fa-f])[0-9A-Fa-f]+(?![0-9A-Fa-f])替换为:
0x$0关键在中间这个正向先行断言(?=[0-9A-Fa-f]*[A-Fa-f]):它要求当前位置往后看,存在“一串十六进制字符,其中至少有一个字母A-F”。这样一来,1A、C3、DEAD都会匹配;10、20、30因为全是数字,没有A-F字母,直接跳过。
我用这个表达式处理过一批日志,里面混着日期、计数器和寄存器值,效果很好。它默认“含字母的才是看起来像十六进制”,虽然不能100%保证语义正确,但在混合文本里比“见到数字全加前缀”安全得多。
2.4 按固定长度匹配:适配合法字节流
很多时候,十六进制数是有固定格式的,比如内存数据按字节显示时,每个数是两位:
1A 2B 03 C4 FF 10这时候可以用{2}把长度限定死:
\b[0-9A-Fa-f]{2}\b替换为:
0x$0组合起来就能只匹配正好两位的十六进制数,不会把1A2B这种连续串当成一个数,也不会把A这种单字符扯进来。如果数据是四字节的,就换成{4};如果是长度在1到8位之间,就用{1,8}。
按长度匹配的最大好处是“所见即所得”,逻辑简单,误伤概率低。缺点是要求你对数据格式非常了解,如果源数据长度本身不固定,这个方案就不适用了。
3. 实操示例:三种常见数据格式的完整替换流程
3.1 空格分隔的日志字节流
这是最典型的场景。假设日志文件里是这样一行:
A1 B2 C3 D4 E5 F6 00 1F目标是把每个字节都加上0x前缀,变成:
0xA1 0xB2 0xC3 0xD4 0xE5 0xF6 0x00 0x1F步骤非常简单:
Ctrl + H打开替换窗口。- 查找内容填
\b[0-9A-Fa-f]+\b,替换为填0x$0。 - 勾选“正则表达式”,点“全部替换”。
这里用最简的写法就够,因为整行数据都是十六进制字节,没有其余干扰项。替换完成后,建议再按一次Ctrl + H,把0x0x这种异常字符串搜一遍,确认没有重复前缀。
3.2 代码中的常量列表
如果是给C语言或Python代码里的一串宏定义加前缀,情况会稍微麻烦。比如:
#define A 1A #define B 2B #define C 10你希望有没有字母都加前缀,让它们变成0x1A、0x2B、0x10,这时候最简正则依然有效:
- 查找
\b[0-9A-Fa-f]+\b - 替换
0x$0
但要注意,代码里可能还有define、ALPHA这类变量名,ALPHA里的A虽然符合十六进制字符,但整个单词还有L、P这些不是十六进制字符的字母,所以\b[0-9A-Fa-f]+\b不会匹配ALPHA,这算是不幸中的万幸。真正的风险在于face、dead这种恰好全由A-F组成的单词。所以如果代码里有英文单词,强烈建议用第2.3节那个“必须包含A-F字母”的版本,并且结合上下文判断。
3.3 已经有一部分0x前缀,要补全缺失的
第三种情况很常见:文件里既有0x1A,又有裸的1A,需求是“不管带没带前缀,最终都要统一成0x”。直接用来最简正则会把0x1A里的0也匹配出来,变成0x0x1A。
所以要用第2.2节给出的“可选前缀 + 捕获组”方案:
- 查找
(?<![0-9A-Fa-fxX])(?:0[xX])?([0-9A-Fa-f]+) - 替换
0x$1
我在替换前数了一下文件里的匹配次数,老实的1A和带前缀的0x1A都会被匹配,但替换后带前缀的保持不变,没带的全补上。这个方案在实际数据里非常稳,推荐保存到常用正则列表里。
3.4 替换前先“查找全部”做一次确认
不管用上面哪种正则,我强烈建议在点“全部替换”之前,先点一下旁边的“查找全部”。这一步能让你在搜索结果窗口里看到所有会被替换的内容和数量。
我自己的习惯是:先看匹配数量是不是和自己的预期一致。比如我肉眼数出日志里有50个地址,结果“查找全部”显示500个,那一定是正则过宽,把一堆不该匹配的字符串也圈进来了。这时候直接点“全部替换”,改坏文件再恢复就麻烦了。宁可多花十几秒检查,也不要闭眼替换。
4. 我踩过的坑,以及值得记下的排查技巧
4.1 为什么\b在连字符、下划线面前会“失灵”
很多人第一次用\b[0-9A-Fa-f]+\b时,会遇到一种情况:“1A-2B”这样的数据,只匹配出了1A和2B,看起来没问题;但换成“1A_2B”,有时候却整个1A_2B都不匹配了,或者只匹配出2B。
原因是\b表示的是“单词字符”和“非单词字符”之间的边界。下划线_属于单词字符,所以1A_2B中1A后面跟的是_,它和字母都是单词字符,之间没有边界,\b不会出现在1A和_这里,导致1A无法单独匹配。连字符-属于非单词字符,所以1A-2B可以正常匹配出两段。
如果你遇到类似下划线分隔的数据,不要硬用\b,而是要显式地用负向后行、负向前行把边界定义为“不是十六进制字符”。比如:
(?<![0-9A-Fa-f])[0-9A-Fa-f]+(?![0-9A-Fa-f])这个写法不管前后是下划线、连字符、空格还是中文,只要不是十六进制字符,就能正常识别。这也是为什么前面我在高级写法里主要用断言而不是\b的原因。
4.2 “face”“be”这类英文单词也会被当成十六进制
十六进制字符集只有0-9A-Fa-f,这意味着大量英文单词天然就是合法十六进制数,比如face、be、dead、bad、cafe。如果文件是普通英文注释或字符串,你用\b[0-9A-Fa-f]+\b一替换,注释里的face会变成0xface,看着像乱改。
怎么避免?我常用的办法是加“必须有字母”的约束,也就是第2.3节那个正则,匹配串里至少要有一个 A-F。但这只能排除纯数字,无法排除face这种单词。要真正确认哪些是数据、哪些是文字,还得靠上下文。
实践中的妥协方案是:先确认文件类型。如果文件本身就是日志数据、代码常量、寄存器配置,里面很少出现完整的英文单词,那就可以放心用;如果文件是说明文档、带大量注释,那就要先用“查找全部”把匹配结果刷一遍,肉眼排除可疑项。
4.3 替换完出现0x0x,多半是“重复加前缀”
你自己手写正则的时候,如果只写[0-9A-Fa-f]+,不处理0x本身,很容易把0x1A里的0单独匹配出来,替换成0x0x1A。这种问题在日志里尤其隐蔽,因为0x1A本来就很像0x0x1A的子串,肉眼一扫未必能发现。
排查方法很简单:替换完成后,再开一次替换框,查找0x0x,如果搜到结果,说明重复加了。要修复,可以再执行一次:
- 查找
0x0x([0-9A-Fa-f]+) - 替换
0x$1
不过这只是补救,更推荐的做法是一开始就用第2.2节那个“可选前缀”的正则,从根源上杜绝重复。
4.4 大小写混用怎么处理
十六进制数字的大小写影响搜索结果,1A和1a在语义上是同一个数,但在正则里它们是不同字符串。如果你的数据里既有大写又有小写,正则字符集要写成[0-9A-Fa-f]覆盖两种情况。
另外,替换后你可能会发现前缀0x是固定小写,但后面的1A可能变成1a(因为源数据本来就是小写)。如果希望统一格式:
- 如果全部转大写:先复制一份备份,用菜单“编辑”→“格式转换”→“转为大写”处理整个文件。不过这会一并把注释、字符串里的大小写也改了,所以只适合数据文件。
- 如果只想改十六进制部分:用Python Script插件脚本,在脚本里对捕获组做
.upper(),这是更精细的做法,后面第5节给出代码。
我处理过的很多串口日志都是小写字母,而代码规范要求大写,遇到这种情况我一般直接用脚本一次性统一。
5. 进阶玩法:用Python Script插件做更灵活的批量处理
5.1 什么时候值得动用脚本
正则替换虽强,但遇到“不仅加前缀,还要统一大小写”“只处理选中区域”“根据前面某个字段决定要不要加前缀”这类需求时,表达式会变得又长又难维护。这时候我会切换到Python Script插件,写几行脚本,一劳永逸。
Python Script是Notepad++一个很老牌的插件,安装后能在“插件”菜单里看到Python Script入口。新版Notepad++的插件管理器不一定自带它,可能需要手动下载,如果装不上,也可以用NppExec等替代方案,但个人觉得Python Script最顺手。
如果只是临时一次性的替换,我建议还是用正则;如果是每天都要跑的固定流程,脚本的价值就体现出来了。比如我维护的一套日志清洗步骤,对选中区域执行脚本后,能一次性完成加前缀、转大写、去除重复值三步操作。
5.2 一段能用的Python脚本,以及关键点解释
下面这段脚本,是我存下来的“给十六进制数加0x前缀”最常用版本。它会对选中区域处理,如果没选中内容,则处理整个文档。
import re text = editor.get_sel_text() if not text: text = editor.get_text() pattern = re.compile(r'(?<![0-9A-Fa-fxX])(?:0[xX])?([0-9A-Fa-f]+)') def add_prefix(match): return '0x' + match.group(1).upper() new_text = pattern.sub(add_prefix, text) if editor.get_sel_text(): editor.replace_sel(new_text) else: editor.setText(new_text)几个关键点说一下:
editor.get_sel_text()拿到当前选中的文本,没选中就返回空字符串。我用这个特性实现“有选区就只替换选区,没选区就处理全文”,防止手滑改掉整份文件。- 正则部分和刚才第2.2节完全一样,但替换函数里用了
.upper(),会把十六进制部分统一成大写,比如1a变成0x1A。 pattern.sub会把所有匹配到的内容替换成add_prefix的返回值。
运行方法:打开Python Script的控制台,粘贴脚本运行;也可以先把脚本保存成.py文件,再通过菜单调用。我一般先在文档里选中一小段测试,确认结果无误后,再全选运行完整脚本。
5.3 把脚本变成菜单命令,一劳永逸
如果你和我一样要反复运行,可以把脚本注册成菜单命令。在Python Script里选择“Create Script”,给它起个名字,比如hex_add_prefix.py,然后把上面的代码保存进去。之后在插件菜单里就能直接点执行。
更妙的是,你还可以给它设快捷键:菜单“宏”→“管理快捷键”→“插件命令”,找到对应的脚本,指定一个组合键。我把它绑在Ctrl+Alt+H,现在处理日志基本就是“选中数据→按快捷键→完事”。整个流程快得像是编辑器原生功能。
用脚本处理后,建议再检查一下是否所有该加前缀的数字都变了。我习惯在脚本最后不直接setText,而是先在输出面板打印一条“共替换 xx 处”的消息,省得每次都心里没底。这个就留给你自己发挥吧。
我个人在实际操作中的体会是:正则替换这类事情,最怕的不是“不会写正则”,而是“没搞清楚数据格式就急着替换”。把文件类型、数字来源、是否已有前缀这三件事想清楚,再挑对应的正则,基本不会出错。最后再分享一个小习惯:每次替换前,先在查找框点一下“查找全部”,看看匹配数量和预期是否一致。如果差很多,千万别点“全部替换”,先回头把正则边界条件捋一遍。这个习惯帮我省下了无数次回滚的功夫,也希望你能用得上。