ARTICLE DETAIL

资讯详情

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

字符串处理实战:多语言逆序、分割与转换陷阱解析

字符串处理实战:多语言逆序、分割与转换陷阱解析 字符串大概是编程里最“不起眼”却又最能暴露水平的部分。我写了十几年代码从C语言的char[]一路折腾到 Java、Python、C#、JavaScript 和各类SQL方言发现一个很现实的问题越基础的操作越容易翻车。逆序一个字符串人人都会但遇到UTF-8中文就乱码split一个字符串三行写完可正则元字符、尾部空元素能让你立刻怀疑人生字符串转数字更是重灾区parseInt、atoi、int() 在不同语言里各有一套“失败时怎么办”的哲学。这一篇是“字符串 | part02”我不打算重复教科书上的定义直接进实战把逆序、分割拼接、类型转换、包含判断、比较排序、数组互转这些高频场景在不同语言里过一遍。该给的代码给代码该讲的坑讲清楚。适合看这篇的是写过一点代码但总觉得自己对字符串处理还不够扎实的同学以及日常在多种语言和数据库之间来回切换的搬砖选手。1. 逆序从C语言指针到Unicode最简单也最容易被绕晕的操作1.1 先过一遍C语言的原地逆序先说最经典的C语言版本。绝大多数教材里教的是这样void reverse(char s[]) { int len strlen(s); for (int i 0, j len - 1; i j; i, j--) { char tmp s[i]; s[i] s[j]; s[j] tmp; } }这段代码逻辑上没有大问题但你必须意识到两点。第一它只能处理可修改的字符数组如果你把字符串字面量直接传进来比如reverse(hello)在C标准里就是未定义行为现代编译器通常会警告但如果你写的是指针数组存放字符串再把数组里的某个元素传进来运行时不一定会提示你而是直接给你一个段错误。第二strlen每次都会从头扫到尾找\0如果你只逆序一次无所谓但在需要反复逆序字符串的代码里它会成为性能瓶颈。所以更实际的写法是用指针把长度也算进来void reverse(char *s) { int len strlen(s); char *p s, *q s len - 1; while (p q) { char tmp *p; *p *q; *q-- tmp; } }这个版本里p从头往中间走q从尾往中间走交换后各自收拢一格。本质上字符串逆序就是双指针交换别用额外数组也别写递归——递归版本虽然看起来优雅长字符串的栈开销是实打实的风险。Python里这题一行s[::-1]就完事Java里用new StringBuilder(s).reverse().toString()C#里常见的是把字符数组Array.Reverse之后再new string但三种写法背后的内存模型完全不同直接照抄不可取。1.2 按字节逆序为什么会让中文乱码很多教材不会告诉你一个事实C语言的char和strlen处理的是字节不是字符。在UTF-8编码下一个汉字占3字节你按字节去逆序等于把一个完好的汉字编码拆开再倒着拼回去——结果必然乱码。处理办法有三个方向。第一如果确认字符串是UTF-8先把字节流解码成码点数组逆序数组后再重新编码这样每个汉字都作为一个整体移动第二在Windows下用wchar_t和L宽字符串但注意wchar_t在不同平台宽度不一致Windows下是2字节UTF-16Linux/GCC下是4字节UTF-32想跨平台就得小心第三直接用高级语言的字符串类型——Python的字符串按Unicode码点存储、Java的String内部是UTF-16数组、C#的string也是UTF-16这些语言帮你处理了字符边界除非你特意去操作byte数组否则不会出现半个汉字的问题。顺带说一个和小票、短信计费相关的需求“汉字算一个字符英文字母和数字两个算一个字符”。这种描述通常意味着按显示宽度截断字符串。朴素的实现是遍历字符串给每个字符计算显示宽度落在CJK范围简单判断可以看 U4E00 到 U9FFF的按2计西文字符和数字按1计累计到上限再截断。这个逻辑在高级语言里用码点遍历即可但如果你在C语言里按字节去截就极可能在两个汉字中间切断形成半个汉字——这是生产环境中常见的乱码原因。1.3 逆序的实战伪装回文判断与拼数问题逆序在业务里最常出现的形态其实不是“把字符串倒过来”而是回文判断和拼数问题。回文判断两种思路一种是逆序后比较原串和逆序串另一种是双指针从两头往中间扫遇到不等就提前退出。后者在长字符串上更省内存也更适合在循环里做批量判断。拼数问题则把逆序和排序搅在一起。比如给你一组数字字符串要求拼出一个最大数。核心原则是定义两个串 a 和 b 之间的比较规则——如果a b大于b a那么 a 应该排在 b 前面。很多人第一次做这题会直接按字典序排序结果拼出来的数和期望差很远。代码层面Arrays.sort(arr, (a, b) - (b a).compareTo(a b));from functools import cmp_to_key arr.sort(keycmp_to_key(lambda a, b: (b a) (a b)))这里有个细节要强调如果字符串很长把a b转成整数再比较会溢出正确做法是直接按字符串字典序比较拼接结果别转数字。而“逆序输出 c 语言”这类题目本质上就是回文判断的前置步骤练的是指针移动和边界控制练熟了对理解很多字符串算法都有好处。2. split与拼接业务代码里最高频也最容易翻车的两个方向2.1 同一个split三个语言三个脾气字符串分割Java里写s.split(,)Python里写s.split(,)C#里写s.Split(,)JS里写s.split(,)——同一个拼写行为可能完全不一样。我踩过的坑主要是三个。第一个是尾部空字符串丢失。Java的String.split默认会丢弃尾部空字符串a,b,,.split(,)返回[a,b]不是[a,b,,]。如果业务需要保留每一列必须用split(,, -1)。Python则相反a,b,,.split(,)保留空串返回[a,b,,]但.split(,)返回[]而不是[]解析空消息时这个细节容易让人意外。同样是split一个丢弃一个保留写之前先确认目标语言行为比写完之后再补测试要省时间。第二个是正则元字符。Java和JS的split接收的是正则表达式a.b.c.split(.)会把每个字符都当成分隔符结果切得稀碎正确写法是split(\\.)C#的Split接收的是字面量字符或字符串传.就是按点切完全不用转义。假如你要按竖线|切Java里直接写split(|)正则引擎会把每个字符都切一遍得到一堆碎片而不是两段——必须split(\\|)。跨语言用split之前先搞清楚第一个参数到底是字面量还是正则这是最大的坑。第三个是业务数据里的全角逗号。产品给了一个Excel导出的列用户填的是中文逗号“”程序用英文逗号切结果整列都是脏数据。这种问题的标准解法是在切分前先做字符归一化把全角逗号、全角空格替换成半角再走split。替换的时候注意JavaScript的str.replace(,, ,)默认只替换第一个匹配要全局替换得写str.replaceAll(,, ,)或//gPython、Java、C#的replace默认都是全量替换。全角转半角属于字符串清洗的基础功但几乎每个项目里都会遇到。2.2 拼接的性能账与SQL聚合场景字符串拼接Java里String是不可变的循环里写s item每执行一次都会new一个String对象几万次循环之后GC压力肉眼可见正确姿势是StringBuilder最好在构造时传入初始容量避免扩容。C#里StringBuilder和string.Join都行日志和提示语用$插值可读性更好。Python里推荐.join(parts)CPython对做过一些优化但把这种优化当成语言保证在长文本拼接时还是会踩到性能问题。JS里arr.join()也稳定一些。这里补充一个容易混淆的点Python的字符串同样是不可变的a abc之后执行a[0] x会直接抛TypeError。热搜词里“python字符串直接赋值更改”说的其实是重新给变量绑定一个新字符串是允许的但原地修改字符是不行的。C语言则相反能原地修改的必须是可修改的字符数组字符串字面量本身的修改行为是未定义。三句话总结Python的变量可以重新赋值内容不可变Java的引用可以重新指向String内容不可变C的字符数组可以改内容但字面量千万别碰。再聊SQL里的字符串拼接。同一组多行字符串拼成一列SQL Server 2017 直接STRING_AGG(col, ,)老版本用STUFF FOR XML PATH注意XML会把转义成amp;拼接结果里如果包含特殊字符就会被坑。Oracle用LISTAGG但返回结果超过4000字节会报ORA-01489长文本场景要换别的方式。这类需求在报表、标签打印里非常常见选对版本对应的方案能省很多事。2.3 按显示宽度截断与换行符的统一处理按显示宽度截断在小票打印、短信接口、表格对齐里是刚需。核心就是给每个字符算宽度中文按2西文字母数字按1。实现时注意用码点遍历而不是字节遍历不然会切出半个汉字。在Java里String的length是UTF-16 code unit数量一个emoji占2个一个汉字占1个这跟你肉眼看到的字符数不一样处理时要用codePoint来遍历。这段不算复杂但落地的坑基本都是“用错计数单位”。换行符是另一个容易忽略的隐形地雷。Windows下换行是\r\nLinux/macOS是\n一份文本在Windows记事本编辑过拿到Linux解析经常在行尾多一个\r。bash里写多行字符串也容易翻车msg第一行 第二行 echo $msg如果漏掉双引号直接echo $msg换行会被吞掉。跨平台文本处理建议先统一换行符s.replace(\r\n, \n)。C#的逐字字符串...会保留换行但里面要用表示一个双引号这个转义规则刚接触时容易写错。工业组态屏的脚本里比如昆仑通态那类环境字符串换行通常也不是直接敲回车要通过拼接CHR(13)CHR(10)或者对应环境指定的转义方式实现这属于小众但真实存在的需求遇到时先查该脚本引擎的字符串转义文档别按通用语言的习惯硬套。3. 字符串与数字互转从SQL到C语言失败策略决定代码质量3.1 SQL里的数字判断ISNUMERIC为什么靠不住“判断字符串是不是数字”在数据库里是高频需求。SQL Server里新手喜欢用ISNUMERIC但它是出了名的宽松ISNUMERIC(1e5)返回1ISNUMERIC($100)返回1ISNUMERIC(-1.5)返回1这些值直接CAST成INT时要么报转换错误要么语义跟业务预期完全不同。新代码建议直接用TRY_CASTSELECT TRY_CAST(123 AS INT); -- 123 SELECT TRY_CAST(abc AS INT); -- NULLTRY_CAST(abc AS INT)返回NULL不会抛错。判断能否转换看CAST结果是否NULL就行了比ISNUMERIC可靠得多。Oracle里判断纯数字常用正则REGEXP_LIKE(col, ^[0-9]$)然后用TO_NUMBER转换。还经常遇到LONG类型转字符串的场景——LONG是老语法不能随便TO_CHAR也不能直接放进GROUP BY或ORDER BY常规做法是SUBSTR(long_col, 1, 4000)截成VARCHAR2处理或者用TO_LOB()转成CLOB。DB2里判断数字字符串除了REGEXP_LIKE还有一个老派的TRANSLATE技巧TRANSLATE(column, , 0123456789)把数字字符删掉剩下的内容为空等于就说明原串全部由数字组成。这句判断写法比较绕但胜在不依赖正则引擎老版本DB2也能用。3.2 各语言转换失败时到底会怎样编程语言里字符串转数字的失败策略差异比数据库还大。Java的Integer.parseInt(abc)直接抛NumberFormatExceptionC#的int.Parse也抛异常但int.TryParse(s, out var n)用bool返回值表达成功与否不抛异常这是我在这个场景里比较认可的设计Python的int(s)同样抛ValueError需要用try/except包住或者先做其他校验。Python里判断字符串是不是数字不能只靠str.isdigit()——它认为²上标2是数字isdigit()对1.5返回False对负数也返回False真正能进int()的字符串判断还得看业务场景用正则或isdecimal配合处理。JavaScript则是另一个极端parseInt(123abc)返回123不报错也不警告这种宽松行为容易把脏数据悄悄放过去Number(123abc)返回NaN(123)也是数字转换的常用写法。用之前想清楚业务到底要严格模式还是宽松模式别把语言默认行为当成真理。C语言里atoi(abc)返回0你分不清是成功转换还是真值为0有经验的代码会用strtolchar *end; long v strtol(s, end, 10); if (end s) { // 第一个字符就解析不了 } else if (*end ! \0) { // 后面还有尾巴不是完整数字 }end s说明没有可转换的数字*end ! \0说明后面还有多余字符这两个判断能精细控制解析范围。ABAP那种偏业务的环境里判断字符串是否是数字常用CO/CN比较运算符比如IF lv_str CO 0123456789.。每个语言都有自己的惯用手法核心思路都一样先判断再转换或者转换后严格检查失败信号。3.3 字节层转换getBytes、ASCII码与gzip解压后的还原字符串转数字之外字符串与字节数组之间的转换也很容易出事。C#里想拿字符串的ASCII码byte[] bytes Encoding.ASCII.GetBytes(ABC); // [65, 66, 67]但如果字符串里有中文ASCII编码表里压根没有对应项会被全部替换成0x3F问号。只要内容可能包含中文就一定要用Encoding.UTF8.GetBytes。Java同理s.getBytes()不指定字符集时用JVM默认换个部署环境就可能乱码Java 18 默认UTF-8了但老项目里还是建议养成getBytes(StandardCharsets.UTF_8)的习惯。Byte数组还原成字符串也是同样逻辑先确定字节流的编码再决定用哪个Charset去decode。GzipInputStream解压一组字节拿到byte[]之后如果直接new String(bytes)而不指定UTF-8在Windows默认GBK的环境下十有八九是乱码。热搜词里同时出现“gzipinputstream 转字符串”和“json转字符串”说明很多人做HTTP接口对接时先压缩、再解压、再转字符串、再解析JSON这一整条链路上任何一环编码不一致都会出问题。提示字符串转字节数组、字节数组转字符串永远显式指定编码不要依赖运行环境的默认值。宁可多敲几个字符别赌环境的默认行为。4. 包含、相等、大小写与排序比较规则里的暗礁4.1 判断包含子串的跨语言差异判断一个字符串是否包含某个子串每个语言都有自己的API和坑。Python用abc in s最自然Java用s.contains(abc)C#用s.Contains(abc)JS是s.includes(abc)。这里面有几件事需要特别留意。第一大小写敏感。Java的contains和C#的Contains默认区分大小写需要忽略大小写时C#可以直接s.Contains(abc, StringComparison.OrdinalIgnoreCase)Java没有这个重载常见做法是s.toLowerCase().contains(abc.toLowerCase())或正则。第二空字符串的包含判断abc.contains()返回true这在解析协议字段时会让人措手不及如果字段允许为空你以为是“包含”其实条件永远成立。第三数据库里的包含判断Oracle常用INSTR(col, abc) 0或LIKE %abc%SQL Server用CHARINDEX(abc, col) 0。注意LIKE %abc%因为前导通配符通常没法走普通索引表大时性能会很难看能用INSTR或CHARINDEX反而更好分析。4.2 字符串相等比内容还是比地址字符串相等比较的坑核心就一句话这个语言里比的是内容还是引用。C语言里strcmp(abc, abc) 0才是内容比较直接用比的是指针地址虽然编译器偶尔会把相同字面量合并到同一块内存让你的测试碰巧通过但一个来自字面量、一个来自运行时缓冲区的字符串用比较永远是false。Java里比引用equals比内容——小字符串因为常量池缓存可能碰巧相等但new出来的对象就必须用equals。C#的被重载成了内容比较ReferenceEquals才是比引用和Java正好相反。JavaScript里会做类型转换5 5是true写才能挡掉这种坑。Python里已经是内容比较is才是身份比较但CPython对小字符串有intern机制hello is hello在某些环境为True这种依赖实现细节的写法不应该出现在正式代码里。为了更直观我习惯用一张小表记住差异语言 的默认行为内容比较的正确姿势C指针地址strcmpJava引用地址equalsC#内容已重载 或 string.EqualsJavaScript宽松相等可能类型转换Python内容遇到新语言第一步先查清楚相等运算符的语义比什么都重要。4.3 大小写转换与字符串排序的Locale/Collation问题大小写转换看似没有技术含量坑全在Locale。Java的s.toLowerCase()不传Locale时跟着系统默认走知名例子是土耳其语环境下I.toLowerCase()得到的是不带点的ı而不是i导致代码在土耳其语系统上行为异常。对枚举名、协议字段、编程语言关键字做归一化时建议显式传Locale.ROOT或Locale.ENGLISH。C#的ToLower同样有culture概念处理文件名、URL时用ToLowerInvariant()更稳。字符串排序分两种纯编码序和自然排序。JavaScript的默认sort()按UTF-16码元比较[10, 2, 1].sort()得到[1, 10, 2]10排在2前面这在文件列表、版本号排序里很反直觉要按自然语义排序得传比较函数比如(a, b) a.localeCompare(b, { numeric: true })。C语言里strcmp永远是编码序。数据库排序则看collationSQL Server的Chinese_PRC_CI_AS下中文列按拼音排序换一个排序规则顺序可能完全变样。排序规则是字符串比较的底层基调调整它比在代码里写各种分支都来得彻底。4.4 顺带一提判断驼峰字符串的朴素写法热搜词里有“python 字符串是否驼峰”这类需求通常出现在变量命名规范检查工具里。朴素的思路是遍历字符串统计大写字母个数小驼峰要求首字符小写且后续单词首字母大写大驼峰PascalCase要求首字符大写。Python没有内置的isCamelCase方法自己写个遍历比引正则更好调试正则写个^[a-z][a-zA-Z]*$只能判断形状判断单词边界还得靠大小写切换点。这类应用不复杂核心是别把“大小写转换”和“命名规范校验”混为一谈——前者改大小写后者读结构。5. 字符串与数组互转、枚举名、JSON与连接字符串5.1 字符串与数组互转的一张速查表字符串按分隔符转数组、数组按分隔符拼字符串这两个操作看起来互逆各语言却叫法各异。直接给张表语言字符串 - 数组数组 - 字符串Javas.split(,)String.join(,, arr)Pythons.split(,),.join(my_list)C#s.Split(,)string.Join(,, arr)JavaScripts.split(,)arr.join(,)Cstrtok/ 手写分割手写循环拼接补充几点。C语言的strtok会修改原字符串并且用静态缓冲区保存状态多线程里会串数据生产代码尽量用strtok_r或自己手写分割。JS想把字符串拆成单个字符用[...s]而不是s.split()因为[...s]按码点拆对这种增补平面字符不会拆成两个代理项split()会。Java里s.toCharArray()返回char[]后面对char[]做任何修改都不影响原String因为它是拷贝。数组转字符串最朴素的是join但如果要保留结构化信息JSON才是正式方案。5.2 C语言指针数组与宽字符串字面量的坑C语言里“指针数组存放字符串”是经典考点。char *arr[] {hello, world};这种写法数组本身是可变的但数组元素指向的字符串字面量存储在只读区域arr[0][0]H属于未定义行为。很多新手在本机测试时没崩换编译器或开优化选项就崩原因就在这。如果需要修改字符串内容用char arr[][10]二维数组拷贝一份或者堆上分配内存自己管理。C里更推荐std::arraystd::string, N或std::vectorstd::string基本绕开这类问题。CodeBlocks里常见的“宽字符L表示出错”也是字符串字面量前缀使用不当。L你好是宽字符串字面量要用wchar_t接收但wchar_t在不同平台宽度不同Windows下通常是UTF-16Linux/GCC下是UTF-32同样的L字符串想跨平台共享行为完全不可控。C11/C17之后更推荐明确编码后缀u8你好表示UTF-8u你好表示UTF-16U你好表示UTF-32代码里写得越明确跨平台越省心。现代C项目处理中文字符串我一般建议直接用std::string存UTF-8不要再穿wchar_t的鞋。5.3 模板字符串、枚举转字符串与JSON序列化模板字符串如今已是标配。JS的反引号模板字符串能嵌入表达式、保留换行const name 小明; const message 你好${name};C# 6.0后的$插值、Python的f-string也是同一思路。生成动态SQL、日志模板、错误提示时可读性比拼接好太多。枚举转字符串多用于日志和接口输出。C#里Enum.GetName(typeof(Status), Status.Active)Java里Status.ACTIVE.name()Python的Color.RED.name拿名字、Color.RED.value拿值。需要注意C#直接Status.Active.ToString()默认输出枚举名但Java的toString如果没重写默认输出不是枚举名行为有差异。JSON转字符串是字符串处理绕不开的一环。Python里json.dumps(data, ensure_asciiFalse)才能让中文保持可读而不是一堆\u转义Java的Jackson需要注册JavaTimeModule才能序列化LocalDateTimeC#的System.Text.Json默认情况下对某些类型也有限制。JSON本质是字符串承载结构化数据序列化和转义规则决定了跨系统传输的兼容性。使用任何序列化库时先看它的默认编码和失败策略能避免大量线上问题。5.4 ODBC连接字符串另一种字符串语法最后讲一个容易被忽略的字符串场景连接字符串。ODBC的连接串长这样Driver{ODBC Driver 17 for SQL Server};ServermyServer;DatabasemyDb;UidmyUser;PwdmyPass;它有自己的语法规则键值对用分号分隔值如果包含花括号或特殊字符需要转义不能简单套用JSON解析或普通配置文件读法。比如密码里包含右花括号}通常要写成}}表示一个字面量花括号。很多老系统把连接字符串放在ini文件或环境变量里一旦密码包含特殊字符整个程序连不上查半天才发现是字符串语法问题。遇到这类“看起来像配置实际上是字符串”的场景先找驱动文档确认转义规则比自己试错高效得多。我自己有个习惯接触一个新语言或新数据库先建一张字符串行为对照表记录split时空字符串怎么处理、比较内容还是引用、转换失败抛异常还是返回0、中文按字节还是按字符。这张表不用很完整但能省掉大量排查时间。字符串之所以坑多是因为每门语言的历史包袱和默认行为各不相同表面API相近底下语义差着十万八千里。这篇part02把高频操作过了一遍下次再遇到字符串相关的问题希望能帮你少走几步弯路。
返回列表