替换文本文件中的单个字符时出现意外结果

Posted

技术标签:

【中文标题】替换文本文件中的单个字符时出现意外结果【英文标题】:Unexpected results while replacing single characters in a text file 【发布时间】:2021-01-27 01:00:24 【问题描述】:

我的批处理文件:

@ECHO off

(FOR /f "delims=" %%i in (source.txt) DO (
    SET "line=%%i"
    setlocal enabledelayedexpansion

    SET "line=!line:Ć=F!"
    SET "line=!line:Ç=G!"
    SET "line=!line:Ň=R!"
    SET "line=!line:Ô=T!"

    ECHO.!line!
    endlocal
))>"output.txt"

我的 source.txt 文件:

ĆÇŇÔ

预期的 output.txt 文件:

FGRT

当前的 output.txt 文件:

FFRR

我的问题是:这里出了什么问题?

【问题讨论】:

在批处理文件/CMD 标记中找到 Unicode 规范化专家的机会很低……如果你不能用其他语言(甚至 PowerShell)重写它,你可能想开始阅读字符串规范化以及 cmd 如何自己处理字符串比较。可能检查其他 SE 网站,如果类似的问题在那里是合适的(它与 SO 的主题无关)。 我猜,您的 source.txt 是用 Unicode(16 位)编码的。 cmd 尝试最好将其“翻译”成 8 位 Ansi,但您会丢失一半的“地址空间”。这意味着,多个 Unicode 字符“翻译”为单个 Ansi 字符。但它变得更糟:为了“翻译”它,使用了代码页(参见chcp 命令),因此在不同的计算机上,您可能会得到不同的结果。在我的电脑上使用type source.txt,文件看起来像─å├ç┼ç├ö,保存为Ansi 它看起来像CÃNÈ(注意:与您的ĆÇŇÔ不同) 也就是说,cmd 无法读取 Unicode(但奇怪的是在特殊情况下可以写入 (cmd /u))。如果您需要使用 Unicode/UTF 文件,请切换到另一种(编程)语言。据我所知,PowerShell应该可以处理。 @AlexeiLevenkov 谢谢你的评论 - 我不知道这会这么复杂......:/我认为在批处理文件中这样简单的替换就足以完成这项工作,我没有知道如何使用 PowerShell。 您可以尝试以chcp 1252 开头的脚本。 【参考方案1】:

根据您的comment,这实际上不是代码页问题,因为您的*.bas 文件中有ATASCII 编码。为了转换此类文件以避免反转字形,我将使用一种可以轻松以二进制模式读取文件的语言,并从值大于或等于 0x80 的每个字节中减去 0x80

无论如何,如果您确实想用代码0x8F0x80、@987654337 替换已经执行的转换过程中遗留的字符(ĆÇŇÔ @, 0xE2, resp., 根据您的活动 code page 852),我会按照以下方式进行操作,在任何转换活动中应用 code page 437,因为这定义了原始 IBM PC 的字符集,也称为OEM 字体,不应在后台发生任何不需要的字符转换:

@echo off
setlocal EnableExtensions DisableDelayedExpansion

rem // Define constants here:
set "_ROOT=%~dp0."                & rem // (full path to target directory)
set "_SOURCE=source.txt"          & rem // (name of source file)
set "_RETURN=return.txt"          & rem // (name of return file)
set "_FILTER=^[0-9][0-9]*  *REM " & rem /* (`findstr` search expression to filter
                                    rem     for specific lines; `^`  means all) */

rem // Store current code page:
for /F "tokens=2 delims=:" %%P in ('chcp') do for /F %%C in ("%%P") do set "$CP=%%C"
rem // Set code page to OEM in order to avoid unwanted character conversions:
> nul chcp 437

rem /* Specify character replacements; the `forfiles` command supports substitution
rem    of hex codes like `0xHH`, so you can specify special characters by their code
rem    in order to avoid having to embed them into this script, which might in turn
rem    lead to problems due to dependencies on the current code page; each `0x22`
rem    represents a `"` to enclose each replacement expression within quotes; each
rem    of the following replacement expression is `"` + char. + `=` + char. +`"`: */
for /F "delims=" %%R in ('
    forfiles /P "%~dp0." /M "%~nx0" /C ^
        "cmd /C echo 0x220x8F=F0x22 0x220x80=G0x22 0x220xD5=R0x22 0x220xE2=T0x22"
') do set "RPL=%%R"

rem // Change into target directory:
pushd "%_ROOT%" && (
    rem // Write into return file:
    > "%_RETURN%" (
        rem /* Read from source file, temporarily precede each line with line number
        rem    followed by a `:` in order to not lose blank lines: */
        for /F "delims=" %%L in ('findstr /N "^" "%_SOURCE%"') do (
            rem // Store current line:
            set "LINE=%%L"
            rem // Toggle delayed expansion to avoid troubles with `!`:
            setlocal EnableDelayedExpansion
            rem // Remove temporary line number prefix:
            set "LINE=!LINE:*:=!"
            rem // Filter for lines that are subject to the replacements:
            cmd /V /C echo(^^!LINE^^!| > nul findstr /R /I /C:"!_FILTER!" && (
                rem // Perform replacements one after another:
                for %%R in (%RPL%) do if defined LINE set "LINE=!LINE:%%~R!"
            )
            rem // Return resulting line:
            echo(!LINE!
            endlocal
        )
    )
    rem // Return from target directory:
    popd
)

rem // Restore former code page:
if defined $CP > nul chcp %$CP%

endlocal
exit /B

此方法仅在以下行中执行字符替换:十进制数,后跟一个或多个 SPACEs,后跟 REM(不区分大小写),然后是 空间.


现在这里有一个脚本,可以真正转换 Atari Basic (*.bas) 文件中 REM cmets 中的 ATASCII 字符,使用 certutil 转换二进制字符代码:

@echo off
setlocal EnableExtensions DisableDelayedExpansion

rem // Define constants here:
set "_TARGET=%~dp0."               & rem // (full path to target directory)
set "_SOURCE=source.txt"           & rem // (name of source file)
set "_RETURN=return.txt"           & rem // (name of return file)
set "_FILTER=^[0-9][0-9]*  *REM "  & rem /* (`findstr` search expression to filter
                                     rem     for specific lines; `^`  means all) */
set "_TEMPFN=%TEMP%\%~n0_%RANDOM%" & rem // (path and base name of temporary file)

rem // Change into target directory:
pushd "%_TARGET%" && (
    rem // Write into return file:
    > "%_RETURN%" (
        rem /* Read from source file, temporarily precede each line with line number
        rem    followed by a `:` in order to not lose blank lines: */
        for /F "delims=" %%L in ('findstr /N "^" "%_SOURCE%"') do (
            rem // Store current line:
            set "LINE=%%L"
            rem // Toggle delayed expansion to avoid troubles with `!`:
            setlocal EnableDelayedExpansion
            rem // Remove temporary line number prefix:
            set "LINE=!LINE:*:=!"
            rem // Filter for lines that are subject to the replacements:
            cmd /V /C echo(^^!LINE^^!| > nul findstr /R /I /C:"!_FILTER!" && (
                rem // Found a line, hence write it to temporary file:
                (> "!_TEMPFN!.tmp" echo(!LINE!) && (
                    rem // Convert temporary file to hex dump file:
                    > nul certutil -f -encodehex "!_TEMPFN!.tmp" "!_TEMPFN!.cnv" 4 && (
                        rem // Write to temporary file:
                        (> "!_TEMPFN!.tmp" (
                            rem // Read hex dump file line by line:
                            for /F "usebackq tokens=*" %%T in ("!_TEMPFN!.cnv") do (
                                rem // Reset buffer, loop through hex values:
                                set "BUFF= " & for %%H in (%%T) do (
                                    rem // Determine new hex value, append it to buffer:
                                    set "HEX=%%H" & set /A "FIG=0x!HEX:~,1!-0x8"
                                    if !FIG! lss 0 (
                                        rem // Value was < 0x80, hence keep it:
                                        set "BUFF=!BUFF! !HEX!"
                                    ) else (
                                        rem // Value was >= 0x80, hence subtract 0x80:
                                        set "BUFF=!BUFF! !FIG!!HEX:~1!"
                                    )
                                )
                                echo(!BUFF:~2!
                            )
                        )) && (
                            > nul certutil -f -decodehex "!_TEMPFN!.tmp" "!_TEMPFN!.cnv" 4 && (
                                type "!_TEMPFN!.cnv"
                            ) || echo(!LINE!
                        ) || echo(!LINE!
                    ) || echo(!LINE!
                ) || echo(!LINE!
            ) || (
                rem // Return resulting line:
                echo(!LINE!
            )
            endlocal
        )
    )
    rem // Clean up temporary files:
    del "%_TEMPFN%.tmp" "%_TEMPFN%.cnv"
    rem // Return from target directory:
    popd
)

endlocal
exit /B

【讨论】:

感谢您的回答。目前我没有(或不知道)访问其他语言的权限,所以我使用批处理作为“快速修复”。我不知道在我的情况下,从“每个字节”中减去 0x80 是否是最好的方法 - 我的 Atari Basic 代码不是“纯代码”,并且在汇编语言中有一些插入,由来自全范围的字符表示ATASCII 我宁愿不改变它们。所有 cmets 都在自己的行中,因此它们很容易与其余代码分开,并且它们可能是唯一需要这种转换的东西(能够阅读/理解它们)。 我明白了,所以二进制读取无济于事;有没有办法清楚地识别这些要转换的 ATASCII cmets?如果是这样,那么只能对适用的行进行转换…… 我在您链接的评论下方的评论中提供了这样一个示例:link - 它说100 REM PRINT(转换后)。在我自己答案的批处理代码中,我已经为所有不是 cmets 的行实现了跳过:IF "!line!"=="!line: REM =!" GOTO :LoopEnd. 好吧,我刚刚编辑了我的答案并为这些行建立了一个过滤器,因此其他人保持不变......【参考方案2】:

答案(正如@Gerhard 和@Compo 所建议的那样):这是错误的代码页。

如果其他人也有同样的需要,下面是我当前的工作批处理代码(在 ATARI BASIC 代码中转换反转的 ATASCII 字符)。

它转换定义的字符集(您可以添加更多/删除一些 - 只需修改字符串和字符总数)并通过在开头和结尾添加行使 cmets 更加可见每一个。

@ECHO off

rem --------------------------------------------------
rem CHECK FOR THE SOURCE FILE
rem --------------------------------------------------

IF "%~1"=="" GOTO :End

rem --------------------------------------------------
rem SET THE CODE PAGE
rem --------------------------------------------------

CHCP 1252 > NUL

rem --------------------------------------------------
rem DEFINE THE SET OF CHARACTERS TO CONVERT
rem --------------------------------------------------

SET "input_set_of_chars= Ł¤§¨cŞ«¬­R°+˛ł´u¸ą»Ľ˝ľÁÂĂÄĹĆÇČÉĘËĚÍÎĎĐŃŇÓÔŐÖ×ŘŮÚř"
SET "output_set_of_chars= #$'()*+,-.0123456789;<=>ABCDEFGHIJKLMNOPQRSTUVWXYZx"
SET "number_of_chars=52"

rem --------------------------------------------------
rem CONVERT EACH LINE OF THE SOURCE FILE
rem --------------------------------------------------

(FOR /f "delims=" %%i in (%~1) DO (
    SET "line=%%i"
    CALL :ConvertASCII
))> "%~n1-converted%~x1"
GOTO :End

rem --------------------------------------------------
rem START OF THE CONVERT SUBROUTINE
rem --------------------------------------------------

:ConvertASCII
SETLOCAL enableDelayedExpansion

rem --------------------------------------------------
rem IF THE LINE IS NOT A COMMENT THEN DO NOT CONVERT
rem --------------------------------------------------

IF "!line!"=="!line: REM =!" GOTO :LoopEnd

rem --------------------------------------------------
rem MAKE COMMENT LINE A LITTLE MORE VISIBLE
rem --------------------------------------------------

SET "line=!line: REM = REM ----------!----------"

rem --------------------------------------------------
rem CONVERT ALL DEFINED CHARACTERS
rem --------------------------------------------------

SET "counter=0"
:LoopStart
SET "input_char=!input_set_of_chars:~%counter%,1!"
SET "output_char=!output_set_of_chars:~%counter%,1!"
SET "line=!line:%input_char%=%output_char%!"
SET /a counter+=1
IF "!counter!"=="!number_of_chars!" GOTO :LoopEnd
GOTO :LoopStart

rem --------------------------------------------------
rem ECHO THE CURRENT LINE AND EXIT SUBROUTINE
rem --------------------------------------------------

:LoopEnd   
ECHO.!line!
ENDLOCAL 
EXIT /b 0

:End

【讨论】:

【参考方案3】:

如果 source.txt 未保存为 Unicode,则您的问题可能与您运行循环时的代码页有关。

以下示例切换到代码页1252West European Latin,(正如 Gerhard 在 cmets 中所建议的那样),如果不是这样的话。 虽然我假设代码页 850Multilingual (Latin I) 应该同样有效(只需根据需要将78 行上的1252 替换为所需的代码页)。

@Echo Off
SetLocal EnableExtensions DisableDelayedExpansion
If Not Exist "source.txt" GoTo :EOF
For /F "Delims==" %%G In ('2^> NUL Set _cp') Do Set "%%G="
For /F Tokens^=* %%G In ('"%SystemRoot%\System32\chcp.com"'
) Do For %%H In (%%G) Do Set "_cp=%%~nH"
If Not %_cp% Equ 1252 (Set "_cpc=TRUE"
    "%SystemRoot%\System32\chcp.com" 1252 1> NUL)
(For /F UseBackQ^ Delims^=^ EOL^= %%G In ("source.txt") Do (
    Set "line=%%G"
    SetLocal EnableDelayedExpansion
    Set "line=!line:Ć=F!"
    Set "line=!line:Ç=G!"
    Set "line=!line:Ň=R!"
    Set "line=!line:Ô=T!"
    Echo=!line!
    EndLocal)) 1> "output.txt"
If Defined _cpc "%SystemRoot%\System32\chcp.com" %_cp% 1> NUL

请注意,使用这样的For 循环会从输出中删除所有空行

【讨论】:

我刚刚在我的批处理开始时添加了一个简单的CHCP 1252 &gt; NUL 行,现在它可以工作了 - 我希望它也可以吗? PS。你知道需要它的原因是什么吗?我很困惑,因为我的 Windows 系统代码页已经设置为 1252。 好吧,命令提示符窗口似乎显示 852 - 所以这就是我需要这个修复的原因。 抱歉,这不正确,它一定不是代码页1252,如果该命令单独解决了您的问题。 (现在在您的后续 cmets 中得到证明,您的是 852. 请注意@Lex,上面的代码确定当前的代码页,保存它,更改为新的,如果还没有,执行你的命令,然后将代码页返回给指出它在命令之前。这样,您只是为了预期目的而更改代码页,而不是为了其他目的或脚本/会话,(如果您在开始时添加ChCp 1252 &gt; NUL,就会发生这种情况)。顺便说一句,如果我的回答确实解决了您的问题,并且因为它是唯一的答案,您应该将其标记为已接受,(它对网站和未来的读者都有帮助) 运行我的“固定”批处理文件(只有CHCP 1252 &gt; NUL)后,相同的命令提示符仍然显示852(不是1252),所以我认为这足以满足我的需要。 :)

以上是关于替换文本文件中的单个字符时出现意外结果的主要内容,如果未能解决你的问题,请参考以下文章

直接打印到文本显存时出现意外输出

尝试使用 AJAX 在 Laravel 中上传单个文件时出现数组到字符串转换错误

在 PostgreSQL 中将双精度转换为文本时出现意外行为

python将指定文本中的字符串替换后,生成新的文本文件。

将 ABAddressBook 中的联系人导入文本文件时出现问题

打印 UIWebView 时出现意外的分页符