中文文件名排序跟拼音对不上:os.listdir 返回的是文件系统顺序,三个中文名按字节排结果正好相反

中文文件名排序跟拼音对不上:os.listdir 返回的是文件系统顺序,三个中文名按字节排结果正好相反

有一批中文命名的素材文件,放在同一个目录里,想让它们按拼音排好给前端列表用。我很自然地写了一句 sorted(os.listdir(path)),跑完发现顺序还是乱的------排了,但排得跟拼音没关系。

这篇就把这件事拆开:os.listdir 到底返回什么,sorted() 到底在比什么,拼音序又是另一套什么规则,为什么同一批名字按字节排出来跟拼音序正好反过来。

1 先看清:os.listdir 返回的不是排序结果

1.1 一个最小复现

三行代码就能看出问题。在临时目录里造三个中文名文件:

bash 复制代码
cd "$(mktemp -d /tmp/cn-sort.XXXXXX)" && touch 李四.txt 张三.txt 阿明.txt
python3 -c "import os; print(os.listdir('.'))"

本机实测(macOS 14.5 arm64,APFS)输出:

text 复制代码
['李四.txt', '张三.txt', '阿明.txt']

这正好是我 touch 的顺序,不是任何排序。原因很直接:os.listdir 是把目录的目录项(dirent)按文件系统给出的顺序读出来,它做的是「枚举」,不是「排序」。Python 文档从来没承诺过这个顺序,同一个目录在不同文件系统、不同时刻都会不一样。

os.listdir 返回的是目录项枚举顺序,随创建顺序变化,不是任何排序结果

这里有个容易忽略的点:连续跑几次顺序通常稳定,看起来像"有规律"。那只是当前文件系统的目录项排列稳定,换个目录、换个创建次序就变了,不能当契约用。

1.2 sorted() 比的是码点,不是拼音

那再加一层 sorted() 总该对了吧。也不对。Python 对字符串默认比较的是 Unicode 码点,而 UTF-8 是保序编码------码点大小顺序和 UTF-8 字节序列的大小顺序一致。所以对中文来说,sorted() 等价于「按 UTF-8 字节排」。

汉字在 Unicode 码表里的位置来自部首、笔画等历史整理顺序,跟读音没关系。所以 sorted(['张三','李四','阿明']) 得到的是字表顺序,不是拼音顺序。

sorted() 走 Unicode 码点序,对中文等于按字表位置排,与拼音序无关

很多人看到 sorted() 结果"看着像有点规律",就默认它等于拼音序,这是最容易踩的一处误判。

2 两套完全不同的规则

2.1 按字节排:只看编码,不看读音

「按字节排」是把字符串编码成 UTF-8,再逐字节比较。张 是 e5 bc a0,李 是 e6 9d 8e,首字节 e5 < e6,所以「张」排在「李」前面。这个规则绝对确定、跨平台一致,也正因为确定,它跟中文语义没有半点关系。

bytes 比较、sorted(os.listdir())、MySQL 的 utf8mb4_bin 排序,走的都是这一套。

2.2 按拼音排:需要额外的排序规则表

拼音序属于「语言相关排序」(collation):先知道这个汉字念什么,再按字母序比,后面还常会挂上声调、笔画做次级规则。这套规则不在 Python 内置里,得靠外部数据或系统 locale 提供:

  • pypinyin:把汉字转成拼音字符串再排,最可控,零系统依赖
  • locale.strcoll + 中文 locale:依赖系统 LC_COLLATE 的排序表,各平台行为不一致
  • ICU / PyICU:跨平台的完整 collation 实现,重量级

先记住一句话:字节序是「编码顺序」,拼音序是「读音顺序」,两套规则互不来往。

3 造一个反转:三个中文名,字节序和拼音序正好相反

光说没用,直接在机器上把反转造出来。选三个名字:李四 、张三 、阿明。

  • 拼音序:a(阿) < li(李) < zhang(张)
  • 需要验证字节序是不是整条反过来

下面这段脚本把三个名字、UTF-8 十六进制、两种排序结果全打印出来,收尾自动清掉临时目录,不留残留:

python 复制代码
# cn_sort_demo.py
import os, tempfile, shutil

names = ["李四.txt", "张三.txt", "阿明.txt"]

d = tempfile.mkdtemp(prefix="cn-sort-")
try:
    for n in names:                      # 故意按这个顺序创建
        open(os.path.join(d, n), "w").close()

    print("os.listdir 原序 :", os.listdir(d))

    print("\n[UTF-8 字节十六进制]")
    for n in names:
        print(f"  {n:10} -> {n.encode('utf-8').hex(' ')}")

    by_bytes = sorted(names, key=lambda s: s.encode("utf-8"))
    print("\n按字节序        :", by_bytes)

    from pypinyin import lazy_pinyin
    print("按拼音序        :", sorted(names, key=lazy_pinyin))

    print("\n原序 == 字节序 ?", os.listdir(d) == by_bytes)
finally:
    shutil.rmtree(d)

真实输出(本机实测,macOS 14.5 arm64,APFS,Python 3.14.3):

text 复制代码
os.listdir 原序 : ['李四.txt', '张三.txt', '阿明.txt']

[UTF-8 字节十六进制]
  李四.txt     -> e6 9d 8e e5 9b 9b 2e 74 78 74
  张三.txt     -> e5 bc a0 e4 b8 89 2e 74 78 74
  阿明.txt     -> e9 98 bf e6 98 8e 2e 74 78 74

按字节序        : ['张三.txt', '李四.txt', '阿明.txt']
按拼音序        : ['阿明.txt', '李四.txt', '张三.txt']

原序 == 字节序 ? False

把关键点摊开:

名字 首字 首字 UTF-8 拼音 字节序位置 拼音序位置
张三.txt 张 e5 bc a0 zhang 1 3
李四.txt 李 e6 9d 8e li 2 2
阿明.txt 阿 e9 98 bf a 3 1

字节序是 张三 → 李四 → 阿明,拼音序是 阿明 → 李四 → 张三。首尾整条调了个头,完全相反。

为什么会这样:张 的码点 U+5F20、李 的 U+674E、阿 的 U+963F,在码表里依次递增,所以字节序是 张 < 李 < 阿;而拼音是 a < li < zhang。码表位置由部首笔画整理出来,读音只有巧合才会对齐,整条反过来的例子一抓一大把。

顺带一句话提醒:os.listdir 原序这回正好等于字节序,纯属 APFS 上创建顺序跟码点顺序撞一起了,别拿这个当规律。上面测到的是 False 还是 True,取决于创建顺序,不能写进逻辑。

三个名字的字节序与拼音序对比,首尾整条反转

4 第三个坑:locale.strcoll 在 macOS 上的实际表现

想省掉 pypinyin 依赖,很多人会去用 locale.strcoll。它就是坑最多的那一块。

4.1 默认 locale 是 C,等于又退回字节序

不设 LC_COLLATE 时,locale.getlocale(locale.LC_COLLATE) 本机实测返回:

text 复制代码
(None, None)

这时生效的是 C locale,strcoll 做的是逐字节比较,中文排出来跟 sorted() 一模一样,白折腾。

4.2 setlocale 会「假装成功」

按官方文档的写法先设 locale:

python 复制代码
import locale, functools

locale.setlocale(locale.LC_COLLATE, "zh_CN.UTF-8")
print(sorted(names, key=functools.cmp_to_key(locale.strcoll)))

本机实测(macOS 14.5 arm64)这段不抛异常,setlocale 返回 'zh_CN.UTF-8',看着像成功。但排序结果还是:

text 复制代码
['张三.txt', '李四.txt', '阿明.txt']

换 zh_CN.GBK、zh_CN.GB18030、zh_CN.GB2312、zh_TW.UTF-8、zh_HK.UTF-8,本机实测结果全一样,还是字节序。locale -a 里这些名字都在,setlocale 也接受,但 macOS 的 libc 并没有为它们提供拼音 collation 规则,等于「接受了参数,什么也没做」。

这比直接报错更难查:在你的 Linux 上跑得好好的,挪到 Mac 上不报错、不崩溃,只是结果悄悄错了。

4.3 另一个坑:strcoll 不能直接当 key

locale.strcoll 是两参数函数 strcoll(a, b),直接写成 key=locale.strcoll 会抛:

text 复制代码
TypeError: strcoll expected 2 arguments, got 1

必须包一层 functools.cmp_to_key(locale.strcoll)。这一点在文档里不太显眼,sorted 只吃单参数 key。

4.4 一条命令自查本机

想确认自己机器是什么行为,一行够:

bash 复制代码
python3 -c "import locale,functools;n=['李四.txt','张三.txt','阿明.txt'];locale.setlocale(locale.LC_COLLATE,'zh_CN.UTF-8');print('ok');print(sorted(n,key=functools.cmp_to_key(locale.strcoll)))"

打印 ok 之后再看到 张三, 李四, 阿明 这个字节序结果,就是本机没有拼音 collation;如果打印出 阿明, 李四, 张三,才说明 locale 规则真的生效了。先跑这条再选方案,比事后排查省事。

5 稳定复用的写法

要中文按拼音排,固定用 pypinyin,别赌系统 locale:

python 复制代码
import os
from pypinyin import lazy_pinyin

def list_cn_sorted(path, suffix=".txt"):
    names = [n for n in os.listdir(path) if n.endswith(suffix)]
    return sorted(names, key=lazy_pinyin)

print(list_cn_sorted("/tmp/your-dir"))

本机实测(macOS 14.5 arm64,APFS)对上面那三个文件输出:

text 复制代码
['阿明.txt', '李四.txt', '张三.txt']

lazy_pinyin 返回拼音列表,列表之间逐元素比较,首字同音时自动比下一个字,最后才退回 Python 默认的码点比较,顺序是全确定的。要再加笔画或声调做次级规则,就在 key 里拼上第二段。

6 让异地同事一起验:把脚本临时挂到公网

这类「你这儿反转、我这儿没反转」的问题,光看截图很难说清。最省事的办法是让同事在自己机器上跑同一段脚本,把输出贴回来对照。

但对比的重点不是 os.listdir 原序,而是「字节序 vs 拼音序」这两列。文件系统不同,目录项顺序本来就不同------同一批名字在 ext4 和 APFS 上,os.listdir 原序几乎不会一致,比原序只会越比越乱。

本机起个临时服务:

bash 复制代码
mkdir -p /tmp/cn-sort-share && cd /tmp/cn-sort-share
python3 -m http.server 8765

另一个终端:

bash 复制代码
cpolar http 8765

cpolar 会给一个公网地址,同事浏览器打开就能拿到脚本和对照表。口径提前说清,免得到时候以为地址丢了是自己操作错了:

  • 免费版给的随机公网地址不固定,24 小时内不保证还是同一个域名,隧道重启会变;想给同一批人一个长期不变的入口,得用 cpolar 后台里固定二级子域名的档位。
  • 要直连固定 TCP 端口(比如 SSH 过去跑),免费版给的是随机端口,固定端口同样需要后台里对应的档位。

临时对一次结果,免费版完全够;对完把 cpolar 的隧道停掉,python3 -m http.server 一起 Ctrl+C 退掉。

7 总结

这件事已经交代清楚了:os.listdir 是枚举不是排序,sorted() 是码点序不是拼音序,再加上真正的拼音序,一共是几套互不相同的东西。用「李四 / 张三 / 阿明」三个名字,字节序排成 张 → 李 → 阿、拼音序排成 阿 → 李 → 张,首尾整条反转,UTF-8 十六进制和两种输出都在上面贴了实测原文。

关键三点:

  • 不要相信 os.listdir 的顺序,它随文件系统和创建顺序变,业务逻辑不能依赖它。
  • sorted() 对中文就是按 UTF-8 字节排,可复现、跨平台一致,但跟读音无关。
  • 要拼音序就上 pypinyin ;locale.strcoll 在 macOS 上会「接受参数但不生效」,还要求 cmp_to_key 包一层,能不用就别用。

同一段脚本在两台机器上跑出不同结果,是这类问题最常见的表象。把「字节序 / 拼音序」的边界划清楚,再让同事各自跑一遍,排序规则一次说清,后面做文件列表、素材归档、导出报表都不用再靠试。

相关推荐
Thanks_ks13 分钟前
【第 008 讲】在终端中运行 Python 程序:交互模式与脚本模式 | 绝对路径与相对路径 | 以模块方式运行 | 常见报错的识别与排查
python·异常处理·终端·脚本·命令行·cmd·python 解释器
威哥爱编程22 分钟前
模拟面试智能体开发实战 08:总评报告先过三道闸,防泄题防夸大防错建议,才敢发到候选人手上
python·agent·harmonyos
郝学胜-神的一滴27 分钟前
LangChain Skill 实现(渐进披露 On-Demand Skill,LangChain 官方 Deep Agent Skill 模式)
开发语言·python·langchain·numpy
投图匠11 小时前
十种不同风格的 CTP接口 框架
开发语言·python
2601_9679801112 小时前
深入理解词向量算法及其在Python中的实践
python·word2vec·glove·fasttext·词向量
129Lab13 小时前
电池循环测试数据自动化分析:用Python+AI工具构建一键式寿命评估流水线
python·数据分析·电池测试·数据自动化·寿命评估
treasure14013 小时前
【复古】浅尝Django前后端不分离
前端·css·python·django·bootstrap·html·web
Dmz丶14 小时前
前端转 AI 100 天 Day 44:前端集成实战——用 React 给 Agent 做个正式界面
python
jay神14 小时前
基于 YOLOv8 的手势识别系统(Web 版)
前端·人工智能·python·深度学习·yolo·计算机视觉·计算机毕业设计