py3 中文字符串对齐问题
一、综述
py3中str的len是计算字符数量,例如len(‘ab’) --> 2, len(‘a中b’) --> 3。
但在对齐等操作中,是需要将每个汉字当成宽度2来处理,计算字符串实际宽度的。
所以我们需要开发一个strwidth函数,效果: strwidth(‘ab’) --> 2,strwidth(‘a中b’) --> 4。
结论及推荐字符串域宽计算方法为:
def strwidth(s):
"""string width
中英字符串实际宽度
>>> strwidth('ab')
2
>>> strwidth('a⑪中⑩')
7
"""
try:
res = len(s.encode('gbk'))
except UnicodeEncodeError:
count = len(s)
for x in s:
if ord(x) > 127:
count += 1
res = count
return res
然后实现关键函数listalign用于处理某一列的对齐:
from align import listalign
ls = ['22', '哈哈', '中_文a']
print(*listalign(ls), sep='\n')
# 22
# 哈哈
# 中_文a
再利用listalign实现一个对齐二维数组的函数arralign:
from align import arralign
ls = [
[1, '22', 'c'],
[123, 4, '哈哈', 'cde'],
[1, '中_文a', 'dd']
]
print(arralign(ls))
# print(arralign(ls, chinese_char_width=1.8)),本篇文章为了显示对齐,要用汉字域宽1.8的参数
# 1 22 c
# 123 4 哈哈 cde
# 1 中_文a dd
完整代码: align.py。
二、实现方法分析
百度一下能找到一些方法资料,我们来研究(TiGuan)一下。
2.1 使用中文空格chr(12288)
沧海漂游_,Python 中英文混输格式对齐问题,CSDN,2017.8
这篇原理是对只有中文的字符串,填充的时候用域宽也是2的中文空格“chr(12288)”代替域宽为1的英文空格,从而实现只有中文的某一列的对齐效果。但这样遇到中英文混合字符串,例如把“清华大学”改成’a清华大学b’,就对不齐了。
2.2 字符集分类处理
mozaibin,python 中英文混合格式化输出对齐,鱼C论坛,2018.4
mozaibin应该是对编码做分类计算,感觉搞复杂了,而且算一个“⑩”就出错了,算出来是1,实际上是2。
2.3 判断ord是否大于127
云涛连雾,【Python】Python中中文的字符串格式化对齐,CSDN,2016.1
用ord计算每个字符的编码值,大于127的就是域宽2的字符,长度加1。
2.4 使用正则
用正则re.sub(r'[^\u0001-\u007f]+', r'', s)
删除中文字符,原字符串长度记为len1,删除后长度记为len2,可以用公式得到域宽: 2*len1 - len2。
2.5 使用gbk编码
冬雪雪冬,python 中英文混合格式化输出对齐,鱼C论坛,2015.12
利用gbk编码中每个汉字是2个字节的特点来计算:len(s.encode('gbk'))
。
我自己在用了一段时间正则后,也独立想到了这个方法。
但是这个方法在处理非gbk编码内的字符,例如“⑪ ⑫ ⑬”,会出错。
三、各方法比较分析
注意第三~五节里的代码仅做测试,实际代码以最终文件align.py为准。
3.1 正确性测试
先实现五种计算方法:len、分类、ord、正则、gbk
import re, time
def strwidth1(s):
"""py自带长度计算函数"""
return len(s)
def strwidth2(s):
"""
mozaibin写的是什么鬼。。。
http://bbs.fishc.org/thread-67465-1-1.html
测试结果:计算⑩等字符会出错
"""
widths = [
(126, 1), (159, 0), (687, 1), (710, 0), (711, 1),
(727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0),
(4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1),
(8426, 0), (9000, 1), (9002, 2), (11021, 1), (12350, 2),
(12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1),
(55203, 2), (63743, 1), (64106, 2), (65039, 1), (65059, 0),
(65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2),
(120831, 1), (262141, 2), (1114109, 1),
]
width = 0
for each in s:
if ord(each) == 0xe or ord(each) == 0xf:
each_width = 0
continue
elif ord(each) <= 1114109:
for num, wid in widths:
if ord(each) <= num:
each_width = wid
width += each_width
break
continue
else:
each_width = 1
width += each_width
return width
def strwidth3(s):
"""也有人想到算ord编号值即可
https://github.com/Jueee/PythonLiaoXueFeng/blob/master/81-Chinese.py
"""
count = len(s)
for x in s: # 我改成filter后反而变慢了,不知道是不是我姿势不对。
if ord(x) > 127:
count += 1
return count
def strwidth4(s):
"""最开始想到用正则处理"""
len1 = len(s) # 中文字符数 + 英文字符数 --> len1
s = re.sub(r'[^\u0001-\u007f]+', r'', s)
len2 = len(s) # 英文字符数 --> len2
return 2*len1 - len2
def strwidth5(s):
"""后来想到用gbk编码算长度就好了"""
return len(s.encode('gbk', errors = 'ignore')) # gbk好像跟gb2312效果没有差别?
正确性测试:
def test(s):
funcs = globals()
for i in range(1, 6):
print(funcs['strwidth' + str(i)](s), end = ' ')
print()
# 大部分文本,方法2~4都能正确计算出结果
test('広有射怪鳥事 ~ Till When?')
# 19 26 26 26 26
# 方法2计算'⑩'错误
test('⑩')
# 1 1 2 2 2
# 如果出现换行符,其实算宽度已经没有意义了。出现这种问题,应该是外部要有方法决定\n的显示问题
test('aa\ncc')
# 5 5 5 5 5
# 遇到非gbk编码内字符,gbk方法会出错
test('⑪')
# 1 1 2 2 0
3.2 效率测试
_startTimes = {}
def tic(key=0):
"""默认用第0个计时器,注意也可以使用非数字键值"""
global _startTimes
_startTimes[key] = time.clock()
def toc(prefix='', key=0, output=True):
"""返回时间秒数,保留4位小数"""
t = time.clock() - _startTimes[key]
if output:
print(prefix, '用时%.2f秒' % t)
return round(t, 4)
def test_speed(s):
funcs = globals()
for i in range(3, 7):
func = funcs['strwidth' + str(i)]
tic()
for _ in range(10**6): func(s)
toc(f'{i}:')
# 字符串越长gbk方法越快
test_speed('広有射怪鳥事 ~ Till When?')
# 3: 用时 1.55秒
# 4: 用时 1.40秒
# 5: 用时 0.77秒
# 英文较多的时候ord比正则快的多
test_speed('aa\ncc')
# 3: 用时0.43秒
# 4: 用时0.95秒
# 5: 用时0.72秒
# 字符串短的时候ord最快
test_speed('⑩')
# 3: 用时0.25秒
# 4: 用时0.85秒
# 5: 用时0.71秒
# 总得来说,各种算法还是比len函数慢了3~10倍
对方法5和方法3做一个封装,我们发现这是目前最鲁棒且性能最好的,后续采用的strwidth即以这里的strwidth6为准:
def strwidth6(s):
try:
return len(s.encode('gbk'))
except:
count = len(s)
for x in s:
if ord(x) > 127:
count += 1
return count
def test_speed(s):
tic()
for _ in range(10**6): strwidth5(s)
toc('5:')
tic()
for _ in range(10**6): strwidth6(s)
toc('6:')
test_speed('広有射怪鳥事 ~ Till When?')
# 5: 用时0.78秒
# 6: 用时0.55秒
test_speed('aa\ncc')
# 5: 用时0.73秒
# 6: 用时0.50秒
test_speed('⑩')
# 5: 用时0.72秒
# 6: 用时0.49秒
test_speed('⑪')
# 5: 用时0.72秒
# 6: 用时0.97秒
四、封装对齐函数
4.1 左对齐ljust、居中center、右对齐rjust
模仿str的成员函数ljust、center、rjust,重写3个对中文字符串做对齐操作的函数
def ljust(s, width, fillchar=' '):
"""
>>> ljust('a哈b', 6)
'a哈b '
"""
n = strwidth(s)
s = s + fillchar * (width - n)
return s
def center(s, width, fillchar=' '):
"""
>>> center('a哈b', 6)
' a哈b '
>>> center('a哈b', 7)
' a哈b '
"""
n = width - strwidth(s)
# 遇到奇数长度,这里有两种做法,这里采用尽量往左放的效果
s = fillchar * (n - n//2) + s + fillchar * (n//2)
return s
def rjust(s, width, fillchar=' '):
"""
>>> rjust('a哈b', 6)
' a哈b'
"""
n = strwidth(s)
s = fillchar * (width - n) + s
return s
4.2 每个汉字域宽为1.8的处理方法
在github和码云网站上,每个汉字的域宽并不是2,而是大概1.8的一个值!所以如下图,9个数字跟5个汉字域宽是差不多的。
为了让文本在这种环境下也能对齐,需要添加中文空格chr(12288)将字符串的域宽控制在整数,即保证字符串里汉字数量(含中文空格)始终是5的倍数。扩展的strwidthb函数如下,在计算一个字符串域宽的时候,也对原始字符串作了修改,返回两个值,第1个是修改后的字符串,第2个是修改后的字符串域宽。
def strwidthb(s, fmt = 'r'):
"""每个中文域宽为1.8字符时的处理方法
s:原字符串
fmt:目标对齐格式
返回值1:处理后的字符串s
返回值2:码云标准下的字符串宽度
>>> strwidthb('哈哈a') # 前面补3个中文不可见字符,并且认为这个字符串长度是10(不是11)
(' 哈哈a', 10)
"""
#1、计算一些参数值
s = str(s)
l1 = len(s)
l2 = strwidth(s)
y = l2 - l1 # 中文字符数
x = l1 - y # 英文字符数
ch = chr(12288) # 中文空格
#2、如果汉字数量不是5的倍数,则补足
if y % 5 != 0:
# 需要补充中文字符数
t = 5 - (y % 5)
if fmt == 'r': s = ch * t + s
elif fmt == 'l': s = s + ch * t
else: s = ch * (t - t//2) + s + ch * (t//2)
y += t
#3、计算每个汉字是1.8字符宽度的情况下的字符串宽度
l = round(y*1.8 + x)
return s, l
添加的中文空格位置跟字符串最终要进行的对齐格式有关,所以还多了一个fmt参数。
如果还有其他不同域宽的汉字,也可以采用这个原理进行处理。将域宽小数部分0.8转为分数4/5=a/b,确保汉字字符数是b的倍数,且每b个汉字只能算2*a个域宽。
五、ArrayAlign数组对齐功能
按照上述思想,完成完整代码align.py,整个程序的开发结构如图:
这里主要设计思想是listalign函数,可以对二维数组进行拆解成多个一维数组。
实现最终需要的接口函数:arralign,该函数有非常多灵活控制的功能,详见源码中的注释。
另外还实现了两个类似的功能Array2d、PrintAlign。
六、总结与展望
6.1 总结
本文记录了笔者在网上找了一些处理中文字符串对齐问题的资料后,综合分析得出的一些结论和对齐处理方法,包括github、码云里汉字域宽只有1.8的特殊处理方法。
结合鲁棒性和效率,推荐优先使用len(s.encode('gbk'))
保证效率,在gbk处理失败的情况再用ord计算域宽的混合方法来实现strwidth。
最后整合了一些功能,开发了用于二维数组对齐的arralign接口函数。
6.2 展望
- 研究pandas等表格库的中文对齐方法。(不过pandas好像也会有中文对不齐的情况)
- 因为暂时能解决笔者的问题和需求,没有再深入研究下去。一些代码细节应该还能再优化。