ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Windows任务计划+批处理FTP脚本实现无人值守自动传输

Windows任务计划+批处理FTP脚本实现无人值守自动传输

简介:围绕批处理命令与FTP自动传输整理的Word文档,面向Windows及UNIX环境下的系统运维人员和网络管理员,用于解决每天重复登录、手工收发文件既繁琐又容易出错的问题。文档以1.ftp与1.bat两个文本的编写为主线,说明如何用 ftp -n -s 参数静默调用脚本,如何取舍二进制与ASCII两种传输模式,如何用Mget与Mput完成收发切换,并把批处理加入“任务计划”,按周期自动执行;同时给出UNIX下shell脚本的写法与可执行权限设置,便于跨平台对照。包内共1个doc文件,约44KB,正文含脚本片段、步骤说明与应用案例,可直接当模板改造使用。已有2121人学习,适合需要搭建定时传输任务、减少人工干预的初、中级运维读者参考。

1. 两个文本文件替代一个定时任务:这套 FTP 自动传输方案的定位

半夜两点半,一台常开的 Windows 机器要从对端拉走当天的数据文件,第二天早上八点前必须到位。这个需求用「批处理命令实现 FTP 文件的自动传输」是最省事的解法:一个.ftp文件写会话命令,一个.bat文件切目录并把脚本传给ftp.exe,最后交给系统自带的任务计划按周期触发。全程不装任何软件,运行时就是C:\Windows\System32\ftp.exe。适合的场景很明确:Windows 常开、对端是 UNIX 或另一台 Windows、文件量几十到几百个、总量几百 MB 以内、对端只提供标准 FTP。不适合的同样明确:要断点续传、要加密链路、要做复杂重试和告警。下面按「脚本怎么写 → 批处理怎么包装 → 计划任务怎么挂 → 哪里会翻车」拆开说。

2. 拆 1.ftp:从 open 到 bye 的九行命令,每行都在决定成败

2.1 脚本文件的本质是把键盘输入重定向成文件

ftp.exe本身是个交互式程序,你在提示符下敲open、user、cd,它读一行执行一行。-s:文件名做的事只有一件:把标准输入从这个文件读,而不是从键盘读。所以脚本里能写什么,完全取决于客户端在交互模式下认什么命令,没有额外语法,没有变量替换,没有 if 判断,也没有循环。这一点想明白,后面很多「为什么我写了变量它不认」的疑问就自然消失了。

把原料里的脚本摊开看,实际长这样:

open 10.30.15.3 user anonymous 123456 cd data bin prompt mget *.* bye quit exit

open建立控制连接,默认端口 21,如果对端改了端口就写成open 10.30.15.3 2121。user后面跟用户名和密码,这一行实际对应 FTP 协议里的 USER 和 PASS 两条命令;密码不写在user同一行也可以,下一行单独写,客户端会把它当 PASS 发出去。但要注意,如果user后面只写了用户名、下一行又忘了写密码,客户端会当场停在Password:提示上等人输入,无人值守场景下就是一直挂着直到超时。

bye、quit、exit三个是同一个意思,都是发 QUIT 并断开。写一个就够,多写不报错,但日志里会多出一段无意义的响应。原料里三行都写了,这属于「写了也没坏处,但没必要」。真正需要留意的是cd data这一行:cd切的是远端目录,本地目录要靠lcd或者批处理里的cd来处理,这两个概念在初学时最容易混。

2.2 bin 与 ascii:模式选错,文件看着传完了其实是坏的

bin是binary的缩写,对应 FTP 协议的TYPE I;不写这一行,Windows 客户端默认走TYPE A,也就是 ASCII 模式。两者的差别不在速度,在于是否对字节做转换。ASCII 模式会按行处理,把本地的行尾转成网络标准 CRLF,对端再转成自己的行尾;二进制模式逐字节搬运,一个字节都不动。

原料里有一句「如果是 Windows 之间传,去掉 bin 一行就可以了」,这话得辩证看。两台 Windows 之间传纯文本,ASCII 模式确实能正常工作,因为两边的行尾都是 CRLF,来回转换等于没转。但一旦传的是压缩包、可执行文件、数据库备份,去掉bin就是灾难——ASCII 模式会把文件里恰好出现的0x0D、0x0A字节当成行尾改写,传完大小对不上,解压报 CRC 错误,备份文件恢复不了。更麻烦的是这种损坏不会当场报错,日志里照样是226 Transfer complete,等你去恢复数据的时候才发现。

按文件类型选模式的对照关系,我一般按这张表来定:

文件类型传输模式选错的后果
.txt/.csv/.log/.sqlascii用 bin 传,UNIX 端每行末尾多个^M,解析脚本读出错位
.zip/.rar/.7z/.gzbin用 ascii 传,压缩包损坏,解压直接报错
.exe/.dll/.sobin用 ascii 传,可执行文件加载失败
.dmp/.bak数据库备份bin用 ascii 传,备份集无法恢复,且不易察觉
.jpg/.png/.pdfbin用 ascii 传,图片花屏、PDF 打不开

实操上的做法是按方向拆脚本,而不是在一个脚本里来回切模式。收文本一个.ftp,收二进制另一个.ftp,各自在自己那个脚本开头写死模式。混在一个脚本里也不是不行,ascii和bin可以在会话中反复切换,但脚本一长就容易出现「切过去忘了切回来」的问题,排查起来很费劲。

2.3 prompt 与 mget/mput:通配符的两次展开和确认开关

prompt在 Windows 客户端里是个切换开关,敲一次关掉交互确认,再敲一次打开。关掉之后,mget一次匹配到十个文件也不会逐个问mget xxx?。不写这一行的后果,在无人值守场景下很直观:任务计划状态一直显示「正在运行」,日志停在一长串mget a.txt?上,等到超时才退出,一个文件都没传下来。原料里把prompt放在mget前面,顺序是对的,这个顺序不能反。

mget *.*里的通配符不是客户端在本地展开的,而是原样发给服务器,由服务器的NLST或LIST来匹配。这带来两个边界。第一,*.*在多数 UNIX 服务端只能匹配「文件名里带点」的文件,像README、Makefile这种不带点的文件匹配不到,要改成mget *。第二,mget不递归,远端data目录下的子目录里的文件不会被带下来;真要按层级收,常见做法是一个子目录写一个cd+mget段落,或者干脆在对端先打包成一个文件再传。

发送方向同理,把mget换成mput,cd换成远端的上传目标目录,本地目录靠lcd指定。一个改好之后可直接抄的版本:

open 10.30.15.3 user anonymous 123456 binary prompt lcd C:\datatran cd data mget *.txt mget *.csv bye

lcd这一行很多示例里不写,靠批处理里的cd来兜底。两种做法都行,但显式写lcd更稳,因为批处理的当前目录会被任务计划的「起始于」字段影响,而lcd是写死在脚本里的绝对路径,不受外部干扰。密码和用户名并到user一行也建议这么改,少一行就少一个错位的机会。

3. 1.bat 的封装层:ftp -n -s 几个参数各自在挡什么

3.1 -n 不是「不登录」,是「不要自动匿名登录」

批处理这一层看着只有三行,但ftp后面那几个参数每一个都在解决一个具体问题。先看原料给的写法:

C: cd datatran ftp -n -s:"c:\1.ftp"

-n是最容易被误解的一个。它的准确含义是「启动时抑制自动登录」,也就是不主动发USER anonymous去尝试匿名登录。如果不加-n,客户端启动后会先做一次匿名登录尝试,再执行脚本里的open和user,结果是日志里凭空多出一段 331/230 响应,某些对失败次数敏感的服务端还会因此触发限制。脚本里既然已经写死了用户名密码,-n就应该加上。

-s:后面紧跟脚本路径,冒号和文件名之间不能有空格,这一点和多数命令行工具的-s 文件名写法不一样。路径里带空格的时候用引号包住整个参数,写成-s:"C:\my scripts\1.ftp"。原料用的是c:\1.ftp,脚本放在盘符根目录,这个习惯在单人环境里没问题,但放到多台机器统一部署时最好归到一个固定目录,比如C:\scripts\,否则重装系统或者换盘符就得挨个改。

其余几个参数按需加,我整理成一张表备查:

参数作用什么时候必须加
-n禁止启动时自动匿名登录脚本里已写死账号密码时
-s:file指定命令脚本文件无人值守场景总是
-v抑制远端响应输出日志要干净、只留关键行时
-i多文件传输时关闭交互确认脚本里漏写prompt时的兜底
-d打开调试,输出每条 FTP 命令排查「为什么这条没执行」时临时加
-A强制匿名登录对端只开放匿名访问时
-w:size设置接收缓冲区大小大文件吞吐调优,一般不用动

-i和脚本里的prompt是同一个效果的两条路径,建议两个都写:脚本里写prompt是为了脚本本身独立可读,命令行加-i是为了别人临时改脚本时忘写也不至于卡死。这种「双保险」在无人值守场景里不算冗余,算必要成本。

3.2 C: 和 cd:批处理的当前目录才是文件落地的地方

C:切盘符,cd datatran切目录,这两行的作用是给后面启动的ftp.exe定一个当前工作目录。mget下载下来的文件默认落在当前目录,也就是这两行切过去的C:\datatran。如果这两行漏了,ftp.exe继承的是父进程的当前目录,手工双击 bat 的时候父进程是资源管理器,目录可能恰好是你想要的;一旦挂进任务计划,父进程是taskeng.exe或svchost.exe,当前目录变成C:\Windows\System32,文件全落到系统目录里,还因为权限问题写不进去。

更稳的写法是一行到位:

cd /d C:\datatran

/d参数让cd同时处理盘符切换和目录切换,不用先写C:再写cd。原料里分两行的写法在C:盘内没问题,但如果哪天目录换到D:盘,只写cd D:\datatran是不生效的——它只改 D 盘的当前目录,不切当前盘,文件还是落在 C 盘。这是个很典型的老坑,/d能一次性绕开。

目录切完,ftp这一行建议加上输出重定向,把会话日志留到文件里:

ftp -n -v -s:"C:\scripts\1.ftp" >> "C:\logs\ftp_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log" 2>&1 echo [%TIME%] exit=%ERRORLEVEL% >> "C:\logs\ftp_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"

>>是追加,避免同一天多次运行互相覆盖;2>&1把错误流也并进同一个文件,否则连接失败的提示只会闪一下窗口就没了。最后那行echo exit=%ERRORLEVEL%很关键——%ERRORLEVEL%是紧跟上一条命令的退出码,中间隔了别的命令就被覆盖了,所以必须紧贴着写。

3.3 先手工跑通再挂计划:验证的三个观察点

挂任务计划之前,一定先在命令行里手工跑一遍,而且要模拟任务计划的运行环境,不是在资源管理器里双击。打开cmd,cd到一个不相干的目录,再执行C:\scripts\1.bat,这样才能暴露相对路径的问题。

跑完之后按三个点检查日志。第一,看有没有230 Login successful,没有的话是账号、密码或者对端权限的问题,跟脚本后面的命令无关。第二,数226 Transfer complete出现了几次,和远端目录里的文件数对一下,少几个就是通配符没匹配上或者某个文件被跳过。第三,比对一个具体文件的大小,本地和对端一致才算真的传完;如果用的是 ASCII 模式传二进制文件,大小可能一致但内容已经坏了,这种情况只能靠校验和发现。

%DATE%这个变量有个隐性依赖:它的格式跟着系统区域设置走。中文系统默认是2024/05/12 周日这种形态,%DATE:~0,4%取到年份没问题;换成英文系统就是Sun 05/12/2024,同样一段截取取到的是星期几的缩写,日志文件名直接乱掉。要跨机器部署的话,把时间戳换成wmic os get localdatetime或者用 PowerShell 的Get-Date -Format yyyyMMdd更保险,别在这个细节上栽跟头。

4. 挂进任务计划:无人值守必须拧对的三个开关

4.1 图形向导和 schtasks 命令行,选哪个

老系统上的路径是「开始 → 程序 → 附件 → 系统工具 → 任务计划 → 添加任务计划」,一路下一步填周期和密码就完事。新系统(Win7 之后)改叫「任务计划程序」,在运行框里敲taskschd.msc打开,左侧「创建基本任务」适合手工配一次,「创建任务」能配的选项更全。图形界面的好处是能看见每个开关的中文说明,坏处是配十台机器要点十遍。

批量部署我一般直接用schtasks:

schtasks /create /tn "FTP_Auto_Transfer" /tr "cmd /c C:\scripts\1.bat" /sc daily /st 02:30 /ru SYSTEM /rl HIGHEST /f

参数逐个对着看:

参数含义需要注意的地方
/tn任务名同名会冲突,配/f直接覆盖
/tr要执行的命令路径带空格必须用引号,建议外套一层cmd /c
/sc调度周期daily / weekly / minute / onlogon / onstart
/st开始时间24 小时制 HH:mm,不带秒
/mo间隔配/sc minute /mo 15表示每 15 分钟
/ru运行账户填 SYSTEM 时不需要密码
/rp账户密码/ru指定普通账户时不填会退化成「仅登录时运行」
/rl权限级别HIGHEST 需要当前是管理员
/f同名覆盖不加会弹交互确认,脚本里就卡住了

/tr外面套cmd /c是刻意为之。任务计划对「程序路径」和「参数」是分开解析的,直接写C:\scripts\1.bat有时会被当成程序名去找,路径里一旦有空格就断在中间。套一层cmd /c之后,整条命令交给命令解释器处理,路径、重定向、&&连接符都按批处理的规则走,行为更可控。

4.2 「只在用户登录时运行」和「不管用户是否登录都运行」

这一组选项是无人值守能不能成立的分水岭。图形界面里在「常规」选项卡顶部,两个单选按钮:上面那个是「只在用户登录时运行」,下面那个是「不管用户是否登录都运行」。向导默认选的是上面那个,因为它不需要存密码。

选上面的后果是:人一下班注销,任务就停。有些环境里用户不注销、只是锁屏,任务还能跑,于是测试的时候一切正常,正式跑了一周突然开始抽风——因为中间有人重启了机器,登录会话没了。这种「间歇性失败」最难查,因为失败的时候没人在现场看。

选下面的选项要输入账户密码,密码存在系统的凭据存储里。这里有个后续麻烦:域环境或者本地安全策略要求定期改密码,密码一改,任务计划里存的旧密码就失效,任务开始报0x8007052E(登录失败)。要么在改密码的流程里把任务计划一起更新,要么干脆把运行账户换成SYSTEM。

SYSTEM的好处是不需要密码、不受登录会话影响、权限足够高。代价是它看到的网络环境是机器本身的,不是某个登录用户的,用户手工映射的网络驱动器盘符它一律看不见,必须用 UNC 路径。纯 FTP 传输不涉及映射盘,所以SYSTEM是个很省心的选择;但如果批处理里还要往网络共享目录里搬文件,就得换回普通账户加存密码的方式。

4.3 起始于、失败重试和「上次运行结果」

图形界面里「操作」那一页有个「起始于(可选)」字段,很多人跳过不填。这个字段设的是任务启动时进程的当前目录,不填的话就是C:\Windows\System32。批处理里如果已经写了cd /d,这个字段填不填无所谓;如果批处理依赖相对路径,这里必须填,否则就是前面说的那种「手工跑正常、计划跑找不到文件」。

schtasks命令行没有直接对应「起始于」的参数,要设的话得导出 XML 改WorkingDirectory节点,或者把/tr写成cmd /c "cd /d C:\datatran && 1.bat"。后者更省事,也不依赖 XML 的格式版本。

「如果任务失败,按以下频率重新启动」这个设置在 FTP 场景里很有用。对端偶尔网络抖动、服务端连接数满了、DNS 解析慢,都会导致一次连接超时,重试两三次基本能覆盖掉。频率设 5 分钟、重试 3 次是个比较稳的组合,间隔太短会在对端还没释放连接的时候又顶上去。

跑完之后看「上次运行结果」那一列,几个常见码记一下:0x0是成功,0x1是一般性错误(通常是脚本里的命令返回了非零),0x2是找不到文件(路径写错了),0x41301是任务正在运行(不是错误,是还没跑完),0x8007052E是登录失败(密码失效)。看到0x0也别急着放心,前面说过,FTP 连不上但脚本正常退出,退出码也可能是 0,真正的判断要看日志内容。

5. 五次真实环境故障:从卡在 y/n 到「只在登录时运行」

下面这几条都是在这类脚本上真实遇到过的,按「现象 → 原因 → 解决」写,遇到类似症状可以直接对照。

5.1 现象:任务状态一直「正在运行」,日志停在一串mget xxx?

原因:脚本里漏了prompt,mget匹配到多个文件时逐个等确认,无人值守环境下没人敲 y,进程一直挂着直到超时退出。原料里的脚本是有prompt的,但很多人抄的时候只抄了open、user、cd、mget、bye几行,把prompt和bin当成可选行略掉了。

解决:脚本里mget之前必须有一行prompt,命令行再加-i做兜底。已经卡住的任务要在任务计划里手动「结束」掉,否则下一次触发会因为「任务已在运行」被跳过,看起来像是彻底不跑了。

5.2 现象:手工双击 bat 一切正常,挂进计划就报「找不到文件」

原因:两种可能。一是批处理里没有cd /d,任务计划的当前目录是System32,相对路径全部失效。二是任务计划的「起始于」为空,而 bat 里引用脚本时用了相对路径,比如-s:1.ftp而不是-s:C:\scripts\1.ftp。

解决:bat 第一行写cd /d C:\datatran,脚本路径一律用绝对路径,/tr外面套cmd /c。三件事一起做,这类问题基本不再出现。

5.3 现象:压缩包传过去解压报 CRC 错误,但文件大小差不多

原因:用了 ASCII 模式传二进制文件。原料里那句「Windows 之间传就去掉 bin」被照搬到所有场景,结果传的是 zip、dmp、exe。ASCII 模式把文件里恰好出现的0x0D0A当成行尾改写,大小差别不大,内容已经错了。

解决:二进制文件一律bin,文本文件才用ascii,按文件类型拆成不同脚本。已经传坏的文件没法修,只能重传;如果对端有历史备份,比对一下大小和校验和就能确认是哪一批坏的。

5.4 现象:白天测试都正常,下班之后任务就再也不跑了

原因:任务计划用了默认的「只在用户登录时运行」。测试的时候人坐在机器前,会话一直在;下班注销或者机器重启之后没人登录,任务就没有可用的登录令牌,触发时直接被跳过。图形界面里这个选项藏在「常规」页顶部,向导里根本不会提示。

解决:改成「不管用户是否登录都运行」并输入密码,或者把运行账户换成SYSTEM。改完之后做一次验证:注销当前用户,等一个触发周期,再登录进来看「上次运行结果」和日志文件有没有新内容。不注销就验证的,等于没验证。

5.5 现象:账号密码都对,就是登录失败,日志里user那一行被截断了

原因:FTP 脚本按空格分词,密码里带空格、#、;这类字符时,user 用户名 密码这一行会被拆成多段,客户端把第二段当成密码,后面的当成未知命令。原料示例里密码是123456,看着没问题,换成真实环境的复杂密码就中招。

解决:最直接的办法是把密码改成不含空格和特殊符号的字符串。改不了的话,就别把密码写死在脚本里,改成每次运行前用echo生成一个临时脚本,跑完删掉——虽然多了几步,但至少不会被分词坑到。另外,把明文密码放在.ftp文件里本身就有风险,文件权限要收紧到只有运行账户可读。

6. 从「能跑」到「跑坏了知道」:退出码判断、日志计数与过期清理

脚本能定时跑起来只是及格线,真正省心的是它出问题的时候你能第一时间知道。ftp.exe的退出码信息量很有限,连接失败返回 2,命令执行中出错返回 1 或 0,光看退出码判断不了「文件到底传没传全」。可行的办法是回到日志里数226 Transfer complete出现的次数,和预期文件数比。下面这段是可以直接用的收尾逻辑:

@echo off setlocal set LOGDIR=C:\logs if not exist "%LOGDIR%" md "%LOGDIR%" set LOG=%LOGDIR%\ftp_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log cd /d C:\datatran ftp -n -v -s:"C:\scripts\1.ftp" > "%LOG%" 2>&1 rem 统计成功传输次数,和预期文件数比对 set EXPECT=12 for /f %%c in ('findstr /c:"226 Transfer complete" "%LOG%" ^| find /c /v ""') do set ACTUAL=%%c if not "%ACTUAL%"=="%EXPECT%" ( echo [%DATE% %TIME%] 传输数不符 预期=%EXPECT% 实际=%ACTUAL% >> "%LOGDIR%\alert.log" exit /b 1 ) rem 清理 7 天前的日志,避免目录无限膨胀 forfiles /p "%LOGDIR%" /m *.log /d -7 /c "cmd /c del @path" >nul 2>&1 exit /b 0

关键在for /f那一行:findstr筛出所有含226 Transfer complete的行,管道交给find /c /v ""计数。find /c单独用会要求输入文件名,配上/v ""表示「统计所有不含空串的行」,也就是统计全部行,这是批处理里数行数的老技巧。EXPECT要按实际情况改,文件数每天变的话可以改成和远端目录列表比对,或者干脆只判断「实际数是否大于 0」。forfiles /d -7按修改时间筛出 7 天前的文件,/c "cmd /c del @path"逐个删掉,日志目录就不会越堆越大。

还有一个容易被忽略的点:%ERRORLEVEL%在setlocal作用域里是即时值,但在if块里引用时如果是延迟展开环境,要用!ERRORLEVEL!。上面的写法把ftp和判断拆在两条独立语句里,避开了这个坑,代价是丢掉了ftp的原始退出码。要两个都保留的话,在ftp那行之后立刻set RC=%ERRORLEVEL%,再用%RC%判断。

从那以后我每次挂无人值守任务,都强制走一遍「手工跑 → 看日志 → 注销会话再跑一次」这三步,一步都不省。省下的那十分钟,往往要用一个通宵来找回来。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进