本帖最後由 rictirse 於 2012-11-5 23:23 編輯
這問題大概只存在於中小型專案以上,雖用 AutoIt 完成,實際費時不少,但這問題本身就必須花很多時間,所以大概不會有人繼續在意效能上的問題。
首先必須先定義這問題之「列舉」與「深入列舉」 (恕小弟避開了 DFS / BFS 之定義說明 )。
「列舉」指的是,給定一個目錄 (假設 C:),會列舉出 C: 底下所有指標之檔案(或資料夾),
但注意的是,如果 C: 底下還有其它資料夾,如 C:Folder1、C:Folder2,便不再進去找其他檔案。
「深入列舉」和上面說的「列舉」,只差在會深入裡面的 C:Folder1、C:Folder2 做深度搜尋。
這篇文章主要分三大部份探討。
1. 列舉目前之硬碟 / 磁碟槽 : 主要摘自 help 範例加以變化
2. 如何列舉某個目錄下之目錄 / 檔案: 主要摘自 help 範例加以變化
3. 如何深入列舉某個目錄下之目錄 / 檔案 : 沒學過遞迴 (recursive) 可能不知該如何下手,本文將予以範例
4. 建立比 _FileToArray 方便使用之副函式 : 為了日後開發方便,包裝成較易呼叫、使用之函式。
5. 進階議題: 由於筆者時間有限,本打算寫一份完整的 ScanFolder.au3,但開發時間估計不少 (因估計可以包的函式大概十項左右),在此紀錄一些進階議題,對於進階議題有興趣的可以先自行實作,若做不出來,小弟有時間的話會試試看。
因本文實在太長,part 2 部份是有機會、有空的話,會把自己寫的,易於維護、利於開發的 header 補足放上。
小弟學淺,對本篇有經驗、意見或補充,歡迎指教,在此先行感激。
1. 列舉目前之硬碟 / 磁碟槽
最舉目前之硬碟 / 磁碟槽只有一個指令:DriveGetDrive ( "type" ),
其中 "type" 包含了 "ALL", "CDROM", "REMOVABLE", "FIXED", "NETWORK", "RAMDISK", "UNKNOWN"。
以下範例是列舉目前電腦看得到之磁碟槽 (摘自 help)。[code=autoit]
Local $aArray = DriveGetDrive("ALL")
If @error Then
; An error occurred when retrieving the drives.
MsgBox(4096, "DriveGetDrive", "It appears an error occurred.")
Else
For $i = 1 To $aArray[0]
; Show all the drives found and convert the drive letter to uppercase.
MsgBox(4096, "DriveGetDrive", "Drive " & $i & "/" & $aArray[0] & ":" & @CRLF & StringUpper($aArray[$i]))
Next
EndIf
[/code]注意 "type" 非常重要,蠻多人直接使用 "ALL" 去取得磁碟槽,但如果要做「深入列舉」+ 「寫入」時,
事實上 "CDROM" 是可以直接略過不記的,
若需要用的可能只有 "REMOVABLE"、 "FIXED",其它的都不想進行輸出/處理,該怎辦?
基本上目前 AutoIt 還沒辦法做到這地步,必須做一點技巧性修改。
第一種方法是,先做 DriverGetDriver("ALL"),再對每個取得之 driver 做 DriveGetType,判斷是何種 type。[code=autoit]
Local $aArray = DriveGetDrive("ALL")
If @error Then
MsgBox(0, "Error", "DriveGetDrive Error")
Else
For $i = 1 To $aArray[0]
; MsgBox(0, "", StringFormat("Drive : ""%s"" Type : %s", StringUpper($aArray[$i]), DriveGetType($aArray[$i])))
$Type = DriveGetType($aArray[$i])
If $Type="Fixed" Then
MsgBox(0, "", StringFormat("Get Fixed : %s", $aArray[$i]))
ElseIf $Type = "Removable" Then
MsgBox(0, "", StringFormat("Get Removable : %s", $aArray[$i]))
EndIf
Next
EndIf
[/code]第二種方法是,分兩次列舉,也就是先做 DriveGetDrive("REMOVEABLE"),再做 DriveGetDrive("FIXED")。[code=autoit]
Dim $Type[2] = [ "FIXED", "REMOVABLE"]
For $i = 0 To UBound($Type)-1
Local $aArray = DriveGetDrive($Type[$i])
MsgBox(0, "", "Driver Type : " & $Type[$i])
If @error Then
MsgBox(4096, "DriveGetDrive", "It appears an error occurred.")
Else
For $j = 1 To $aArray[0]
MsgBox(4096, "DriveGetDrive", "Drive " & $j & "/" & $aArray[0] & ":" & @CRLF & StringUpper($aArray[$j]))
Next
EndIf
Next
[/code]個人較偏向方法 2,理由是,一定會先舉出 "FIXED",再取出 "REMOVABLE",且要增加 / 減少 Type 時,直接從 $Type 做修改即可。
2. 如何列舉某個目錄下之所有資料夾/檔案
這裡講的是,不會往子資料夾裡繼續找,要往子資料夾裡繼續找在下個 section 會提。
要列舉某個目錄下之所有資料夾 / 檔案並不困難,其中 AutoIt 之 File.au3 已經有提供一份函式:
_FileListToArray($sPath [, $sFilter = "*" [, $iFlag = 0]])
$sPath 是欲列舉之路徑,$sFilter 是欲列舉之檔名型態,可用 * ? 替之;
關鍵在於 $iFlag,0 代表同時傳回檔案與資料夾,1代表只傳回檔案,2代表只傳回資料夾。
要注意的是 Return Value,正常成功時,ret[0] 是傳回數量,ret[1]...ret[n] 是內容;
失敗的話會傳回0,想知道原因必須去看 @error,@error=4 代表沒檔案,@error=1 代表找不到資料夾
一份 demo code 如下 (摘自 help)[code=autoit]
#include <File.au3>
#include <Array.au3>
Local $FileList = _FileListToArray(@DesktopDir)
If @error = 1 Then
MsgBox(0, "", "No Folders Found.")
Exit
EndIf
If @error = 4 Then
MsgBox(0, "", "No Files Found.")
Exit
EndIf
_ArrayDisplay($FileList, "$FileList")
[/code]但事實上打開 File.au3 ,查看 _FileListToArray,也只是把 FileFindFirstFile/ FileFindNextFile 這兩個函式封裝起來,( 這兩個函式推斷應該是接間調用 Win32 API : FindFirstFile 與 FindNextFile而來 ),換句話說,如果列舉之 Filter 為 *.* 時,其實自己直接用 FileFindFirstFile / FileFindNextFile 去做效能還比較高!
假設要列舉 D:Code Document 底下所有檔案 資料夾,原始碼如下 (修自 Help)[code=autoit]
; Shows the filenames of all files in the current directory.
Local $Filter = "D:Code Document*.*" ; 注意,不可以只寫 D:Code Document!
Local $search = FileFindFirstFile($Filter)
Local $Output, $i = 1
; Check if the search was successful
If $search = -1 Then
MsgBox(0, "Error", "No files/directories matched the search pattern")
Exit
EndIf
While 1
Local $file = FileFindNextFile($search)
If @error Then ExitLoop
$Output = $Output & StringFormat("%02d : %sn", $i, $file)
$i+=1
if Mod($i , 30) = 0 Then
MsgBox(0, $Filter, $Output)
$Output = "";
EndIf
WEnd
MsgBox(0, $Filter, $Output)
FileClose($search)
[/code]有興趣可把 $Filter 改成自己電腦有的部份跑跑看。
另外,有些 coder 在判斷「是不是檔案」的時候,是判斷「有沒有副檔名」,
這方法蠻糟的,因 C:Tmp ,代表的意義可能是 C 槽底下的 Tmp 資料夾,
也可能是 C 槽底下的 Tmp 檔案 (沒有副檔名的檔案)
關鍵只在於必須用到另一個副函式:FileGetAttrib ( "filename" ),有興趣可先看 help,
以下 code 為一副函式,單純判斷是否為資料夾[code=autoit]
; -----------------------------------------------------
; function : IsFolder
; param : $File , file or path
; return : if $File doesn't exist, return -1
; : if $File is a folder, return 1
; : if $File is not a folder, return 0
; -----------------------------------------------------
Func IsFolder(const $File)
$Attrib = FileGetAttrib($File)
If @error=1 Then ; File Error
return -1
ElseIf StringInStr($Attrib, "D") Then ; Folder
return 1
Else
return 0
EndIf
EndFunc
[/code]這個範例大概沒什麼人有興趣,一般搜尋大多是用 *.txt / *.doc / *.jpg ... 等等,
若 Filter 放的是 "*.*" 沒什麼稀奇的,若只想找出 *.rmvb 也沒什麼好稀奇的,
但若是想找出某些特定附檔名(如所有影音檔),如 *.rmvb / *.mp3 / *.mp4 ... 怎麼做?
很遺憾,目前沒辦法只列舉特定 filter,作法不外乎兩種
(1) 先將 filter 設成 *.*, 再依序判斷副檔名是否為 rmvb / mp3 / mp4。
(2) filter 設三次,*.rmvb / *.mp3 / *.mp4,依序進去做列舉,所以要列舉三次。
這兩種方法哪種效率較高?若沒做深層列舉時,方法 (1) 會較高;若做深層列舉時,答案是不一定,和資料量、資料夾深度有關,但分析有點複雜,這裡就不再提。
3. 如何深入列舉某個目錄下之目錄 / 檔案
深層列舉其實也很簡單,但用到了 recursive 概念,它的概念大概是這樣。[code=autoit]
Func ScanFolder( $Path )
; 先對 $Path 做列舉
; 當用 FileFindNextFile / FileFindFirstFile 取得之 $file 為檔案時,做輸出或處理。
; 當用 FileFindNextFile / FileFindFirstFile 取得之 $file 為路徑時,再做 ScanfFolder($Path & "" & $file)
EndFunc
[/code]去除防呆裝置,完整之程式碼如下[code=autoit]
; ----------------------------------------------
Dim $Filter = "D:" ; 確認 $Filter 必定存在,最後不要加 ,這裡不防呆
Dim $FileCount
Dim $FolderCount
Dim $Begin, $Diff
; ScanFolder 1
$FileCount = 0
$FolderCount = 0
$Begin = TimerInit()
ScanFolder1($Filter)
$Diff = TimerDiff($Begin)
MsgBox(0, "ScanFolder 1", _
StringFormat("FileCount : %dnFolderCount : %dn %lf ms", $FileCount, $FolderCount, $Diff))
; ----------------------------------------------
; 深入列舉 Folder - 1
Func ScanFolder1(const $Path)
Local $search = FileFindFirstFile($Path & "*.*")
Local $FullPath
; If $search = -1 Then Return ; 取消防呆
while 1
Local $file =FileFindNextFile($search)
If @error Then ExitLoop
$FullPath = $Path & "" & $file
;ToolTip($FullPath, 0, 0) ; (A) 這裡可以做任何事
If StringInStr( FileGetAttrib ($FullPath), "D") Then ; 找到的是資料夾
ScanFolder1($FullPath) ; 繼續搜尋資料夾
$FolderCount+=1
Else
$FileCount+=1
EndIf
WEnd
FileClose($search)
EndFunc
[/code]上面註解 (A) 部份是做輸出,可再做一些額外之判斷進行處理,由於這裡有拿來做測時,所以不做任何事,就空跑做列舉。
下面是筆者環境執行之結果
FileCount : 167444
FolderCount : 11409
Elaspe : 6799.538774 ms
那用 _FileToArray 可以做到深度列舉嗎?答案是可以的。假設函式名稱為 ScanFolder($Path),步驟如下
首先,先對 $Path 做 _FileToArray,只列舉出檔案 (出來的檔案直接做處理);
接著再做一次 _FileToArray,只列舉出資料夾,再對每個資料夾做 ScanFolder($Path) 動作,原始碼如下參考。[code=autoit]
#include <File.au3>
; ----------------------------------------------
Dim $Filter = "D:" ; 確認 $Filter 必定存在,最後不要加 ,這裡不防呆
Dim $FileCount
Dim $FolderCount
Dim $Begin, $Diff
; ScanFolder 2
$FileCount = 0
$FolderCount = 0
$Begin = TimerInit()
ScanFolder2($Filter)
$Diff = TimerDiff($Begin)
MsgBox(0, "ScanFolder 2", _
StringFormat("FileCount : %dnFolderCount : %dn %lf ms", $FileCount, $FolderCount, $Diff))
; ----------------------------------------------
; 深入列舉 Folder - 2
Func ScanFolder2(const $Path)
Local $FileList , $FolderList, $FullPath
; 先只列舉檔案
$FileList = _FileListToArray($Path, "*", 1) ; Only File List
If $FileList <> 0 Then ; 有找到檔案
For $i = 1 To $FileList[0]
; ToolTip($FileList[$i], 0, 0) ; (A) 這裡可以做任何事
Next
$FileCount+=$FileList[0]
EndIf
; 再列舉資料夾
$FolderList = _FileListToArray($Path, "*", 2) ; Only Folder List
If $FolderList <> 0 Then ; 有找到資料夾
For $i = 1 To $FolderList[0]
$FullPath = $Path & "" & $FolderList[$i]
;ToolTip($FullPath, 0, 0); (A) 對資料夾做任何事
ScanFolder2($FullPath) ; 再深入掃描
Next
$FolderCount+=$FolderList[0]
EndIf
EndFunc
[/code]結果如下
FileCount : 167444
FolderCount : 11409
Elaspe : 5416.371854 ms
對這結果應不需太過驚,哪種方法速度較快必須視資料夾的深度與結果而定,結果沒有一定。
至於其它寫法這裡就不放上。
4. 建立比 _FileToArray 方便使用之副函式
一般 _FileToArray 是將取得的結果丟到陣列裡面去,然後再由 Coder 去取得陣列裡之內容進行處理。
問題便出在這樣寫下來多了一層陣列的迴圈,維護也不是件容易的事,於是改用 Call 方式進行。
以下為 _XFileFolder.au3,建立後放入 C:Program FilesAutoIt3Include 裡
(懂得 #include 原理可不用強制放入,不懂的話還是直接放進去吧...)[code=autoit]
; ================================================
;
; 檔 名 : _XFileFolder.au3
; 作 者 : EdisonX / Edison.Shih
; 修 改 日 期 : 20120102
; 版 本 : 1.0
;
; ================================================
#include-once
const $SCAN_FOLDER = 1
const $SCAN_FILES = 2
; ================================================
; 函式名稱 : _ScanfFolderSimple
; 函式說明 : 單層掃描資料夾/檔案
; 參數 1 : const $sPath Filter, 掃描資料夾路徑 < 必填 , 最後不可有反斜線>
; 參數 2 : const $sFilter, <必填>
; 參數 3 : const $Func , 針對每個掃到之資料夾/檔案之處理函式 < 必填 >
; 參數 4 : const $scan_flag , <選填>
; $SCAN_FOLDER 代表只掃資料夾 ,
; $SCAN_FILES 代表只掃檔案,
; 相加代表全都掃
; 傳回值 :
; ================================================
Func _ScanFolderSimple(const $sPath, const $sFilter , const $Func , _
const $scan_flag=3)
Local $FolderFlag , $FileFlag
Local $FullPath , $search, $File
If $scan_flag == 3 OR $scan_flag == 1 Then $FolderFlag=1 ; 有要找資料夾
If $scan_flag == 2 OR $scan_flag == 1 Then $FileFlag=1 ; 有要找檔案
$search = FileFindFirstFile($sPath & "" & $sFilter)
If $search = -1 Then Return ; 找不到東西
while 1
$File = FileFindNextFile($search)
If @error Then ExitLoop
$FullPath = $sPath & "" & $File
If StringInStr( FileGetAttrib ($FullPath), "D") And $FolderFlag Then ; 找到的是資料夾
Call($Func, $FullPath)
EndIf
If StringInStr( FileGetAttrib ($FullPath), "D")=0 And $FileFlag Then ; 找到的是檔案
Call($Func, $FullPath)
EndIf
WEnd
FileClose($search)
EndFunc
[/code]這份 header 裡使用的 function 原理在上面都有提過,包成這樣有什麼好處?我們直接寫個 demo 吧...
< 記得把這份 au3 放到 C:Program FilesAutoIt3Include >[code=autoit]
#include <_XFileFolder.au3>
_ScanFolderSimple("D:", "*.*", "ProcessFile", $SCAN_FILES)
Func ProcessFile(const $sFile)
ToolTip($sFile, 10, 10)
Sleep(300)
EndFunc
[/code]這樣是否簡潔多了?
5. 進階議題
上面的 _ScanFolderSimple,只有找單層而已,沒有再做深入列舉。要改也不難,有興趣可自行實作。
另外針對這問題,筆者所知至少還有三種設計模式,關鍵考量都是在於三點:好不好呼叫、方不方便維護、效率高不高。
由於筆者認為使用 AU 主要目的、最大優點應是將「開發時間」縮到最小,故給了一份 _ScanFolderSimple 架構參考,
這份架構會了之後,可以再寫一份 _ScanFolderDeep,做深入掃描,它的缺點是用了間接之函式呼叫,效率會差一點;
還有一方研究是將深入掃描之遞迴拿掉 (效率將提昇),這種研究對初學者較吃力,這裡就不附上與探討了。
本文敘述至此 (其實筆者也編了三天的文章了),常用的掃描議題應已敘述,其餘可加點巧思,加以改進。若對本文所有疑惑或建議,歡迎提出,共同討論。
以上。 by EdisonX |