尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python3字符串全攻略:不可变性、编码与高效拼接避坑指南

Python3字符串全攻略:不可变性、编码与高效拼接避坑指南 做数据分析、写爬虫、用Django做后台甚至刷LeetCode的字符串题你几乎绕不开Python3的字符串。看着简单但真正上手你会发现坑比想象中多编码乱码、不可变对象带来的修改陷阱、循环拼接的效率问题每一项都能让你在线上环境里栽个跟头。这篇文章我不打算复述教科书上那套定义而是把平时处理字符串的实战经验、踩过的坑以及和其他语言对比后的体会一并整理出来给正在学Python3基础的朋友一份能直接照着用的操作手册。1. 字符串的本质Python3与其他语言的血缘和分岔1.1 不可变性为什么改字符串是不存在的操作先理解一个很多人忽略的底层设计Python3中的str是不可变对象。所谓不可变意思是字符串一旦创建出来它内部的内容就被定格了你没有办法像改C语言字符数组那样在原地把某个字符换掉。s hello # s[0] H # 这一行会直接报错 # TypeError: str object does not support item assignment我用一个生活类比来解释这事。Python的字符串就像石碑上刻好的文字想改内容不行只能另立一块新碑。而C语言里的char数组是黑板拿粉笔就能擦掉重写。这就是设计哲学层面的区别。那Python为什么偏要用不可变背后有三个很实在的理由第一多线程安全。多个线程同时读一个字符串对象不需要加锁因为它永远不变不会出现一个线程在改、另一个线程读到一半的情况。第二哈希稳定。字符串经常被当作字典的键键值需要一个稳定不变的哈希值才能准确定位。如果字符串可变作为键时被改掉字典就彻底乱了。第三内存优化。CPython解释器会对短字符串做驻留处理相同内容的字符串直接复用同一个对象省内存。如果字符串可变这种复用根本不敢做。这带来一个很实际的推论凡是看起来在修改字符串的操作底层其实都是生成新字符串。最常见的s aPython会创建一个全新的字符串对象把旧内容复制过去再追加a然后让s指向新对象。这在小字符串上没什么感觉一旦在循环里拼几百次性能会肉眼可见地变差后面我会专门讲这个问题。另外一个直接后果是如果想实现字符串的原地修改标准答案是把字符拆进列表改完再拼回来或者改用bytearray这种可变字节序列。s hello chars list(s) # [h, e, l, l, o] chars[0] H s .join(chars) # Hello这也是热词里python字符串直接赋值更改经常被搜的原因——很多人抱着一改就行的预期结果碰了壁。1.2 跨语言对照C/C/Java老手看过来搜字符串逆序输出c、cstring字符串结束符这类词的人多半是刚从C或者C转过来学Python的。这部分我专门做个对照帮你把脑子里的旧模型切换过来。语言字符串类型可变性底层表示结束符Cchar数组 / char*可变字节数组以\0结尾Cstd::string可变封装好的动态数组自动管理JavaString不可变char[] / byte[]无C#string不可变char[]无Python3str不可变视解释器实现无C语言里字符串以\0结尾所以strlen要遍历到字符串结束符为止这背后是字符数组思维。而Python的str没有结束符它自己记录长度你在切片时完全不需要关心什么终止符。C老手需要适应的点是std::string可以用下标直接改某个字符Python不行。Java和C#开发者反而会觉得很亲切因为大家同为不可变字符串只是Python的切片语法比Java的substring和C#的Substring用起来舒服得多。拿热词里提到的c#语言怎样截取字符串来对比。C#写的是string s hello world; string part s.Substring(0, 5);而Python是s hello world part s[:5]区别不只是写法短Python切片支持负索引和步长这是其他语言原生不提供的。你随手写一个s[::-1]就是字符串逆序在C语言里你得自己写循环交换字符。很多从C转过来的朋友第一次看到这行代码都直呼神奇。2. 三种类型一场戏str、bytes与bytearray2.1 str是文本bytes是字节别混着用Python3最让从Python2过来的人头疼的一点就是强制区分str和bytes。很多人刚学python3基础时被绕晕同样是一串字符为什么有时候打印出来是hello有时候是bhello简单理解str是给人看的文本它存储的是Unicode码点每个元素代表一个字符。bytes是给机器看的字节流每个元素是一个0到255之间的整数。两者之间的桥是编码和解码。s hello b s.encode(utf-8) # str转bytes得到 bhello s2 b.decode(utf-8) # bytes转str得到 hello日常开发中最常见的一个错误是在写二进制文件时把str直接传进去或者在写文本文件时传了bytes。比如# 报错a bytes-like object is required, not str # with open(data.bin, wb) as f: # f.write(hello)正确做法是把字符串编码成bytes再写入。反过来requests库抓网页时response.content返回bytesresponse.text则帮你按推断的编码解码成了str。两个都能用但代表的东西完全不同一个是字节一个是文本。bytes还有一点需要注意它的切片切出来的还是bytes而如果你把bytes放进一个字符串的拼接操作里类型会当场爆炸。我在实际项目里见过很多次这种报错归根结底是对两类对象缺乏边界感。如果需要一个可变的字节序列就用bytearray。它在文件分块处理、协议组包场景下很常用因为它支持索引赋值可以原地修改ba bytearray(bhello) ba[0] ord(H) print(ba) # bytearray(bHello)2.2 编码转换Unicode、UTF-8与GBK的落地指南编码这块是字符串处理里最大的泥潭你搜python查找excel中字符串、cstring字符串结束符这些词时背后往往就藏着一个乱码问题。先理清概念。Unicode是一本字符户口本给全世界的字符都分配了一个唯一的码点。而UTF-8、UTF-16、GBK这些是存储方案解决的是怎么把这串码点写到磁盘上、传进网线里。同一个你字在Unicode里的码点是固定的但用UTF-8存是3个字节用GBK存是2个字节。s 你好 print(len(s)) # 2这是字符数 print(len(s.encode(utf-8))) # 6UTF-8编码后的字节数 print(len(s.encode(gbk))) # 4GBK编码后的字节数乱码的根源永远只有一个写入时的编码和读取时的编码不是同一套。你在Windows上一个GBK的txt文件拿到Linux上按UTF-8读必然出乱码。实操建议很直接项目内所有文件统一用UTF-8保存没有例外。用open打开文件时明确指定encoding参数不要依赖系统默认值。用open打开文件时明确指定errors参数不要依赖系统默认值。with open(data.txt, r, encodingutf-8) as f: content f.read()如果读取时遇到解码错误Python默认抛UnicodeDecodeError程序直接崩溃。你可以用errorsignore跳过坏字节或者用errorsreplace把它替换成特殊字符。我的经验是ignore会静默丢数据replace至少让你知道这里有异常调试阶段用replace更靠谱。还有一个绕不开的坑某些老旧系统还在用GBK输出内容爬虫抓取时网页却声明的是UTF-8。这时候可以通过chardet库去检测字节流的编码再用检测结果去decode。import chardet data b\xc4\xe3\xba\xc3 # 一段未知编码的样本 encoding chardet.detect(data)[encoding] # 大概率是 GBK text data.decode(encoding)2.3 切片和索引最容易踩错的边界Python字符串支持索引和切片这是你最常用也最容易错的地方。索引从0开始负数从右往左数s[-1]是最后一个字符。切片s[start:end]是左闭右开区间——包含start位置不包含end位置。s hello world print(s[0]) # h print(s[-1]) # d print(s[1:5]) # ello print(s[:5]) # hello取前5个字符 print(s[6:]) # world print(s[::-1]) # dlrow olleh逆序输出热词里字符串截取前两位这个需求写成代码就是s[:2]。记住左闭右开这个约定你就会明白为什么s[0:2]长度为2而不是3。切片有一个好脾气和一个坏脾气越界时它不报错而是自动截断到边界但单索引越界时会直接抛IndexError。s abc print(s[100:]) # 不报错返回 # print(s[100]) # 报错 IndexError: string index out of range写代码时我会尽量用切片而非手写边界判断因为切片本身就帮你处理了溢出。比如取出URL最后一个路径段用s.rsplit(/, 1)[-1]能省下很多防御性代码。3. 高频操作的效率与正确姿势3.1 拼接不是小事号、join与f-string的实测对比字符串拼接算是所有Python开发者的日常操作但很少有人考虑过性能差异。前面讲了str不可变所以每次用拼接解释器都要重新分配内存、复制旧内容、追加新内容。循环次数多了这个成本是灾难级的。我写过一个测试循环拼接一万次短字符串import timeit def concat_with_plus(n): s for i in range(n): s str(i) return s def concat_with_join(n): return .join(str(i) for i in range(n)) print(timeit.timeit(concat_with_plus(10000), globalsglobals(), number100)) print(timeit.timeit(concat_with_join(10000), globalsglobals(), number100))实测下来join的耗时比号少了接近一个数量级数据量越大差距越明显。原因不难理解号每次拼接都产生一个全新的中间字符串而join把最终大小一次性分配好每个字符串只写入一次。结论是固定数量的少量拼接用哪个都无所谓循环里累计字符串必须用列表收集再用join如果只是把几个变量塞进一句话里f-string是最优雅高效的。# 推荐 parts [] for item in items: parts.append(process(item)) result .join(parts) # 不推荐 result for item in items: result process(item)3.2 分割、替换、去空白这些API的脾气你得摸清split和replace是处理文本的两大主力但它们有几个不易察觉的细节。split不传参数时会按任意空白字符分割并且把连续的空白当作一个分隔符处理。而split(,)是精确匹配逗号连续逗号会产生空字符串元素。line hello world\tpython print(line.split()) # [hello, world, python] print(a,,b,,c.split(,)) # [a, , b, , c]很多初学者在这上面栽过跟头按空格分割却得到了空串或者想按逗号分割却用了默认参数。如果你想要处理连续分隔符的效果最稳妥的办法是先split(,),再用filter过滤空字符串或者直接用正则里的re.split配合量词。replace默认替换全部匹配项也可以传第三个参数限制替换次数。s one and two and three print(s.replace(and, )) # one two three print(s.replace(and, , 1)) # one two and threestrip系列是处理脏数据的好帮手。strip()去掉两端空白lstrip()去左边rstrip()去右边。它们不仅能去空格还能去换行、制表符甚至自定义要去掉的字符集。我在清洗CSV文件时经常这么干raw 商品名称, 价格 cleaned raw.strip().strip().strip() print(cleaned) # 商品名称, 价格连续调两个strip看着不优雅但效果很直白先去掉外层空格再去掉单引号最后再清理一次内部的边缘空格。比写一个正则快也更容易读。3.3 格式化三兄弟%、format与f-string怎么选Python3里给字符串填充变量有三套写法新老交替容易让人困惑。最早的是%格式符从C语言沿袭来的老古董但现在依然能用name alice score 95.678 print(%s scored %.1f % (name, score))接着是str.format方法用花括号占位print({} scored {:.1f}.format(name, score))最后是Python 3.6引入的f-stringprint(f{name} scored {score:.1f})我的建议是Python 3.6以上的项目无脑用f-string。它最短、最直观而且能直接写表达式这在拼复杂字符串时优势明显。比如f{user.name.upper()}: {total_price / count:.2f}format那套得写成传参形式可读性差一大截。手机上写Python3比如用python3手机版跑小脚本f-string同样支持只要Python版本是3.6以上就没问题。唯一要注意的是如果你维护的是老项目比如还在Python 3.5甚至2.7的环境那f-string会直接SyntaxError只能用format兜底。4. 正则表达式实战从日志里救命的那招4.1 re模块的核心方法速览字符串方法只能处理规则简单的场景一旦想匹配以数字开头、后面跟着字母或-号这种模式就必须上正则。Python3的re模块提供了几个核心方法先把它们的关系理清。re.match只从字符串开头尝试匹配re.search会在整个字符串中搜索第一个匹配位置。这是最容易混淆的一对。import re text 订单号: 2024-0519-001 print(re.match(r\d, text)) # None因为开头是订 print(re.search(r\d, text)) # 匹配到2024re.findall返回所有匹配项的列表re.finditer返回迭代器适合处理大量匹配。re.sub做正则替换re.split则允许你用正则作为分隔符。print(re.findall(r\d, a1b22c333)) # [1, 22, 333] print(re.sub(r\s, _, a b c)) # a_b_c print(re.split(r[,;], a,b;c)) # [a, b, c]正则里我最推荐用的一个技巧是命名组。普通分组通过match.group(1)按数字索引取值命名组则可以直接用组名pattern r(?Pdate\d{4}-\d{2}-\d{2}) m re.search(pattern, 今天是2024-05-19) print(m.group(date)) # 2024-05-19日志字段一多命名组会让代码可读性直线上升不用数着第几个括号。4.2 实战日志清洗和字段提取的全流程把前面所有知识串起来我写一个完整的日志处理案例。假设你有一个服务日志每一行长这样2024-05-19 10:32:15 [INFO] pay-service: user_id1024, productSKU-8899-01, price299.00 2024-05-19 10:33:02 [ERROR] pay-service: user_id1024, productSKU-8899-02, price51.50目标是提取日期、时间、日志级别、user_id、product、price并统计不同产品的总销售额。先读文件注意编码问题然后逐行解析import re pattern re.compile( r(?Pdate\d{4}-\d{2}-\d{2}) r(?Ptime\d{2}:\d{2}:\d{2}) r\[(?Plevel\w)\] ruser_id(?Puser_id\d), rproduct(?Pproduct\S), rprice(?Pprice\d\.?\d*) ) sales {} with open(app.log, r, encodingutf-8, errorsreplace) as f: for line in f: m pattern.search(line) if not m: continue data m.groupdict() price float(data[price]) product data[product] sales[product] sales.get(product, 0) price for product, total in sales.items(): print(f{product}: {total:.2f})这里面有几个设计细节值得说。一是用re.compile预编译循环里反复用同一模式时性能更好。二是pattern.search而不是match因为日志行可能带前导空格。三是open文件时同时指定encoding和errors真实日志文件里总有不规范字节直接崩溃的话排查成本太高。跑完这段代码你会得到一个干净的汇总结果。这其实就是字符串处理三板斧的完整演练读取时处理编码解析时用正则汇总时用字典和格式化输出。日常的数据清洗任务十有八九是这三步的组合。5. 常见问题排查速查表5.1 字符串与数字互转边界条件比你想的多字符串转数字和数字转字符串在热词里出现了好多次而且覆盖了C、C#、SQLServer等一堆语言可见这是个通用痛点。Python3里最简单的是int()和float()i int(42) # 42 f float(3.14) # 3.14 s str(42) # 42但真实数据根本不会这么规整。用户输入了 42 带空格或者4,200带千分位直接int()就会崩溃。我的处理习惯是写一个安全转换函数def to_int(raw, default0): if raw is None: return default try: return int(str(raw).strip().replace(,, )) except ValueError: return defaultstrip去空格、replace去掉千分位逗号、try/except兜底。这样一来脏数据最多让你的结果退化成默认值而不会让整个流程异常中断。另一个容易忽略的点是浮点精度。涉及金额计算的场景直接用float做累加可能会得到299.00000000000006这种结果因为二进制浮点数无法精确表示某些十进制小数。这时候应该用decimal.Decimalfrom decimal import Decimal price Decimal(299.00)热度词里还有个sqlserver 字符串转数字数据库场景下转错一个字段可能导致全表排序失效。Python这边同理批量处理数据前先跑一遍安全转换函数能省掉后面大量Debug时间。5.2 截取、逆序、比较高频需求的标准答案这三个都是送分题级别的需求但有些细节值得记一下。截取前两位直接s[:2]。截取后两位s[-2:]。几乎所有类似需求都可以用切片解决你只要记住区间是左闭右开就够了。逆序输出Python一行s[::-1]搞定。如果你看到热词里字符串逆序输出c、字符串逆序c语言pta那多半是C语言课设题要在Python里实现一行代码的事而已。比较字符串是否相等用不要用is。比较的是内容is比较的是内存地址。这里有个经典陷阱a hello world b hello world # 实际上会直接拼接成同一个字符串 print(a b) # True print(a is b) # False 或 True取决于解释器行为和字符串长度短字符串会被CPython驻留所以is可能返回True这让人误以为is也能用。换成运算产生的长字符串两行的地址就可能不一样。规则很简单判断内容一律用判断是否为同一个对象才考虑is。C#里的string.CompareTo、Java里的equals也是类似场景下的不同答案但最终原则一致内容比较不比较引用。5.3 字符类型判断与其他语言迁移者的坑判断字符串里是否只含字母和数字是个常见需求Python内置了几个判断方法但它们的边界很隐蔽。isalnum()检查是否只含字母和数字isdigit()检查是否都是数字isalpha()检查是否都是字母isdecimal()更严格只认十进制数字。print(abc123.isalnum()) # True print(123.isdigit()) # True print(12.3.isdigit()) # False小数点不算数字最容易踩的坑是中文。在Python3里你好.isalpha()返回True因为Unicode把汉字归类为字母。如果业务上要求只允许英文字母光靠isalpha就不够得再加一层ASCII判断def is_ascii_alpha(s): return bool(s) and all(a c.lower() z for c in s)另一个关联热词是字符串字母大小写转换。upper()和lower()是全角转换swapcase()可以把大小写互换。如果你只想让首字母大写用capitalize()如果想每个单词首字母都大写用title()。做一个字符串判断是否驼峰并转换成下划线风格的函数也很常见import re def camel_to_snake(s): # 在边界加上下划线例如 abcDefGI - abc_def_gi return re.sub(r(?[a-z0-9])(?[A-Z]), _, s).lower() print(camel_to_snake(salesOrderCount)) # sales_order_count def snake_to_camel(s): return .join(word.capitalize() for word in s.split(_)) print(snake_to_camel(sales_order_count)) # SalesOrderCount字符串排序也是个高频话题。sorted默认按Unicode码点排序这导致大写字母会排在小写字母前面Banana会排在apple前面。想让排序符合人类直觉可以传keystr.lowerfruits [apple, Banana, cherry] print(sorted(fruits)) # [Banana, apple, cherry] print(sorted(fruits, keystr.lower)) # [apple, Banana, cherry]最后提一个很多人搜ida显示中文字符串时遇到的现象IDE或终端里显示字符串乱码很多情况下是控制台编码和文件编码不一致。Windows命令行默认GBK文件却是UTF-8打印中文就乱。在代码里强制设置PYTHONIOENCODINGutf-8或者把标准输出重定向为UTF-8通常就能解决。遇到这种问题先别怀疑字符串处理逻辑先检查编码链路是不是闭环的。字符串处理这门功夫说到底不是背多少API而是建立两套思维模型一套是文本与字节的分界知道什么时候该用str、什么时候该用bytes一套是不可变对象的代价知道哪些写法优雅但昂贵哪些写法土气但高效。把这两套模型刻进脑子再回头看C语言里的\0、C的std::string、Java的String你会发现自己已经能从一个更高的维度看待字符串这个老朋友了。最后分享一个小技巧当你需要把几百个字符替换成另外几百个字符时别写一大串replacePython的str.translate配合maketrans可以批量完成而且速度飞快。我处理过一份脏数据里面有各种全角标点、特殊空格、乱码字符用translate一张映射表全部搞定代码还不到十行。这种备好工具再干活的习惯会让你的字符串处理之路顺很多。
返回列表