Windows批处理脚本:解析CSV文件并输出文本文件

8

我在另一个页面上看到了一篇回答 (帮助编写批处理脚本来解析CSV文件并输出文本文件) - 代码非常棒:

@ECHO OFF
IF "%~1"=="" GOTO :EOF
SET "filename=%~1"
SET fcount=0
SET linenum=0
FOR /F "usebackq tokens=1-10 delims=," %%a IN ("%filename%") DO ^
CALL :process "%%a" "%%b" "%%c" "%%d" "%%e" "%%f" "%%g" "%%h" "%%i" "%%j"
GOTO :EOF

:trim
SET "tmp=%~1"
:trimlead
IF NOT "%tmp:~0,1%"==" " GOTO :EOF
SET "tmp=%tmp:~1%"
GOTO trimlead

:process
SET /A linenum+=1
IF "%linenum%"=="1" GOTO picknames

SET ind=0
:display
IF "%fcount%"=="%ind%" (ECHO.&GOTO :EOF)
SET /A ind+=1
CALL :trim %1
SETLOCAL ENABLEDELAYEDEXPANSION
ECHO !f%ind%!!tmp!
ENDLOCAL
SHIFT
GOTO display

:picknames
IF %1=="" GOTO :EOF
CALL :trim %1
SET /a fcount+=1
SET "f%fcount%=%tmp%"
SHIFT
GOTO picknames

它对我制作的格式为csv文件的示例非常有效:

Header,Name,Place
one,two,three
four,five,six

然而我想要更改的实际文件由64个字段组成 - 所以我将tokens = 1-10修改为tokens = 1-64,并增加了%%a等变量,最多增加到64个(最后一个称为%%BL)。现在,当我在带有64个标记的“大”csv文件上运行批处理时,什么都不会发生。没有错误(好),但没有输出!(坏)。如果有人能帮忙,那就太棒了......如果我可以解决这个问题,整个应用程序就要完成了!或者如果有人有一些示例代码,可以对无限数量的标记执行类似的操作...最终,我想制作一个字符串,它将是这样的:
field7,field12,field15,field18

我的建议是用其他语言实现这个功能(有很多库可以解析CSV文件),而不是批处理。 - MPelletier
1
是的,我也想这样做,但遗憾的是它必须在运行嵌入式Windows的12年老PC上运行,并且占用空间很小 :( - Jeff Webb
:) 是啊!但我没能说服他们升级!我很惊讶它们甚至还能作为收银机工作,更别提我现在对它们做的事情了 :) - Jeff Webb
1
等一下,VB6运行库不就是一个dll吗?有了它,vbscript和VB6代码都可以正常工作,并且占用相对较小的空间。 - MPelletier
4个回答

21

重要更新 - 我认为Windows批处理不适合您的需求,因为单个FOR / F无法解析超过31个标记。请参见下面附录底部的解释。

但是,使用批处理仍然可以实现您想要的功能。这段丑陋的代码将为您提供对所有64个标记的访问权限。

for /f "usebackq tokens=1-29* delims=," %%A in ("%filename%") do (
  for /f "tokens=1-26* delims=," %%a in ("%%^") do (
    for /f "tokens=1-9 delims=," %%1 in ("%%{") do (
      rem Tokens 1-26 are in variables %%A - %%Z
      rem Token  27 is in %%[
      rem Token  28 is in %%\
      rem Token  29 is in %%]
      rem Tokens 30-55 are in %%a - %%z
      rem Tokens 56-64 are in %%1 - %%9
    )
  )
)

附录提供了关于上述内容如何工作的重要信息。

如果您只需要在该行的64个令牌中分散的一些令牌,则解决方案可能会稍微容易一些,因为您可能可以避免使用疯狂的字符作为FOR变量。但仍需进行仔细的记账。

例如,以下内容将使您可以访问令牌5、27、46和64。

for /f "usebackq tokens=5,27,30* delims=," %%A in ("%filename%") do (
  for /f "tokens=16,30* delims=," %%E in ("%%D") do (
    for /f "tokens=4 delims=," %%H in ("%%G") do (
      rem Token  5 is in %%A
      rem Token 27 is in %%B
      rem Token 46 is in %%E
      rem Token 64 is in %%H
    )
  )
)
2016年4月更新 - 在DosTips用户Aacini、penpen和aGerman的调查工作基础上,我开发了一种相对简单的方法,可以使用FOR /F同时访问数千个标记。这项工作是这个DosTips主题的一部分。实际代码可以在以下三个帖子中找到: 原始回答 FOR变量仅限于单个字符,因此您的%%BL策略无法奏效。变量区分大小写。根据Microsoft的说法,在一个FOR语句中,您仅限于捕获26个标记,但如果您使用的不仅仅是字母,则可以获得更多标记。这很麻烦,因为您需要ASCII表来确定哪些字符放在哪里。但是,FOR不允许任何字符,并且单个FOR /F可以分配的最大标记数是31 +1。任何尝试解析和分配超过31个标记的操作都会悄然失败,就像您发现的那样。
幸运的是,我认为您不需要那么多标记。您只需使用TOKENS选项指定要使用的标记即可。
for /f "usebackq tokens=7,12,15,18 delims=," %%A in ("%filename%") do echo %%A,%%B,%%C,%%D

将会给你第7、12、15和18个令牌。

补充

2016年4月更新几周前我得知以下规则(写于6年前)与代码页有关。下面的数据已经验证过适用于代码页437和850。更重要的是,扩展ASCII字符128-254的FOR变量序列并不匹配字节码值,并且在代码页上变化巨大。事实证明,FOR /F变量映射基于底层UTF-(16?)代码点。因此,在使用FOR /F时,扩展ASCII字符的用途有限。请参见http://www.dostips.com/forum/viewtopic.php?f=3&t=7703了解更多信息。

我进行了一些测试,并报告以下结果(响应jeb的评论更新):

大多数字符都可以用作FOR变量,包括扩展ASCII 128-254。但是,某些字符不能用于在FOR语句的第一部分中定义变量,但可以在DO子句中使用。有些则两者都不能使用。有些没有限制,但需要特殊语法。

以下是具有限制或需要特殊语法的字符摘要。请注意,尖括号中的文本,例如<space>代表单个字符。

Dec  Hex   Character   Define     Access
  0  0x00  <nul>       No         No
 09  0x09  <tab>       No         %%^<tab>  or  "%%<tab>"
 10  0x0A  <LF>        No         %%^<CR><LF><CR><LF>  or  %%^<LF><LF>
 11  0x0B  <VT>        No         %%<VT>
 12  0x0C  <FF>        No         %%<FF>
 13  0x0D  <CR>        No         No
 26  0x1A  <SUB>       %%%VAR%    %%%VAR% (%VAR% must be defined as <SUB>)
 32  0x20  <space>     No         %%^<space>  or  "%%<space>"
 34  0x22  "           %%^"       %%"  or  %%^"
 36  0x24  $           %%$        %%$ works, but %%~$ does not
 37  0x25  %           %%%%       %%~%%
 38  0x26  &           %%^&       %%^&  or  "%%&"
 41  0x29  )           %%^)       %%^)  or  "%%)"
 44  0x2C  ,           No         %%^,  or  "%%,"
 59  0x3B  ;           No         %%^;  or  "%%;"
 60  0x3C  <           %%^<       %%^<  or  "%%<"
 61  0x3D  =           No         %%^=  or  "%%="
 62  0x3E  >           %%^>       %%^>  or  "%%>"
 94  0x5E  ^           %%^^       %%^^  or  "%%^"
124  0x7C  |           %%^|       %%^|  or  "%%|"
126  0x7E  ~           %%~        %%~~ (%%~ may crash CMD.EXE if at end of line)
255  0xFF  <NB space>  No         No

特殊字符,如^ < > | &必须进行转义或引用。例如,以下内容是有效的:

for /f %%^< in ("OK") do echo "%%<" %%^<

有些字符不能用于定义FOR变量。例如,以下内容会导致语法错误:

for /f %%^= in ("No can do") do echo anything

但是 %%= 可以通过使用 TOKENS 选项进行隐式定义,并在 DO 子句中访问该值,如下所示:
for /f "tokens=1-3" %%^< in ("A B C") do echo %%^< %%^= %%^>

%是奇数-你可以使用%%%%定义FOR变量。但是,除非使用~修饰符,否则无法访问该值。这意味着引号无法保留。

for /f "usebackq tokens=1,2" %%%% in ('"A"') do echo %%%% %%~%%

上面的代码会产生%% A~是一个潜在的危险FOR变量。如果您尝试使用行末的%%~访问变量,则可能会得到不可预测的结果,甚至可能导致CMD.EXE崩溃!访问它的唯一可靠方式是使用%%~~,当然会删除任何包含的引号。
for /f %%~ in ("A") do echo This can crash because its the end of line: %%~

for /f %%~ in ("A") do echo But this (%%~) should be safe

for /f %%~ in ("A") do echo This works even at end of line: %%~~
<SUB>(0x1A)字符很特殊,因为嵌入到批处理脚本中的<SUB>字面值会被读取为换行符(<LF>)。为了将<SUB>用作FOR变量,必须将该值存储在环境变量中,然后%%% VAR%将用于定义和访问。
正如已经说明的那样,单个FOR / F最多可以解析和分配31个标记。例如:
@echo off
setlocal enableDelayedExpansion
set "str="
for /l %%n in (1 1 35) do set "str=!str! %%n"
for /f "tokens=1-31" %%A in ("!str!") do echo A=%%A _=%%_

上述代码的输出是 A=1 _=31。请注意,仅使用2-30个标记进行解析和分配是有效的,我只是想提供一个简单的例子。如果尝试解析并分配超过31个标记,将会静默失败而不设置ERRORLEVEL。
@echo off
setlocal enableDelayedExpansion
set "str="
for /l %%n in (1 1 35) do set "str=!str! %%n"
for /f "tokens=1-32" %%A in ("!str!") do echo this example fails entirely

您可以按照以下方式解析和分配最多31个标记,并将剩余部分分配给另一个标记:
@echo off
setlocal enableDelayedExpansion
set "str="
for /l %%0 in (1 1 35) do set "str=!str! %%n"
for /f "tokens=1-31*" %%@ in ("!str!") do echo @=%%A  ^^=%%^^  _=%%_

以上代码输出结果为:@=1 ^=31 _=32 33 34 35

现在来说说更糟糕的消息吧。 当我查看Windows批处理脚本中FOR命令的标记数限制时,我发现单个FOR /F无法解析超过31个标记。

@echo off
setlocal enableDelayedExpansion
set "str="
for /l %%n in (1 1 35) do set "str=!str! %%n"
for /f "tokens=1,31,32" %%A in ("!str!") do echo A=%%A  B=%%B  C=%%C

非常不幸的输出是 A=1 B=31 C=%C

1
@Mechaflash - 实际上大多数字符都是变量名称有效的。请参见我上面的补充说明。 - dbenham
1
嗯,也许使用<tab>,;=作为FOR变量会有问题,但是你可以访问它们。我只是无法访问%%<0x00>%%<0x0D> - jeb
从扩展字符(十六进制0x80到0xFF)中选取作为“FOR /F”标记时,我遇到了问题。其中0xAE和0xAF需要使用“^”进行转义,才能正常工作。而0xFF(<0x255>我猜是这个)则无法正常工作。 - npocmaka
@npocmaka - 我刚刚重新测试了从0x80到0xFF的所有字符,唯一失败的是0xFF(不间断空格)。不需要转义。这与我在答案中展示的结果相同。我最初在Vista上进行了测试,今天我在Win 7上进行了测试。 - dbenham
找到了问题。由于某种原因,Notepad++将0xAE0xAF转换为<>... - npocmaka
显示剩余6条评论

4
我的答案由两个部分组成。第一部分是我在“帮助编写批处理脚本以解析CSV文件并输出文本文件”问题中发布的新答案,该问题没有任何字段数量限制。
第二部分是对该答案的修改,它允许在文件名后面添加附加参数来选择从csv文件中提取哪些字段。修改后的代码用大写字母表示。
@echo off
setlocal EnableDelayedExpansion

rem Create heading array:
set /P headingRow=< %1
set i=0
for %%h in (%headingRow%) do (
    set /A i+=1
    set heading[!i!]=%%~h
)


REM SAVE FILE NAME AND CREATE TARGET ELEMENTS ARRAY:
SET FILENAME=%1
IF "%2" == "" (FOR /L %%J IN (1,1,%i%) DO SET TARGET[%%J]=%%J) & GOTO CONTINUE
SET J=0
:NEXTTARGET
    SHIFT
    IF "%1" == "" GOTO CONTINUE
    SET /A J+=1
    SET TARGET[%J%]=%1
GOTO NEXTTARGET
:CONTINUE


rem Process the file:
call :ProcessFile < %FILENAME%
exit /B

:ProcessFile
set /P line=
:nextLine
    set line=:EOF
    set /P line=
    if "!line!" == ":EOF" goto :EOF
    set i=0
    SET J=1
    for %%e in (%line%) do (
        set /A i+=1
        FOR %%J IN (!J!) DO SET TARGET=!TARGET[%%J]!
        IF !i! == !TARGET! (
            for %%i in (!i!) do echo !heading[%%i]!%%~e
            SET /A J+=1
        )
    )
goto nextLine
exit /B

例如:

EXTRACTCSVFIELDS THEFILE.CSV 7 12 15 18

编辑 更简单的方法

下面是一个新版本,它使用目标元素列表而不是数组,因此更简单易懂:

@echo off
setlocal EnableDelayedExpansion

rem Create heading array:
set /P headingRow=< %1
set i=0
for %%h in (%headingRow%) do (
    set /A i+=1
    set heading[!i!]=%%~h
)

REM CREATE TARGET ELEMENTS LIST:
IF "%2" == "" (
    SET TARGETLIST=
    FOR /L %%J IN (1,1,%i%) DO SET TARGETLIST=!TARGETLIST! %%J
) ELSE (
    SET TARGETLIST=%*
    SET TARGETLIST=!TARGETLIST:* =!
)

rem Process the file:
call :ProcessFile < %1
exit /B

:ProcessFile
set /P line=
:nextLine
    set line=:EOF
    set /P line=
    if "!line!" == ":EOF" goto :EOF
    set i=0
    for %%e in (%line%) do (
        set /A i+=1
        for %%i IN (!i!) DO (
            IF "!TARGETLIST:%%i=!" NEQ "!TARGETLIST!" (
                echo !heading[%%i]!%%~e
            )
        )
    )
goto nextLine
exit /B

此版本无需按顺序提供所需字段。
编辑:抱歉!由于 for 参数的事情分散了我的注意力,所以我没有意识到你的最后一个请求。
"Ultimately I want to make a string which will be something like:

field7,field12,field15,field18"

只需修改程序的最后一部分即可实现这一点:
:ProcessFile
set /P line=
:nextLine
    set line=:EOF
    set /P line=
    if "!line!" == ":EOF" goto :EOF
    set i=0
    set resultString=
    for %%e in (%line%) do (
        set /A i+=1
        for %%i IN (!i!) DO (
            IF "!TARGETLIST:%%i=!" NEQ "!TARGETLIST!" (
                set resultString=!resultString!%%~e,
            )
        )
    )
    set resultString=%resultString:~0,-1%
    echo Process here the "%resultString%"
goto nextLine
exit /B

您可以删除标题数组的创建,因为您不需要标题!;)

+1 - 顶部的链接应该是https://dev59.com/z1jUa4cB1Zd3GeqPS5gz。这个解决方案的一个不错的特点是能够处理带引号的字段内的分隔符。但是,该方案的限制是字段不能包含*或?。 - dbenham
非常完美!一招制敌,而且非常容易适应和使用dos wget进行上传。非常感谢您 - 还要感谢所有回复帖子的人,我非常感激 :) - Jeff Webb

1
使用 %%@ 和 %%` 作为起始变量,最大可获得71:
@echo off
for /f "tokens=1-31* delims=," %%@ in ("%filename%") do (
    echo:
    echo  1=%%@
    echo  2=%%A
    echo  3=%%B
    echo  4=%%C
    echo  5=%%D
    echo  6=%%E
    echo  7=%%F
    echo  8=%%G
    echo  9=%%H
    echo 10=%%I
    echo 11=%%J
    echo 12=%%K
    echo 13=%%L
    echo 14=%%M
    echo 15=%%N
    echo 16=%%O
    echo 17=%%P
    echo 18=%%Q
    echo 19=%%R
    echo 20=%%S
    echo 21=%%T
    echo 22=%%U
    echo 23=%%V
    echo 24=%%W
    echo 25=%%X
    echo 26=%%Y
    echo 27=%%Z
    echo 28=%%[
    echo 29=%%\
    echo 30=%%]
    echo 31=%%^^
    for /F "tokens=1-30* delims=," %%` in ("%%_") do (
        echo 32=%%`
        echo 33=%%a
        echo 34=%%b
        echo 35=%%c
        echo 36=%%d
        echo 37=%%e
        echo 38=%%f
        echo 39=%%g
        echo 40=%%h
        echo 41=%%i
        echo 42=%%j
        echo 43=%%k
        echo 44=%%l
        echo 45=%%m
        echo 46=%%n
        echo 47=%%o
        echo 48=%%p
        echo 49=%%q
        echo 50=%%r
        echo 51=%%s
        echo 52=%%t
        echo 53=%%u
        echo 54=%%v
        echo 55=%%w
        echo 56=%%x
        echo 57=%%y
        echo 58=%%z
        echo 59=%%{
        echo 60=%%^|
        echo 61=%%}
        for /F "tokens=1-9* delims=," %%0 in ("%%~") do (
            echo 62=%%0
            echo 63=%%1
            echo 64=%%2
            echo 65=%%3
            echo 66=%%4
            echo 67=%%5
            echo 68=%%6
            echo 69=%%7
            echo 70=%%8
            echo 71=%%9
        )
    )
)

0

当我再次阅读这个问题以及在最受欢迎的答案中提出的解决方案时,我认为可以开发一种更简单的方法来充分利用一系列嵌套的FOR / F命令。我开始编写这样的方法,它将允许使用127个附加标记,将它们放置在ASCII 128-254字符范围内。然而,当我的程序完成后,我发现“自然”128..254顺序中的ASCII字符不能用于此目的...

随后,一群人对这个问题产生了兴趣,并进行了一系列的发现和开发,最终得出了一种方法,可以在一系列嵌套的FOR / F命令中使用许多标记(超过43,000个!)。您可以在this DosTips topic中阅读有关该发现所涉及的研究和开发的详细描述。

最后,我使用了新的方法修改了我的程序,现在它可以以简单的方式处理长达4094个同时标记(来自具有长行的文本文件)。我的应用程序由一个批处理文件组成,名为“MakeForTokens.bat”,您可以在参数中运行所需的令牌数量。例如:
MakeForTokens.bat 64

该程序生成一个批处理文件,名为ForTokens.bat,其中包含管理大量同时令牌所需的所有代码,包括如何处理文件的示例。这样,用户只需要插入自己的文件名和所需的令牌即可获得一个工作程序。
在这种特殊情况下,这将是最终的ForTokens.bat文件,解决了这个问题的陈述,在删除了大部分描述性注释之后。
@echo off & setlocal EnableDelayedExpansion & set "$numTokens=65"

Rem/For  Step 1: Define the series of auxiliary variables that will be used as FOR tokens.
call :DefineForTokens

Rem/For  Step 2:  Define an auxiliary variable that will contain the desired tokens when it is %expanded%.
call :ExpandTokensString "tokens=7,12,15,18"

Rem/For  Step 3:  Define the variable with the "delims" value that will be used in the nested FOR's.
set "delims=delims=,"

Rem/For  Step 4:  Create the macro that contain the nested FOR's.
call :CreateNestedFors

Rem/For  Step 5:  This is the main FOR /F command that process the file.
for /F "usebackq tokens=1-31* %delims%" %%%$1% in ("filename.txt") do %NestedFors% (

   Rem/For  Step 6: Process the tokens.

   Rem/For  To just show they, use the "tokens" variable defined above:
   echo %tokens%

   Rem/For  You may also process individual tokens via another FOR /F command:
   for /F "tokens=1-%tokens.len%" %%a in ("%tokens%") do (
      echo Field  #7: %%a
      echo Field #12: %%b
      echo Field #15: %%c
      echo Field #18: %%d
   )

)

goto :EOF


Support subroutines. You must not modify any code below this line.


:DefineForTokens

for /F "tokens=2 delims=:." %%p in ('chcp') do set /A "_cp=%%p, _pages=($numTokens/256+1)*2"
set "_hex= 0 1 2 3 4 5 6 7 8 9 A B C D E F"
call set "_pages=%%_hex:~0,%_pages%%%"
if %$numTokens% gtr 2048 echo Creating FOR tokens variables, please wait . . .
(
   echo FF FE
   for %%P in (%_pages%) do for %%A in (%_hex%) do for %%B in (%_hex%) do echo %%A%%B 3%%P 0D 00 0A 00
) > "%temp%\forTokens.hex.txt"
certutil.exe -decodehex -f "%temp%\forTokens.hex.txt" "%temp%\forTokens.utf-16le.bom.txt" >NUL
chcp 65001 >NUL
type "%temp%\forTokens.utf-16le.bom.txt" > "%temp%\forTokens.utf8.txt"
(for /L %%N in (0,1,%$numTokens%) do set /P "$%%N=")  < "%temp%\forTokens.utf8.txt" 
chcp %_cp% >NUL
del "%temp%\forTokens.*.txt"
for %%v in (_cp _hex _pages) do set "%%v="
exit /B


:CreateNestedFors

setlocal EnableDelayedExpansion
set /A "numTokens=$numTokens-1, mod=numTokens%%31, i=numTokens/31, lim=31"
if %mod% equ 0 set "mod=31"
set "NestedFors="
for /L %%i in (32,31,%numTokens%) do (
   if !i! equ 1 set "lim=!mod!"
   set "NestedFors=!NestedFors! for /F "tokens=1-!lim!* %delims%" %%!$%%i! in ("%%!$%%i!") do"
   set /A "i-=1"
)
for /F "delims=" %%a in ("!NestedFors!") do endlocal & set "NestedFors=%%a"
exit /B


:ExpandTokensString variable=tokens definitions ...

setlocal EnableDelayedExpansion
set "var=" & set "tokens=" & set "len=0"
if "%~2" equ "" (set "params=%~1") else set "params=%*"
for %%a in (!params!) do (
   if not defined var (
      set "var=%%a"
   ) else for /F "tokens=1-3 delims=-+" %%i in ("%%a") do (
      if "%%j" equ "" (
         if %%i lss %$numTokens% set "tokens=!tokens! %%!$%%i!" & set /A len+=1
      ) else (
         if "%%k" equ "" (set "k=1") else set "k=%%k"
         if %%i leq %%j (
            for /L %%n in (%%i,!k!,%%j) do if %%n lss %$numTokens% set "tokens=!tokens! %%!$%%n!" & set /A len+=1
         ) else (
            for /L %%n in (%%i,-!k!,%%j) do if %%n lss %$numTokens% set "tokens=!tokens! %%!$%%n!" & set /A len+=1
         )
      )
   )
)
endlocal & set "%var%=%tokens%" & set "%var%.len=%len%"
exit /B

您可以从此网站下载MakeForTokens.bat应用程序。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接