
熟悉編程的朋友應該不難理解,為什么字串排序"10"會排在"2"的前面,因為字串大小比較是對各字符的編碼值逐個進行比較,“1”<“2”,所以"10"<“2”,
不過這不是本文想要達到的目標,本篇文章會用一行代碼,讓文本中出現的(任意多組)數字序號可以按照數字順序進行排序,
〇、背景
最近在爬蟲某漫畫網站,帖子標題一般是這樣的格式:
[作者] 標題標題 1 [55P]
[作者] 標題標題 2 [64P]
[作者] 標題標題 3 [75P]
……
[作者] 標題標題 8 [47P]
[作者] 標題標題 9 [44P]
[作者] 標題標題 10 [50P]
[作者] 標題標題 11 [44P]
[作者] 標題標題 12 [43P]
[作者] 標題標題 13 [44P]
……
我按照每篇帖子的標題作為檔案夾名,將帖子里的所有圖片下載到了對應的目錄,
本地保存的結果是這樣的:

在資源管理器中,這些數字順序的檔案夾都得到了“正確的”排序,
但是因為專案需求,我希望在程式中處理這些檔案夾時,卻出現了文章一開頭出現的問題——10、11、12全都跑到了1的后面、2的前面,這是不符合我的預期的,
[作者] 標題標題 1 [55P]
[作者] 標題標題 10 [50P]
[作者] 標題標題 11 [44P]
[作者] 標題標題 12 [43P]
……
[作者] 標題標題 19 [36P]
[作者] 標題標題 2 [64P]
[作者] 標題標題 20 [33P]
[作者] 標題標題 21 [36P]
[作者] 標題標題 22 [33P]
[作者] 標題標題 23 [35P]
……
一、正則取出序號位置進行排序
如果文章的序號始終出現在同樣的位置,那么用正則很容易將這個位置的序號“取出來”,轉換為數字型別,然后利用sorted函式的第2個引數key進行排序即可,
比如本例可以用這樣的代碼進行排序:
folders = sorted(folders, key=lambda s: int(s.split()[-2]))
但是這樣的演算法并不夠“干凈”也不夠“通用”,比如當文本中沒有數字就會報錯,或者數字不在規則設定的位置、或者有多組數字,也無法正確排序,
關鍵是Windows的資源管理器輕易就能做到的事情,為什么Python的排序就是不行呢?

二、微軟的排序策略
生成測驗樣例代碼:
for i in range(1, 20, 4):
for j in range(1, 20, 4):
with open(('abc%dxyz%d_extension'%(i, j))[:12], 'w'):
pass
在檔案瀏覽器中查看排序效果,檔案名可以按照數字大小進行排序,包含多組數字也能正確排序:

更加驚人地,我嘗試使用不同的ASCII碼生成檔案名,查看排序策略,其實微軟實作了更加復雜的排序方式:
生成測驗樣例代碼:
for i in range(1, 256):
try:
with open(chr(i), 'w'):
pass
except:
pass
微軟的排序策略:

微軟的排序囊括了?字符、注音字符、拉丁文等特殊字符,也按照了一定規律進行排序,
(然而卻有人吐槽這一點,甚至希望把這個“Bug”去掉: Windows按名稱排序問題)
那么微軟是如何實作排序邏輯的呢?一開始我考慮是不是將0-9的編碼定義得比其他字符優先級更低,但是這樣可以實作"11">“1a”,卻不能解釋為什么"a1">“11”,

因此,微軟一定是“貼心”地提取了檔案名中出現的所有數字,然后再按照從左到右的序列依次排序,最終得到排序結果,
(然而我看到這篇帖子,才發現我又重復研究輪子了:Java實作Windows中的檔案名排序)
三、Python實作排序
既然沒有捷徑可走,那就想辦法把所有的“數字”和“非數字字符”(不僅是英文)全部提出來,
比如目標字串,目標達到這樣的效果:
'he11owor1d' -> ['he', 11, 'owor', 1, 'd']
可以使用正則運算式進行匹配,不過正則運算式中,"\d+"只能匹配數字,"\D+"只能匹配非數字,"(\D+)(\d+)"可以取出所有的數字和非數字,但是卻匹配不到字串首的數字和字串尾的非數字,
為了解決這一點,手動在串首和串尾分別增加一個英文和數字,就可以讓原本字串內的所有內容全部符合正則規則并匹配到,最后再刪掉頭尾就可以了,
(當然,頭尾統一增加一個字符并不會影響排序順序,所以這兩個字符也可以不刪)
正則運算式匹配:
s1 = re.findall(r'(\D+)(\d+)', 'a' + s + '0')
輸出:
[('ahe', '11'), ('owor', '1'), ('d', '0')]
回傳的結果是二維陣列,通過sum函式組裝成一維陣列:
s2 = sum(s1, ())
輸出:
('ahe', '11', 'owor', '1', 'd', '0')
這個很秀的操作是使用了sum函式的第2個控制引數:
sum(iterable, start=0, /)
Return the sum of a ‘start’ value (default: 0) plus an iterable of numbers
When the iterable is empty, return the start value.
This function is intended specifically for use with numeric values and may reject non-numeric types.
將陣列中表示數字的字串轉化成數字,寫成串列遞推式可以用一行代碼寫出來:
s3 = [int(s) if s.isdigit() else s for s in s2]
輸出:
['ahe', 11, 'owor', 1, 'd', 0]
接著將這個陣列回傳,作為sorted函式的第2個引數key的回傳作為排序判斷,
當對串列內的字串進行排序時,會按照傳入key的排序函式進行計算,分別計算得到的結果(每一個字串對應一個陣列),對結果進行排序,
陣列的比對就是各元素逐一比較大小了,這樣可以得到對字串預期的排序結果,
包含生成測驗樣本的代碼:
import os
import re
# 生成測驗資料集
for i in range(1, 20, 4):
for j in range(1, 20, 4):
with open(('abc%dxyz%d_extension'%(i, j))[:12], 'w'):
pass
# 自定義排序函式
def filenamesort(s):
s1 = re.findall(r'(\D+)(\d+)', 'a' + s + '0')
s2 = sum(s1, ())
s3 = [int(s) if s.isdigit() else s for s in s2]
return s3
# 排序函式對含有數字的字串的計算結果測驗
s = 'he11owor1d'
print(filenamesort(s))
# 原始順序和自定義排序結果
print(sorted(os.listdir()))
print(sorted(os.listdir(), key=filenamesort))
生成樣本和輸出排序結果:

第2段是Python對串列的默認排序,第3段是使用我的自定義排序結果,可以看到結果符合預期,
但是微軟做到的對?和注音標記等符號也進行了排序,這個就不在我的需求范圍內了,
四、一行代碼排序
當然,你還可以把這些代碼繼續簡化,用一行代碼來寫出來:
print(sorted(os.listdir(), key=lambda s: [int(s) if s.isdigit() else s for s in sum(re.findall(r'(\D+)(\d+)', 'a'+s+'0'), ())]))
嗯,所以我不是標題黨,
INF、后記
后來經小小明大佬的指點,代碼還可以進一步簡化:
print(sorted(os.listdir(), key=lambda s: [int(s) if s.isdigit() else s for s in re.findall(r'\D+|\d+', 'a'+s+'0')]))
INF、后后記
后來又發現可以再簡化短一點:
print(sorted(os.listdir(), key=lambda s: sum(((s, int(n)) for s, n in re.findall(r'(\D+)(\d+)', 'a%s0'%s)), ())))
這個方法減少了n次str.isdigit()函式的運算,所以理論上還會更快一些,
另一方面為了增加代碼的復用性,還可以寫成這樣的兩行:
fns = lambda s: sum(((s,int(n))for s,n in re.findall('(\D+)(\d+)','a%s0'%s)),())
print(sorted(os.listdir(), key=fns))
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/277670.html
標籤:python
