按字母排序行

把文本一行一条拆开后按语言习惯排序:默认用 Intl.Collator 的 zh-Hans-CN 拼音序,也能切 UTF-16 码元序、英文 A–Z 与数字自然序,配合忽略空行、去重与升降序,输出和右侧四格统计、下方 16 行名次对照表同步刷新。

排序后行数16
输入行数(含空行)17
被忽略的空行1
被去掉的重复行0

输入 17 行:忽略 1 个空行、去重 0 行后参与排序 16 行;按「中文拼音序 zh-Hans-CN」升序输出,首行「北京」、末行「item2」。

排序规则对照表(16 行演示值,默认显示 12 行)
原行首字符码元码元序名次拼音序名次自然数序名次
bananaU+0062477
AppleU+0041166
cherryU+0063599
appleU+0061355
BananaU+0042288
dateU+006471111
elderberryU+006581212
figU+006691313
dateU+006471111
ÁppleU+00C11344
grapeU+0067101414
item10U+0069111516

表内 16 行取自输入框示例,名次为演示值;三列分别固定按码元序、zh-Hans-CN 拼音序、numeric 自然数序计算,不受上方选项影响,这样你能在同一张表里看出三种规则的差别。

使用方法

  1. 把要排的文本粘进上方输入框,一行一条:示例共 17 行,其中第 14 行是刻意留的空行,用来验证「忽略空行」开关的效果。
  2. 在「排序规则」里选口径。默认「中文拼音序(zh-Hans-CN)」会同时把中文按拼音、英文按字母排;想复现 JavaScript 不传比较函数时的行为,就选「UTF-16 码元序」。
  3. 按需勾选开关:默认开「忽略空行」与「忽略大小写与重音」;「去重」默认关,勾上后完全相同的行只保留第一次出现的那条。
  4. 点「开始排序」,或在输入框里直接按 Ctrl/Cmd+Enter。四格统计(排序后行数、输入行数、被忽略空行、被去重行数)与 16 行对照表会一起刷新。
  5. 看结果:「复制结果」把排好的文本放进剪贴板,「导出对照表 CSV」导出带 BOM 的名次表,「复制对照表」则按 TSV 贴给对方。

按字母排序行能解决什么问题

第一个场景是中文名单排序。把「上海、北京、深圳」三行交给不传比较函数的 sort(),得到的是「上海」在前——因为「上」的 UTF-16 码元是 U+4E0A,「北」是 U+5317,比较时只看码元大小,与拼音毫无关系。按字母排序行默认走 zh-Hans-CN,输出顺序是「北京、上海、深圳」,与汉语拼音 b、sh、sh 的先后一致。

第二个场景是带编号的字符串。「item10」和「item2」按字典序排会得到 item10 在前,因为字符「1」(U+0031)小于「2」(U+0032)。切到「数字自然序」后,连续数字被当成一个整体比较,输出 item2 在 item10 之前,版本号、章节号、章节编号这类数据才不会被排乱。

第三个场景是数据清洗前的规整。示例里 date 出现了两次、中间还夹着一个空行,先把「忽略空行」与「去重」打开,再排序,得到的就是 15 行干净数据,省得先手工删一遍再复制到表格里。

第四个场景是多语言混排。同一个「ä」在不同语言里位置完全不同:德语里它紧挨 a 之后,输出 apfel、äpfel;瑞典语里它排在 z 之后,输出 apfel、zebra、zzz、äpfel。给不同地区导出词表时,选对 locale 才不会把顺序排反。

本页只做「按整行内容排序」这一件事。要统计行数请用行统计,要把行顺序随机打乱请用打乱行工具,要专门去掉重复行请用删除重复行,要把字母统一成大写请用字母转大写——它们各自处理的正是本页不做的部分。

三种排序规则的原理和变量

第一层要说清的是函数。Array.prototype.sort() 不传比较函数时,会把元素转成字符串,再逐个 UTF-16 码元比大小,所以它实现的既不是拼音序也不是字母序,只是码元序。要按语言习惯排序,必须传比较函数:Intl.Collator(locale, options).compare。这个构造函数由 ECMA-402 定义,底层用的是 Unicode CLDR 的排序数据,属于 UTS #10 排序算法(Unicode Collation Algorithm)的本地化实现。

第二层是参数为什么这样取。locale 传死 'zh-Hans-CN' 而不留空,是因为留空会用宿主默认 locale:复算脚本实测同一份数据在默认 locale 下「Ápple」会跑到第 1 位,同一段代码在别人机器上结果就变了。sensitivity 取 'base' 的作用是把大小写与重音折叠掉——实测 compare('Apple','apple') 返回 0、compare('Ápple','apple') 也返回 0;若改成 'variant',前者的返回值变成 1,大小写就真正参与比较了。numeric 只在「数字自然序」这一档打开,让 'item9' < 'item10' 成立(关掉时反过来)。

第三层是中间量。文本先按 /\r?\n/ 切成行,再逐行去掉首尾空白(trim),接着按开关丢掉空白行、按开关去重(用 Set 保留首次出现),然后用 Collator 排序,最后 join('\n') 输出。首字符码元用 codePointAt(0) 读出并写成 U+XXXX,这一步只是让你在对照表里看见「码元序为什么排成这样」。三列名次的默认口径分别是:无参 sort、Collator('zh-Hans-CN')、Collator('zh-Hans-CN', { numeric: true })。

第四层是边界阈值。单行超过 5000 字符、去空行后总行数超过 20000 行都会被拦下:排序的比较次数上界是 n 乘 log2(n),10000 行要做约 132877 次比较,行数再多浏览器标签页会明显卡顿。numeric 比较在数字串长度超过 2 的 53 次方减 1(9007199254740991)之后不再可靠——复算里 compare('99999999999999999999','100000000000000000000') 返回 −1,原因是两边都被转成了同一个浮点近似值,想比超长数字串得自己写比较器。

第五层是常见误读。最大的一个误读是以为 sort() 会「自动按字母表排」,它其实只会按码元排,所以中文、重音字符、大小写混排时全部会错位;第二个误读是以为 localeCompare 的结果在所有环境一致,实际上不同 ICU 版本对个别字符的排序可能微调;第三个误读是以为「去重」按不区分大小写判定,本页的判定是「逐字符完全相同」,Apple 与 apple 算两行。

完整示例(数值仅作演示)

用输入框里的 17 行做演示:其中 1 行是空白行,非空行 16 行,去重前 date 出现 2 次。按默认设置(中文拼音序、升序、忽略空行、忽略大小写与重音、不去重)排序,得到 16 行,顺序是:北京、上海、深圳、Apple、apple、Ápple、banana、Banana、cherry、date、date、elderberry、fig、grape、item10、item2。注意中文整体排在拉丁字母之前,这是 zh-Hans-CN 的默认次序。

把规则换成「UTF-16 码元序」,同一份数据立刻错位:首行变成 Apple(U+0041 最小),「上海」升到第 14 名、「北京」落到第 15 名,因为 U+4E0A 小于 U+5317 而 U+4E0A 又大于拉丁字母;「Ápple」掉到最后一名(U+00C1 大于 z 的 U+007A)。这正是「按字母排序行」默认不用码元序的原因。

再验证两个开关:勾上「去重」,date 的两行合成一行,16 行变 15 行,统计格「被去掉的重复行」显示 1;把方向切到「降序」,首行变成 item2、末行变成「北京」。对照表里 item10 与 item2 的拼音序名次是 15 与 16、自然数序名次是 16 与 15——这一对互换就是 numeric 开关唯一改动的两行,其余 14 行名次完全一致。

输入范围与排序规则对照表

输入规模:每行最多 5000 字符,去空行后最多 20000 行,超出会在错误区点名第几行、多少个字符。下表把示例里的 16 行固定按三种规则各排一遍,名次越小越靠前。

16 行演示值在三种规则下的名次(码元序 / 拼音序 / 自然数序)
原行首字符码元码元序名次拼音序名次自然数序名次
bananaU+0062477
AppleU+0041166
cherryU+0063599
appleU+0061355
BananaU+0042288
dateU+006471111
elderberryU+006581212
figU+006691313
dateU+006471111
ÁppleU+00C11344
grapeU+0067101414
item10U+0069111516
item2U+0069121615
上海U+4E0A1422
北京U+53171511
深圳U+6DF11633

读表要点:拼音序与自然数序只有 item10、item2 两行互换,其余 14 行名次一致;码元序与拼音序没有任何一行名次相同,说明两者是两套完全不同的次序。表里出现两次 date,是去重前的原始状态,名次取靠后那次的位置。

边界与精度说明

空白行怎么算:只含空格或制表符的行也按空行处理;把「忽略空行」关掉,它们会参与排序并排在最前面,因为空格的码元 U+0020 比任何字母都小。末尾多出的换行符也会被当成一个空行——'b\na\nc' 按 /\r?\n/ 切出 3 个元素,'b\na\nc\n' 切出 4 个,多出来的那个空串正是它。

去重是逐字符完全相同的判定,不做 Unicode 规范化:带重音的 é 与「e 加组合重音符」在视觉上一样,但会被当成两行;同理 Apple 与 apple 也各占一行。如果确实想按「读起来一样」去重,先切到「忽略大小写与重音」只影响排序不影响去重,去重这一步仍按原字符比。

排序是稳定的,这一点从 ECMAScript 2019 起被语言规范强制要求:比较函数返回 0 的两行会保持输入里的先后。所以忽略大小写与重音之后,Apple、apple、Ápple 三行的相对次序就是它们在输入框里的次序,不会每点一次按钮就换一次位置。

数值精度只影响 numeric 档。数字串长度不超过 2 的 53 次方减 1 时比较结果可靠;超过之后浮点近似会让两个不同的超长数字算成相等,实测 compare('99999999999999999999','100000000000000000000') 返回 −1 而不是 1。金额、流水号这类超长数字,建议先把它们规整成等宽字符串再排。

导出格式:对照表 CSV 带 UTF-8 BOM,Excel 双击打开中文不乱码,数字列是纯数字可直接再算;「复制对照表」走 TSV,粘进 Excel 一格一格自动分列。结果文本本身不带 BOM,「复制结果」拿到的是干净的换行文本。

常见问题

为什么汉字排出来和纸质字典顺序不一样?

因为两套规则不同。纸质字典常用部首或笔画排,本页按汉语拼音的拉丁转写排,也就是 zh-Hans-CN 的 collation 次序。要按笔画排,得自己给每行加一个笔画数字前缀,再用「数字自然序」排,本页不内置笔画表。

我勾了「忽略大小写与重音」,Apple 和 apple 的顺序却没变,是没生效吗?

生效了,只是看不出来。sensitivity 取 base 后这两行的比较结果是 0,而 sort 是稳定排序,返回 0 就保持输入顺序,所以它们仍按你输入时的先后排列。想看出差别,可以对比「忽略」前后 Banana 与 banana 是否被拆散。

item10 为什么会排到 item2 后面,是不是排错了?

没有排错,这是数字自然序的正确结果。字典序按字符比,先比「1」再比「2」,所以 item10 在前;natural 序会把连续数字当成一个整数比,10 大于 2,于是 item2 在前。要看字典序的效果,把规则切成「UTF-16 码元序」即可。

排序后我的行内容好像被改动了?

只在一种情况下会变:每行首尾的空白会被 trim 掉,制表符与行尾多余空格不会出现在结果里。行内部的空格、标点、大小写全部原样保留。如果你要连首尾空白一起排,建议先把它们换成可比较的可见字符。

能不能按行长度排,或者按逗号后面的第二列排?

本页只按整行内容比较,不支持按字段或按长度排序。要按第二列排,先在别的工具里把第二列挪到行首、排完再挪回来;要按长度排,可以给每行加一个补零后的长度前缀再排,排完删掉前缀。

同一份数据在别的网站排出来为什么不同?

通常差在三个开关:locale(默认中文拼音序还是英文 A–Z)、sensitivity(大小写重音是否折叠)、numeric(数字是否自然序)。再加上不同浏览器内置的 ICU 数据版本可能略有差异,个别字符会有微调,所以跨环境核对时要把这三个开关写清楚。

上万行会不会卡住浏览器?

20000 行以内没问题:10000 行排序约做 132877 次比较,现代浏览器在一秒内完成。真正会拖慢的是把几万行一次性 render 到页面上,本页采用 textarea 输出而不是逐行 DOM,就是为了避开这个瓶颈。超上限时程序会明确提示而不是硬算。

排好的结果会保存或上传吗?

不会。整个排序只调用浏览器原生的字符串比较与 Intl.Collator,不发任何网络请求,刷新页面即回到示例状态。想留档就点「复制结果」或「导出对照表 CSV」,文件落在你自己的下载目录里。

相关工具