【问题标题】:Copy one random file to another folder recursively whilst keeping folder structure递归地将一个随机文件复制到另一个文件夹,同时保持文件夹结构
【发布时间】:2016-01-10 11:34:58
【问题描述】:

我想创建一个 .bat 脚本以从每个文件夹(也包括子文件夹,因此递归)中仅复制一个随机文件,同时保持文件夹结构。我已经尝试了以下代码,它接近我想要的,但没有复制文件夹结构和每个文件夹一个文件。

@ECHO OFF

SETLOCAL EnableExtensions EnableDelayedExpansion
SET Destination=H:\Temp
SET FileFilter=.ape
SET SubDirectories=/S

SET Source=%~dp1
SET FileList1Name=FileList1.%RANDOM%.txt
SET FileList1="%TEMP%\%FileList1Name%"
SET FileList2="%TEMP%\FileList2.%RANDOM%.txt"

ECHO Source: %Source%
IF /I {%SubDirectories%}=={/S} ECHO + Sub-Directories
IF NOT {"%FileFilter%"}=={""} ECHO File Filter: %FileFilter%
ECHO.
ECHO Destination: %Destination%
ECHO.
ECHO.
ECHO Building file list...

CD /D "%Source%"
DIR %FileFilter% /A:-D-H-S /B %SubDirectories% > %FileList1%

FOR /F "tokens=1,2,3 delims=:" %%A IN ('FIND /C ":" %FileList1%') DO SET     TotalFiles=%%C
SET TotalFiles=%TotalFiles:~1%

ECHO The source has %TotalFiles% total files.
ECHO Enter the number of random files to copy to the destination.
SET /P FilesToCopy=
ECHO.

IF /I %TotalFiles% LSS %FilesToCopy% SET %FilesToCopy%=%TotalFiles%

SET Destination="%Destination%"
IF NOT EXIST %Destination% MKDIR %Destination%

SET ProgressTitle=Copying Random Files...

FOR /L %%A IN (1,1,%FilesToCopy%) DO (
    TITLE %ProgressTitle% %%A / %FilesToCopy%
    REM Pick a random file.
    SET /A RandomLine=!RANDOM! %% !TotalFiles!
    REM Go to the random file's line.
    SET Line=0
    FOR /F "usebackq tokens=*" %%F IN (%FileList1%) DO (
        IF !Line!==!RandomLine! (
            REM Found the line. Copy the file to the destination.
            XCOPY /V /Y "%%F" %Destination%
        ) ELSE (
            REM Not the random file, build the new list without this file included.
            ECHO %%F>> %FileList2%
        )
        SET /A Line=!Line! + 1
    )
    SET /A TotalFiles=!TotalFiles! - 1
    REM Update the master file list with the new list without the last file.
    DEL /F /Q %FileList1%
    RENAME %FileList2% %FileList1Name%
)

IF EXIST %FileList1% DEL /F /Q %FileList1%
IF EXIST %FileList2% DEL /F /Q %FileList2%

ENDLOCAL

目的地应该像上面的代码一样在 .bat 代码中设置。有人可以帮我吗?提前致谢!

【问题讨论】:

  • 首先,你能提供你目前尝试过的代码吗?还有,复制到哪里? .bat 所在的文件夹?
  • 你可以看看enter link description here如何选择随机文件

标签: windows batch-file


【解决方案1】:

使用 XCOPY 复制目录树结构(仅限文件夹)很简单。

从给定文件夹中选择一个随机文件并不难。首先,您需要对文件进行计数,使用 DIR /B 列出它们并使用 FIND /C 对它们进行计数。然后使用模运算符在范围内选择一个随机数。最后使用 DIR /B 再次列出它们,FINDSTR /N 对它们进行编号,另一个 FINDSTR 选择第 N 个文件。

也许最棘手的一点是处理相对路径。 FOR /R 可以遍历目录树,但它提供了完整的绝对路径,这对源非常有用,但在尝试指定目标时没有任何好处。

您可以做一些事情。您可以获取根源路径的字符串长度,然后使用子字符串操作推导出相对路径。有关计算字符串长度的方法,请参阅 How do you get the string length in a batch file?

另一种选择是使用 FORFILES 遍历源树并直接获取相对路径,但速度极慢。

但也许最简单的解决方案是将未使用的驱动器号映射到源文件夹和目标文件夹的根目录。这使您可以直接使用绝对路径(在删除驱动器号之后)。这是我选择的选项。此解决方案的唯一不利方面是您必须知道系统的两个未使用的驱动器号,因此不能简单地将脚本从一个系统复制到另一个系统。我想你可以以编程方式 发现未使用的驱动器号,但我没有打扰。

注意: 源树不包含目的地很重要

@echo off
setlocal

:: Define source and destination
set "source=c:\mySource"
set "destination=c:\test2\myDestination"

:: Replicate empty directory structure
xcopy /s /t /e /i "%source%" "%destination%"

:: Map unused drive letters to source and destination. Change letters as needed
subst y: "%source%"
subst z: "%destination%"

:: Walk the source tree, calling :processFolder for each directory.
for /r y:\ %%D in (.) do call :processFolder "%%~fD"

:: Cleanup and exit
subst y: /d
subst z: /d
exit /b


:processFolder
:: Count the files
for /f %%N in ('dir /a-d /b %1 2^>nul^|find /c /v ""') do set "cnt=%%N"

:: Nothing to do if folder is empty
if %cnt% equ 0 exit /b

:: Select a random number within the range
set /a N=%random% %% cnt + 1

:: copy the Nth file
for /f "delims=: tokens=2" %%F in (
  'dir /a-d /b %1^|findstr /n .^|findstr "^%N%:"'
) do copy "%%D\%%F" "z:%%~pnxD" >nul

exit /b

编辑

我修复了上述代码中的一个不明显的错误。原来的 COPY 行如下:

copy "%%~1\%%F" "z:%%~pnx1" >nul

如果源树中的任何文件夹的名称中包含 %D%F,则该版本将失败。如果使用%var% 扩展变量或使用%1 扩展:subroutine 参数,则此类问题始终存在于FOR 循环中。

使用%%D 代替%1 可以轻松解决此问题。这是违反直觉的,但只要任何 FOR 循环当前处于活动状态,FOR 变量就在范围内是全局的。 %%D 在大部分 :processFolder 例程中都无法访问,但在 FOR 循环中可用。

【讨论】:

  • 要处理相对路径,您可以使用xcopy /L ".\*.*" "%TEMP%\",因为它列出了相对路径,以防提供相对源路径; /L 表示列出但不要复制;使用| find ".\",您可以删除摘要行# file(s) copied;另请参阅this post 我使用此技术的地方...
  • @aschipfl - 起初我认为这是个好主意,但它似乎只适用于文件路径。但我只需要所有文件夹的相对路径。
  • 是的,它只返回文件;但是您可以利用它,因为您只需要循环一次输出xcopy /L,而不是为树中的每个子目录建立额外的循环;我刚刚用my approach 和你的Aacini's 进行了一些性能测试,我发现我的“赢”了巨大的目录树(copy 命令echoed out);当然,由于数组大小/环境空间有限,我的和 Aacini 的每个目录中的大量文件都会失败......
【解决方案2】:

处理目录树的“自然”方式是通过递归子程序;这种方法最大限度地减少了该过程固有的问题。正如我在this post 所说的那样:“您可以在 Batch 中编写递归算法,让您可以精确控制在每个嵌套子目录中所做的事情”。我在this answer 获取了复制一棵树的代码,并对其稍作修改以解决此问题。

@echo off
setlocal

set "Destination=H:\Temp"
set "FileFilter=*.ape"

rem Enter to source folder and process it
cd /D "%~dp1"
call :processFolder
goto :EOF


:processFolder
setlocal EnableDelayedExpansion

rem For each folder in this level
for /D %%a in (*) do (

   rem Enter into it, process it and go back to original
   cd "%%a"
   set "Destination=%Destination%\%%a"
   if not exist "!Destination!" md "!Destination!"

   rem Get the files in this folder and copy a random one
   set "n=0"
   for %%b in (%FileFilter%) do (
      set /A n+=1
      set "file[!n!]=%%b"
   )
   if !n! gtr 0 (
      set /A "rnd=!random! %% n + 1"
      for %%i in (!rnd!) do copy "!file[%%i]!" "!Destination!"
   )

   call :processFolder
   cd ..
)
exit /B

【讨论】:

  • 我想过写一个递归例程,但觉得不值得。如果任何文件夹路径包含!,此解决方案总是会失败,如果文件名包含!,则可能会失败。当然,这可以修复。使用包含许多文件的测试树,您的代码花费的时间是我的两倍多。我使用了你的递归方法和一个额外的 CALL 来消除延迟扩展的需要,以及我随机选择文件的方法,它比我发布的解决方案快 25%。
  • @dbenham:嗯,这似乎与环境大小有关,像往常一样。如果环境完全清空(通过整个路径调用find/findstr),我认为这个过程会更快。我也认为使用延迟扩展应该比使用 CALL 技巧稍快。
【解决方案3】:

这是使用xcopy /L 遍历源目录中所有文件的另一种方法,由于/L,它实际上不会复制任何内容,而是返回相对于源目录的路径。有关代码的解释,请参阅所有remarks:

@echo off
setlocal EnableExtensions DisableDelayedExpansion

rem Define source and destination directories here:
set "SOURCE=%dp~1"
set "DESTIN=H:\Temp"

rem Change to source directory:
cd /D "%SOURCE%"
rem Reset index number:
set /A "INDEX=0"
rem Walk through output of `xcopy /L`, which returns
rem all files in source directory as relative paths;
rem `find` filters out the summary line; `echo` appends one more line
rem with invalid path, just to process the last item as well:
for /F "delims=" %%F in ('
    2^> nul xcopy /L /S /I /Y "." "%TEMP%" ^
        ^| find ".\" ^
        ^& echo^(C:\^^^|\^^^|
') do (
    rem Store path to parent directory of current item:
    set "CURRPATH=%%~dpF"
    setlocal EnableDelayedExpansion
    if !INDEX! EQU 0 (
        rem First item, so build empty directory tree:
        xcopy /T /E /Y "." "%DESTIN%"
        endlocal
        rem Set index and first array element, holding
        rem all files present in the current directory:
        set /A "INDEX=1"
        set "ITEMS_1=%%F"
    ) else if "!CURRPATH!"=="!PREVPATH!" (
        rem Previous parent directory equals current one,
        rem so increment index and store current file:
        set /A "INDEX+=1"
        for %%I in (!INDEX!) do (
            endlocal
            set /A "INDEX=%%I"
            set "ITEMS_%%I=%%F"
        )
    ) else (
        rem Current parent directory is not the previous one,
        rem so generate random number from 1 to recent index
        rem to select a file in the previous parent directory,
        rem perform copying task, then reset index and store
        rem the parent directory of the current (next) item:
        set /A "INDEX=!RANDOM!%%!INDEX!+1"
        for %%I in (!INDEX!) do (
            xcopy /Y "!ITEMS_%%I!" "%DESTIN%\!ITEMS_%%I!"
            endlocal
            set /A "INDEX=1"
            set "ITEMS_1=%%F"
        )
    )
    rem Store path to parent directory of previous item:
    set "PREVPATH=%%~dpF"
)
endlocal
exit /B

对于这种方法,目标目录也可以位于源目录树中。

【讨论】: