Board logo

标题: [文本处理] 批处理中call子程序时的诡异情形:莫名丢失部分字符 [打印本页]

作者: namejm    时间: 2011-4-13 20:15     标题: 批处理中call子程序时的诡异情形:莫名丢失部分字符

本帖最后由 namejm 于 2011-4-14 13:49 编辑

  在用批处理抓取百度mp3的过程中,我碰到了一件十分诡异的事情,困扰了我好几天。由于完整代码行数较多,我几乎在每条语句后都echo了变量之后再pause,反复对比观察单步执行结果,苦苦跟踪了好几天,终于找到了问题的所在,从而发现了批处理call子程序时的一个bug。

  现象描述:

  我的目的是从每一个xml文件中取出第一对url,它们的位置分别在第一对<encode>和</encode>与<decode>和</decode>之间,例如,在“005_荷塘月色_凤凰传奇.xml”这个文件中,我要获取的字符串是“http://zhangmenshiting.baidu.com/data/music/536029/VndrWW16XHpoV3hpWndoXWpqWHlrW3B7VmprWXpuXHBqV3VmY6Ona3CqlqOZm3Sal2hpaZpsbZ2XaGlpZpxvbWZlZm1rmWxulZeUlmicaDg$”和“%E8%8D%B7%E5%A1%98%E6%9C%88%E8%89%B2.mp3?xcode=bf665e66ef6651f8553396c56deab3f1”,当我用正确的代码去执行的时候,是能够丝毫不差地得到这一对字符串的;当我用错误的代码去执行的时候,获取的结果竟然是“http://zhangmenshiting.baidu.com/data/music/536029/VndrWW16XHpoV3hpWndoXWpqWHlrW3B7VmprWXpuXHBqV3VmY6Ona3CqlqOZm3Sal2hpaZpsbZ2XaGlpZpxvbWZlZm1rmWxulZeUlmicaDg$”和“8DE5989C889//www.tianjinwe.com/tianjin/tjyl/201010/iGJlZGZmaGhjYmRpZ2lva2prbGVmaTc$.mp3”,与正确的执行结果相比,它在获取第二串字符串的时候,不仅把百分号对及其之间的字符都置换为空,更让人惊讶的是,它居然会把.mp3?xcode及它之后的第一个http:之间(含自身)的字符串都“吃”掉了,和后一个<encode>、</encode>这个标记对之间的字符串组合成了一个新的字符串。

  问题所在:

  经过艰苦的跟踪,发现在call子程序的时候,当第一次call子程序执行到set "str=%~1" 这一步的时候,所有的百分号对及其之间的字符串都被当做未赋值的变量引用而被置为空值了,而call子程序做<decode>字符串的替换的时候,当子程序执行到 set "str=%~1" 这一步时,竟然已经把这个xml文件中的第一个</decode>标记给“吃”掉了,而此时,替换字符串 <decode> 的动作尚未发生,for语句截取第一个</decode>之前的字符串的语句尚未执行!

  解决办法:

  把call子程序模块取消,改为在for内部处理,结果很成功。

  存在的问题:

  但这样做又引发了另外一个问题:如果我把所有的<……>和</……>这样的标记对中的字符串取出来,而这样的标记有多少个还是未知数,这种情况下,又该如何处理?如果换成call子程序的话,将可以用if+goto语句轻松实现,但call子程序在接收变量的时候,却容易丢失字符串:不仅会导致百分号对及其之间的字符串丢失,还有可能把其他一些不特定的字符串也丢掉。丢失百分号对及其之间的字符串,其原因很明了:百分号对一般会被识别为变量引用,当变量未被赋值的时候,引用这些变量,其值将为空。但丢失不特定字符串的具体原因,是我一直百思不得其解的,只能大致把这种现象和cmd的预处理机制联系起来。

以下是本次对比所用到的代码:

错误的代码:
  1. @echo off
  2. title 处理结果出错
  3. setlocal enabledelayedexpansion
  4. for %%i in (*.xml) do (
  5.     for /f "delims=" %%j in (%%i) do (
  6.         call :GetUrl "%%j" encode
  7.         call :GetUrl "%%j" decode
  8.         echo %%i
  9.         echo "!Url_encode!"
  10.         echo "!Url_decode!"
  11.         echo.
  12.     )
  13. )
  14. pause
  15. exit
  16. :GetUrl
  17. set "str=%~1"
  18. set "str=!str:*<%2>=!"
  19. for /f "delims=<" %%i in ("%str%") do set "Url_%2=%%i"
  20. goto :eof
复制代码
正确的代码:
  1. @echo off
  2. title 处理结果正确
  3. setlocal enabledelayedexpansion
  4. for %%i in (*.xml) do (
  5.     for /f "delims=" %%j in (%%i) do (
  6.         set "str=%%j"
  7.         set "str=!str:*<encode>=!"
  8.         for /f "delims=<" %%k in ("!str!") do set "Url_encode=%%k"
  9.         set "str=!str:*<decode>=!"
  10.         for /f "delims=<" %%k in ("!str!") do set "Url_decode=%%k"
  11.         echo %%i
  12.         echo "!Url_encode!"
  13.         echo "!Url_decode!"
  14.         echo.
  15.     )
  16. )
  17. pause
复制代码

另外,小小抱怨一下,新版本的论坛程序在编辑帖子的时候真不够友好,竟然找不到排图的按钮。
[attach]3713[/attach]
[attach]3715[/attach]
[attach]3717[/attach]
作者: hanyeguxing    时间: 2011-4-13 21:04

本帖最后由 hanyeguxing 于 2011-4-13 21:21 编辑

个人认为:call新命令解释器处理参数时的问题,call的新命令解释器对参数做动态扩展
解决方法:一般必须使用call时,要区分处理的字符串,如果含敏感字符则以变量继承,如果不含可以以参数传递
例如,把set "str=%~1"放进for 中set "str=%%j",这样call时还可以少传递一个参数
作者: tmplinshi    时间: 2011-4-13 21:51

本帖最后由 tmplinshi 于 2011-4-13 22:50 编辑

解决方法一:
...
set str=%%j
call :GetUrl encode
call :GetUrl decode
...


解决方法二:
  1. @echo off
  2. SetLocal EnableDelayedExpansion
  3. for %%i in (*.xml) do (
  4.     for /f "delims=" %%j in (%%i) do (
  5.         set str=%%a
  6.         set str=!str:"=!
  7.         set "str=!str:<decode>=" "!"
  8.         set "str=!str:</decode>=" "!"
  9.         set str=!str:?=?!
  10.         rem 字符串中不能含有 ? 和 *,不然会当做文件处理。
  11.         for %%a in ("!str!") do (
  12.             echo ------------------
  13.             echo %%a
  14.         )
  15.         pause
  16.     )
  17. )
复制代码

作者: CrLf    时间: 2011-4-13 22:42

原来大牛也有短脚的地方呀(不生气吧)...
我认为预处理的本质之一是:
%1>%1%>%%1>!1!
::翻译过来是cmd参数(包括call出来的参数)优先级最高,一出现就会被预处理为常数,优先级最高;其次是未使用变量延迟的%str%变量,它是在解释语块之前进行的;再次是for中的参数%%a,在每次循环之初被解释;最后是变量延迟!str!,优先级比语句的分隔符(&、&&、|、||、"等)还低,优先级仅高于重定向(重定向是否该归到语句之中呢?我感觉可能用cmd来解释重定向比较合理吧,如果真是如此,重定向的优先级必然高于语句本身)。
所以楼主出现这种“诡异”症状的根本原因是call之后,cmd先解释了%1,再解释了%1%,然后才执行echo,由于标签不能位于括号之间(根本原因也与预处理的优先级有关),所以用%~1这种形式应该是无解的,建议不用%1,改用变量延迟或者for传递参数。
作者: namejm    时间: 2011-4-14 00:21

各位的发言都很精彩
除了有深入的分析
并且还提供了比较完美的解决方案
真是一语惊醒梦中人啊
不过看来大家的重点都集中在call的时候对百分号对的处理上
尚未见有人对那些不特定字符被“吃”掉的解释
而我发这个帖子的重点正在这里
一直没有找到合理的解释
希望能有人解答我心中疑惑
作者: hanyeguxing    时间: 2011-4-14 00:47

“但这样做又引发了另外一个问题:如果我把所有的<……>和</……>这样的标记对中的字符串取出来,而这样的标记有多少个还是未知数,这种情况下,又该如何处理?”
先用sed或fr处理啊,这样再用for处理的时候就简单了。例如使用fr替换</decode>为空,<decode>为换行+:
这样只处理:开头的行就可以了
至于sed俺很少用
ps:老大最好贴个源文件出来
作者: CrLf    时间: 2011-4-14 13:06

本帖最后由 zm900612 于 2011-4-14 13:54 编辑

比较厌恶goto和call,秀一下疯狂的for嵌套,将tokens无限拓展:
  1. @echo off&SetLocal EnableDelayedExpansion
  2. for %%i in (*.xml) do (
  3. for /f "useback" %%a in ("%%i") do (
  4. set tmp=%%i
  5. for /l %%l in (1 1 1000) do (
  6. for /f "tokens=1* delims=>" %%a in ("!tmp!") do (
  7. echo %%b
  8. set "tmp=!tmp:<*=!"&set "tmp=!tmp:<*=!"
  9. pause
  10. rem 不断循环,直到轮空
  11. )
  12. )
  13. )
  14. )
复制代码

作者: CrLf    时间: 2011-4-14 13:09

还有,既然问题都是出在预处理机制对百分号产生误会上,那是否可以考虑先替换全部百分号为特殊字符,显示时再替换回来呢?
我感觉最讨厌的特殊字符不是百分号,而是感叹号,尤其是在for中...
作者: CrLf    时间: 2011-4-14 13:19

本帖最后由 zm900612 于 2011-4-14 20:46 编辑

5# lllsoslll


“那么cmd在解释道call:... 这条语句时,会对%%j再次做预处理”,对这个观点我不敢苟同,我认为问题不是处在跳转到标签之前,而是之后,更明确地说,错误是出在解释%1之后紧跟着解释了空变量。
至于call echo %%a...这样的用法之所以能对变量进行二次扩展,我认为不是call本身对其进行解释,而是call一个命令时执行了cmd /c,由cmd去解释并运行它,我相信微软的程序员也遵循从简原则,不会在一个cmd中专门为call再定做一个专门的解释模块。至于for和if不能被call,我只有一个模糊的看法,就是可能是因为for和if可以包含子语句,call也许不能执行语句中的子语句,所以不支持if和for...
作者: cjiabing    时间: 2011-4-15 01:02

以前遇到过类似的问题,可能与楼主的类似。重新发了一下,假如大家有兴趣不妨探讨一下:
FOR命令中一些符号的特殊关系 :http://www.bathome.net/thread-11887-1-1.html
作者: CrLf    时间: 2011-4-15 12:39

本帖最后由 zm900612 于 2011-4-15 12:54 编辑
说了这么一大堆,很多人看帖还不知是怎么回事,我来做个小总结,
==============================
楼主说的莫名丢失字符,,如我11楼所说
就是在cmd在预处理的时候会把%好冒号:之间的字符也当做变量来扩充,所以后 ...
lllsoslll 发表于 2011-4-15 00:30

确实,我想得不够深入,做了个实验,证明call确实在调用标签之前就已经解释了变量:
  1. @echo off
  2. set #=前
  3. prompt 此处修改了变量:
  4. ::本人比较喜欢玩,嘿嘿
  5. for %%a in (%%#%%) do (
  6. call echo for中call前 #=%%a
  7. echo ______________
  8. call :echo %%a
  9. )
  10. echo;&pause
  11. exit
  12. :echo
  13. echo;&echo call中set前 #=%1
  14. echo on
  15. set #=后
  16. @echo off
  17. echo;&echo call中set后 #=%1
复制代码
几乎不用call,所以以前没研究到这程度。sos兄功力真深厚啊,膜拜一下。
看来误区还是要在讨论中容易被发现和纠正啊
作者: CrLf    时间: 2011-4-15 12:51

至于!的问题,因为楼主的xml中并没有声明元素,所以这个顾虑可有可无,特殊情况下当然要考虑特殊字符,但是一般情况下够用就可以了吧...
作者: Batcher    时间: 2011-4-17 12:11

1# namejm


编辑帖子时“排图的按钮”是什么?
作者: caruko    时间: 2011-4-17 23:17

本帖最后由 caruko 于 2011-4-17 23:50 编辑

%:= 果然会导致问题
作者: namejm    时间: 2011-4-17 23:23

1# namejm


编辑帖子时“排图的按钮”是什么?
Batcher 发表于 2011-4-17 12:11

当我想把那些图片居中、左对齐或右对齐的时候,竟然找不到按钮来进行这些操作,记得在DZ6下是可以的。




欢迎光临 批处理之家 (http://www.bathome.net/) Powered by Discuz! 7.2