
做老系统维护的人大概都有过这种经历接手一个跑了七八年的站点数据库连得挺好突然业务方说要在后台加个“公告”或者要读一份由别的系统定时丢过来的数据文件。翻遍代码发现这台机器上根本没有可用的数据库表可以塞也没有上传组件能装。这时候最省事的做法就是把内容落到一个 TXT 文件里用 ASP 直接读写。如何用ASP读写TXT文本文件中的内容这个问题看起来像是入门教程的第一课但我见过太多人在这上面栽跟头——中文变乱码、文件被锁住写不进去、并发时两段日志糊在一起、权限给了还是报“拒绝访问”。这些坑的共同点是能跑通的代码十分钟就能写出来跑不稳的原因却要花一整天去查。这篇内容面向的是还在维护经典 ASP 站点的开发者也适合刚接触 FileSystemObject 的同学对照着看。我不会只贴几行OpenTextFile就结束而是把方案选型、编码处理、写入策略、权限配置、并发规避、错误排查这几件事拆开讲透代码都是可以直接复制到.asp文件里跑的。读完之后你应该能做到知道什么时候该用 TXT、什么时候必须换方案能稳稳地读写 ANSI、UTF-8、UTF-16 三种编码能写出不会写出“半截文件”的写入逻辑遇到报错能凭错误号直接定位到根因。1. 场景判断与方案选型别一上来就写 OpenTextFile写文件操作代码之前先花五分钟想清楚两件事这个数据到底该不该放在 TXT 里以及用什么对象去碰这个文件。这两个判断做错了后面写再多代码都是白费。我在实际项目里见过把手写配置存进 TXT 结果被用户误删的也见过用 FSO 去读一个 200MB 的日志文件把整个应用程序池拖垮的都是选型阶段的锅。1.1 什么情况下 TXT 才是对的选择TXT 的本质优势只有一个跨进程、跨语言、零依赖。别的系统不管是老式定时任务、桌面小工具还是另一台机器上的脚本不需要连你的数据库不需要懂你的表结构把文件往约定目录一丢你的 ASP 页面就能读到。这个特性在系统对接、数据交换、简单配置、操作留痕这几类场景里是刚需也是 TXT 至今没被淘汰的原因。反过来说只要不满足“跨进程交换”这一条TXT 基本都是错的选择。需要多用户同时改的内容、需要按条件检索的内容、需要保证事务一致性的内容一律不要往 TXT 里塞。我碰到过一个项目把用户留言存在按日期命名的 TXT 里结果两个人同时提交时内容互相覆盖最后只能连夜改成数据库。判断标准很简单这份数据会不会被两个以上的请求同时写会就别用 TXT。还有一类容易被忽略的情况是“数据量”。TXT 适合几十 KB 到几 MB 的量级超过 10MB 的文本用 ASP 去整读整写内存和响应时间都会很难看。真要处理大文件思路应该是流式分块读、边读边处理、结果另存而不是一口气ReadAll。1.2 FileSystemObject 与 ADODB.Stream 的分工ASP 里能操作文本文件的对象主要有两个很多人只知道第一个。Scripting.FileSystemObject简称 FSO是最常见的选择它的OpenTextFile、CreateTextFile用起来直观配套的FileExists、FolderExists、CreateFolder、DeleteFile、GetFile也很齐全做文件存在性判断、目录创建、删除这类管理动作非常顺手。它的短板在编码OpenTextFile的format参数只认三档——ASCII/ANSI、UnicodeUTF-16LE、系统默认没有 UTF-8 这一档。这意味着只要你的 TXT 是 UTF-8 编码现在绝大多数跨系统交换的文件都是FSO 读出来就是一片乱码写出去也不是对方要的格式。ADODB.Stream是补这个短板的。它支持通过Charset属性指定utf-8、gb2312、unicode等编码读写都能精确控制还能处理 BOM。代价是它的 API 相对绕需要先Open再操作再Close二进制和文本模式切换时要重新设置Type和Position。我的常规组合是文件管理动作交给 FSO内容读写交给 ADODB.Stream。判断文件在不在、目录建没建、要不要清理过期文件用 FSO 一句话搞定真正读内容写内容尤其是涉及中文和 UTF-8 的一律走 Stream。这样既拿到了 FSO 的便利又避开了它的编码死角。1.3 编码这一关过不去后面全白搭编码是 TXT 读写里最容易翻车、也最容易被低估的一环。同样一个“你好”存成 ANSI简体中文环境下是 GB2312/GBK、UTF-8、UTF-8 带 BOM、UTF-16LE字节序列完全不同。你用错方式打开看到的就是“浣犲ソ”或者一堆问号。判断一个未知 TXT 的编码最土但最有效的办法是用十六进制编辑器看文件头文件开头字节编码判断FSO 能否正确处理Stream 处理方式EF BB BFUTF-8 带 BOM不能会读出乱码Charsetutf-8读取时自动跳过 BOM无 BOM中文为三字节序列UTF-8 无 BOM不能Charsetutf-8正常读FF FEUTF-16LE能format-1Charsetunicode无 BOM中文为两字节ANSI/GBK能format-2或0Charsetgb2312FE FFUTF-16BE不能经典 Stream 不支持需转码这里有个关键认知TristateUseDefault值为 -2取的是服务器的系统区域设置不是“自动识别”。很多教程把它当成万能档位实际上在中文 Windows 上它就等于 ANSI读 UTF-8 文件照样乱码。我在排查一个“服务器换了之后中文全乱”的问题时最后发现就是原代码写了-2而新服务器的区域设置不同行为跟着变了。写文件时更要注意FSO 的CreateTextFile(path, True, True)第三个参数为True时写的是 UTF-16LE 并自动加 BOM文件体积会翻倍而且很多下游系统不认。如果对方明确要 UTF-8就必须用 Stream 写并且决定要不要 BOM——这个话题在 3.1 节会展开。注意把.asp源文件本身保存成什么编码和它读写的 TXT 是什么编码是两件独立的事。源文件是 ANSI 而 TXT 是 UTF-8 时你的代码里写的中文常量会先按 ANSI 解析再按 UTF-8 输出结果依然错。想彻底避免源文件统一存成 UTF-8 无 BOM并在页面首行加% LanguageVBScript CodePage65001 %。2. 读取从整读、逐行到分块同样是“读文件”三种写法适用的场景完全不同。选错了不会报错但会在某个流量高峰突然把站点拖慢。下面按使用频率从高到低讲每种都给出完整可跑的代码和明确的适用边界。2.1 ReadAll最快也最容易吃满内存ReadAll一次性把整个文件读成一个字符串代码最短处理逻辑也最简单——拿到字符串之后你想Split按行拆、想InStr找关键字、想正则匹配都行。适合配置文件、模板片段、几百行以内的数据文件。Function ReadTextAll(filePath) Dim fso, ts, content Set fso Server.CreateObject(Scripting.FileSystemObject) If Not fso.FileExists(filePath) Then ReadTextAll Set fso Nothing Exit Function End If Set ts fso.OpenTextFile(filePath, 1, False, -2) content ts.ReadAll ts.Close Set ts Nothing Set fso Nothing ReadTextAll content End Function这段代码有几个细节值得说。OpenTextFile的第二个参数1是ForReading第三个参数False表示文件不存在时不创建读模式这个参数其实无效但写上更明确第四个参数-2是系统默认编码。ts.Close必须调用不关句柄的话文件会一直被占用后面想写这个文件就会报“另一个程序正在使用此文件”这个坑我在 4.3 节还会再提。ReadAll的边界在哪我的经验值是 2MB。低于这个量级一次性读进来完全没问题超过 2MBASP 的内存占用会明显上升而经典 ASP 是跑在w3wp.exe里的多个并发请求同时ReadAll一个大文件很容易触发应用程序池的内存回收表现就是站点突然变慢甚至 503。真要处理大文件看 2.3 节。还有一点ReadAll拿到的字符串里保留了原始的换行符vbCrLf你用Split(content, vbCrLf)拆行时如果文件是 Unix 换行只有LF就会拆不开。稳妥的写法是先统一content Replace(content, vbCr, vbLf) content Replace(content, vbLf vbLf, vbLf) Dim lines lines Split(content, vbLf)2.2 ReadLine 与 AtEndOfStream 的正确配合逐行读是处理“一行一条记录”的文件的标准姿势比如每行一个手机号、每行一条keyvalue配置。它的好处是内存占用恒定无论文件多大都只占一行的内存坏处是每读一行都有一次方法调用开销几万行以上时性能不如ReadAll后Split。Sub ReadLineByLine(filePath) Dim fso, ts, line, lineNo Set fso Server.CreateObject(Scripting.FileSystemObject) If Not fso.FileExists(filePath) Then Exit Sub Set ts fso.OpenTextFile(filePath, 1, False, -2) lineNo 0 Do While Not ts.AtEndOfStream line ts.ReadLine lineNo lineNo 1 这里处理每一行 Response.Write lineNo : Server.HTMLEncode(line) br Loop ts.Close Set ts Nothing Set fso Nothing End SubDo While Not ts.AtEndOfStream这个写法是标准范式但有个很多人不知道的坑AtEndOfStream在文件最后一行没有换行符时行为是正确的但如果你在循环里同时用了ReadLine和SkipLine判断就会错位。另外空文件的情况下AtEndOfStream一开始就是True循环体一次都不进这是符合预期的不需要额外处理。另一个高频错误是忘了Server.HTMLEncode。TXT 里的内容直接Response.Write出去如果里面有script就是一个现成的 XSS 漏洞。这个和文件读写本身无关但在这类“读文件展示”的场景里出事概率极高我把它当铁律凡是来自文件的内容输出前一律 HTMLEncode。2.3 Read(n) 分块大文件的稳妥打法文件超过几 MB 又必须处理时用Read(n)按固定块大小读。经典 ASP 里没有真正意义上的异步流所以这里的“分块”主要是为了控制单次内存峰值和避免超时而不是提升速度。Sub ReadByChunk(filePath, chunkSize) Dim fso, ts, buf, total If chunkSize 0 Then chunkSize 8192 Set fso Server.CreateObject(Scripting.FileSystemObject) If Not fso.FileExists(filePath) Then Exit Sub Set ts fso.OpenTextFile(filePath, 1, False, -2) total 0 Do While Not ts.AtEndOfStream buf ts.Read(chunkSize) total total Len(buf) 分块处理 buf注意块边界可能切断一行 Loop ts.Close Set ts Nothing Set fso Nothing End Sub这里必须提醒一件事分块读会在任意字节位置切断。如果按 8192 字符切第 8192 个字符很可能落在一行中间直接对这半行做Split就会得到错误结果。正确做法是维护一个“残留缓冲”每次读完一个块从尾部找到最后一个换行符把换行符之前的部分处理掉之后的部分拼到下一次读取的开头。Dim carry, full, pos carry Do While Not ts.AtEndOfStream full carry ts.Read(chunkSize) pos InStrRev(full, vbLf) If pos 0 Then ProcessLines Left(full, pos - 1) carry Mid(full, pos 1) Else carry full End If Loop If Len(carry) 0 Then ProcessLines carryInStrRev是 VBScript 里从右往左找的函数正好用来定位最后一行。这套“残留拼接”的思路在所有流式处理里都是通用的值得记下来。2.4 读取 UTF-8 / Unicode 文件的实操方案到重点了。前面说过 FSO 处理不了 UTF-8那用ADODB.Stream怎么读核心就三行设Type为文本、设Charset、LoadFromFile。Function ReadUtf8Text(filePath) Dim stm, result ReadUtf8Text On Error Resume Next Set stm Server.CreateObject(ADODB.Stream) stm.Type 2 adTypeText stm.Charset utf-8 stm.Open stm.LoadFromFile filePath result stm.ReadText stm.Close Set stm Nothing If Err.Number 0 Then Err.Clear Exit Function End If On Error GoTo 0 去掉可能的 BOM If Len(result) 0 Then If AscW(Left(result, 1)) -257 Or AscW(Left(result, 1)) 65279 Then result Mid(result, 2) End If End If ReadUtf8Text result End Function这段代码有几个点需要解释。第一Type 2必须在Open之前设置。如果在已经打开的 Stream 上改Type会抛“操作在此上下文中不允许”的错误这是初学最常见的报错。第二LoadFromFile会自动识别并处理 UTF-8 的 BOM所以读带 BOM 的文件时通常不需要手动去 BOM。但不同版本的 ADO 表现略有差异保险起见加一段判断AscW返回65279即HFEFF说明第一个字符是 BOM 字符直接砍掉。第三这里用了On Error Resume Next。很多 ASP 教程把它当万能药随手加上就不管了结果是错误被吞掉、逻辑继续往下跑、最后在上层报一个完全不相关的错。正确的做法是每次On Error Resume Next之后立刻检查Err.Number并Err.Clear检查完视情况On Error GoTo 0恢复正常错误处理。这个习惯能省掉大量排查时间。读 GBK 编码的文件把Charset换成gb2312即可读 UTF-16LE 换成unicode。注意gb2312这个字符集在 ADO 里实际覆盖的是 GBK 范围绝大多数简体中文文件都能读。3. 写入覆盖、追加和防半截文件写入看起来比读取简单实际上出事的概率更高——因为写入涉及并发、涉及文件锁、涉及“写了一半程序崩了”这种最难查的情况。这一节把三种写入模式的差异、日志追加的完整写法、以及一种能让写文件“要么全成要么全不成”的技巧讲清楚。3.1 三种打开模式与参数对照FSO 的OpenTextFile第二个参数决定行为这三个值必须记牢常量值行为文件不存在时ForReading1只读报错 800A0035ForWriting2清空重写由第三参数决定True则创建ForAppending8尾部追加由第三参数决定True则创建 覆盖写 Set ts fso.OpenTextFile(path, 2, True, -2) ts.Write 全量内容 ts.Close 追加写 Set ts fso.OpenTextFile(path, 8, True, -2) ts.WriteLine Now() vbTab 用户A登录 ts.CloseForWriting有个容易被忽略的特性它一打开就会把文件截断为 0 字节。也就是说如果你打开之后在写内容之前抛了异常文件就变成了空文件原内容全丢。这个特性在写配置文件时很危险我在 3.3 节会给一个规避方案。ForAppending相对安全但也有个限制只有Write和WriteLine可用不能读、不能 Seek。想“读最后一行然后追加”是做不到的必须分两次打开。关于Write和WriteLine的区别简单说Write不加换行WriteLine自动在末尾补vbCrLf。写日志一律用WriteLine写序列化数据比如拼一整段 JSON用Write。还有个小细节OpenTextFile的第四个参数在追加模式下同样生效。如果你用-1Unicode打开一个 ANSI 文件追加结果是文件变成“前半段 ANSI、后半段 UTF-16”的混合体用任何工具打开都是乱码。追加时必须保证编码参数与文件现有编码一致这一点没有捷径只能靠约定。3.2 追加日志的完整写法日志是最典型的追加场景。我把它拆成一个可直接用的函数包含目录检查、路径拼接、异常处理和编码控制。Sub WriteLog(logDir, moduleName, msg) Dim fso, f, fullPath, logPath On Error Resume Next Set fso Server.CreateObject(Scripting.FileSystemObject) logPath Server.MapPath(logDir) If Not fso.FolderExists(logPath) Then fso.CreateFolder logPath End If fullPath fso.BuildPath(logPath, moduleName _ _ Year(Date()) Right(0 Month(Date()), 2) .log) Set f fso.OpenTextFile(fullPath, 8, True, -2) f.WriteLine Now() vbTab Replace(msg, vbCrLf, ) vbTab _ Request.ServerVariables(REMOTE_ADDR) f.Close Set f Nothing Set fso Nothing If Err.Number 0 Then Err.Clear On Error GoTo 0 End Sub这里每一行都有讲究。第一Server.MapPath只接受虚拟路径不能直接传D:\logs这样的物理路径传了会报Server.MapPath() 错误 ASP 0174 无效路径。这个错误我见过太多次都是把物理路径直接塞进去。第二fso.CreateFolder只能建一级目录如果要建多级比如logs\2024\06必须从浅到深逐级判断逐级创建或者自己写一个递归函数。这是 FSO 的一个硬限制。第三Replace(msg, vbCrLf, )把消息里的换行压掉。不处理的话一条日志里的换行会让日志文件多出几行后续按行解析就全乱了。压缩成空格是最省事的处理。第四On Error Resume Next包了整个函数因为日志写入失败不应该影响主业务流程——日志是旁路不能因为磁盘满了就让用户下单失败。这个“失败要静默”的原则在日志场景里是对的但要注意静默不等于不管至少要保证代码里别因为错误残留影响后续逻辑所以末尾有Err.Clear。第五文件名按模块名_年月切分避免单文件无限增长。更细的可以按天切文件名里带日期fullPath fso.BuildPath(logPath, moduleName _ _ Year(Date()) Right(0 Month(Date()), 2) _ Right(0 Day(Date()), 2) .log)3.3 原子写入先写临时文件再替换前面提过ForWriting会截断文件的问题。对于那些“内容必须完整”的场景配置文件、缓存文件、数据导出文件我推荐一种近似原子的写法先写到临时文件写成功了再替换原文件。这样即使在写入过程中出错原文件也还是完好的。Function SafeWriteText(filePath, content, charset) Dim fso, stm, tmpPath, ok SafeWriteText False ok False Set fso Server.CreateObject(Scripting.FileSystemObject) 临时文件名与原文件同目录保证在同一分区MoveFile 才快 tmpPath fso.BuildPath(fso.GetParentFolderName(filePath), _ fso.GetTempName() .tmp) On Error Resume Next Set stm Server.CreateObject(ADODB.Stream) stm.Type 2 stm.Charset charset stm.Open stm.WriteText content stm.SaveToFile tmpPath, 2 2 adSaveCreateOverWrite stm.Close Set stm Nothing If Err.Number 0 Then If fso.FileExists(filePath) Then fso.DeleteFile filePath, True fso.MoveFile tmpPath, filePath If Err.Number 0 Then ok True End If If Err.Number 0 Then Err.Clear On Error GoTo 0 清理可能残留的临时文件 If Not ok And fso.FileExists(tmpPath) Then On Error Resume Next fso.DeleteFile tmpPath, True Err.Clear On Error GoTo 0 End If Set fso Nothing SafeWriteText ok End Function关键点在于临时文件必须和原文件在同一个目录或同一个分区。MoveFile在同一分区内是重命名操作快且原子跨分区就是复制又慢又不原子完全失去了这个方案的意义。用fso.GetTempName()而不是自己拼随机数能避免同名冲突它返回的是radXXXXX.tmp这种形式。还有一个细节DeleteFile的第二个参数True表示强制删除只读文件。不传或者传False时遇到只读文件会报 800A0046这也是个常见卡点。关于 UTF-8 的 BOMADODB.Stream在Charsetutf-8下保存时会自动写入 BOM。如果你的下游系统不认 BOM有些解析器会把 BOM 当成内容的一部分导致第一列字段名多出三个不可见字节需要手动去掉stm.Position 0 stm.Type 1 切到二进制 stm.Position 3 跳过 EF BB BF Dim outStm Set outStm Server.CreateObject(ADODB.Stream) outStm.Type 1 outStm.Open stm.CopyTo outStm outStm.SaveToFile tmpPath, 2这段要特别注意顺序先Position 0回到开头再Type 1切二进制。顺序反了会报错。这是 ADODB.Stream 的一个怪癖我当初在这里卡了快两个小时。4. 路径、权限与并发真正让人翻车的三件事写文件的代码在本机跑通部署到服务器上失败九成以上是这三个原因之一。它们的共同特点是错误信息极其含糊光看报错根本猜不到原因。这一节把每个都拆开讲。4.1 Server.MapPath 与路径拼接Server.MapPath的规则说复杂也复杂说简单就一句话它只把虚拟路径翻译成物理路径。你在浏览器地址栏里能看到的那种路径以/开头或者相对于当前.asp文件的路径它能处理D:\开头的物理路径、http://开头的 URL 一律不行。写法结果说明Server.MapPath(/)站点根目录物理路径最常用做基于根目录的定位Server.MapPath(./)当前 asp 文件所在目录相对定位移动文件时会失效Server.MapPath(/data/a.txt)站点根下 data 目录里的文件推荐路径含义清晰Server.MapPath(../a.txt)上一级目录能用但不建议容易越界Server.MapPath(D:\a.txt)报错 0174物理路径不接受拼路径时优先用fso.BuildPath(dir, name)而不是手写dir \ name。原因很实际手拼的时候如果dir末尾已经有反斜杠就会拼出C:\\data\\a.txtWindows 大部分场合能容忍但某些 API 会报路径非法。BuildPath会自动处理分隔符省心。还有一个安全问题必须提如果文件名来自用户输入一定要做过滤。用户在参数里传..\..\web.config你直接拼进路径就能读写站点的任意文件。最简处理是两件事同时做去掉路径分隔符和..然后用LCase转小写后校验前缀是否在允许目录内。Function SafeFileName(name) Dim bad, i, ch bad Array(\, /, :, *, ?, , , , |) For i 0 To UBound(bad) name Replace(name, bad(i), ) Next name Replace(name, .., ) SafeFileName name End Function这只是最低限度的拦截。更严格的做法是白名单——只允许字母数字和下划线其他一律拒绝。在能选的情况下永远选白名单。4.2 IIS 下写权限该给谁“拒绝访问”这个错误错误号 800A0046是老 ASP 项目部署时最常见的拦路虎。它的含义非常直白当前进程的身份没有目标目录的修改权限。难点在于很多人不知道该给谁权限。经典 ASP 运行在应用程序池里进程是w3wp.exe它的身份取决于应用程序池的配置。现代 IIS 默认用ApplicationPoolIdentity这是一个虚拟账户实际名字是IIS AppPool\你的应用程序池名称。给权限的步骤如下在资源管理器里右键目标文件夹属性安全编辑添加。在“输入对象名称”框里输入IIS AppPool\池名称注意中间是反斜杠点检查名称能解析出来就对了。勾选“修改”和“写入”确定。有一个容易搞混的点很多老教程让你给IIS_IUSRS或者NETWORK SERVICE加权限在部分环境下确实能生效但那是更宽泛的组给出去等于把权限范围放大了。精确的做法是给应用程序池标识只给需要写的那个目录不要给整个站点根目录。还有个更隐蔽的情况权限已经给了还是报拒绝访问。这时候要查三件事。第一文件夹是不是从别处复制过来的继承了只读属性需要清掉。第二上级目录有没有“拒绝写入”的继承规则子目录上给的允许会被继承的拒绝覆盖用“有效权限”功能查。第三杀毒软件或安全软件拦截了w3wp.exe写文件这个只能看拦截日志。注意给权限时只给“修改”和“写入”不要顺手勾“完全控制”。完全控制包含了修改权限的能力等于把权限体系交给了程序一旦代码里有路径可控的漏洞后果会放大很多。4.3 并发写入的三种应对手段TXT 文件本身没有任何并发控制机制。两个请求同时用ForAppending打开同一个文件写入结果可能是交错的——A 写了半行B 插进来写了它的半行最后这一行既不是 A 的也不是 B 的。这个现象在日志文件里特别常见表现是日志里偶尔出现一行明显被截断的乱码。第一种手段是Application.Lock。它是进程级的互斥锁把写文件包在锁里同一时刻只有一个请求能写。Application.Lock WriteLog /logs, order, 用户下单 orderId Application.Unlock这个方案简单但有明显代价Application.Lock是全局的锁住期间所有请求都排队写日志这种高频操作会把整个站点串行化吞吐量掉得很难看。而且它只在单进程内有效如果应用程序池开了多个工作进程Web Garden锁就失效了。第二种手段是让并发不发生——每个请求写自己的文件。文件名里带上请求的唯一标识时间戳加随机数或者按小时、按进程切分。日志分析时用通配符合并即可。这个思路牺牲了一点文件整洁度换来的是零锁竞争在写日志场景里我一般优先选它。第三种手段是用数据库或队列替代。如果写入量确实大TXT 就不该承担这个职责了。可以写到 SQL Server 的一张日志表或者用MSMQ。这不是“迂回”而是承认 TXT 的边界——它是交换格式不是并发存储。读取这边也有类似的坑。如果你在写文件的同时另一个请求正在ReadAll可能读到一个写到一半的文件。规避办法是 3.3 节的原子写入读的一方永远只看到完整的旧文件或完整的新文件不会看到半截。这也是我推荐原子写入的真正原因——它同时解决了一致性和并发读的问题。5. 实战一个可复用的文本文件操作模块理论讲完把前面所有东西组装成一个能直接落地的模块。我按功能拆成三块配置读取、日志写入、以及一个把“文本文件当小数据库用”的记事本页面。这三块基本覆盖了老 ASP 项目里九成的 TXT 使用场景。5.1 配置读取封装配置文件一般很小读法简单重点在容错和解析。我用的格式是每行keyvalue#开头的是注释空行忽略。 假设已引入一个 Class 或直接用函数 Function LoadConfig(filePath, ByRef dict) Dim fso, ts, line, pos, key, value Set fso Server.CreateObject(Scripting.FileSystemObject) If Not fso.FileExists(filePath) Then Set fso Nothing Exit Function End If Set ts fso.OpenTextFile(filePath, 1, False, -2) Do While Not ts.AtEndOfStream line Trim(ts.ReadLine) If Len(line) 0 And Left(line, 1) # Then pos InStr(line, ) If pos 1 Then key Trim(Left(line, pos - 1)) value Trim(Mid(line, pos 1)) dict(key) value End If End If Loop ts.Close Set ts Nothing Set fso Nothing End Functionpos 1这个判断是为了排除xxx这种没有键名的行。如果把条件写成pos 0碰到这种行就会产生一个空键后续dict()会有各种奇怪表现。这种一行代码的差别在测试阶段根本发现不了上线后配错格式才暴露所以养成“边界条件多想一步”的习惯很值。字典用Scripting.Dictionary注意它的大小写敏感性默认是False不敏感也就是Config(Timeout)和Config(timeout)取到同一个值。如果你的键名区分大小写要显式设置dict.CompareMode 1vbBinaryCompare。这一点在解析由别的系统生成的文件时特别重要我见过因为大小写敏感设置不一致导致配置读不到的问题。5.2 按天切分的日志写入器把 3.2 节的WriteLog补全成带自动清理的版本。老项目跑久了日志目录动辄几十万个文件磁盘满掉导致站点挂掉的事我见过不止一次清理逻辑必须内建。Sub CleanOldLogs(logPath, keepDays) Dim fso, folder, f, expireDate Set fso Server.CreateObject(Scripting.FileSystemObject) If Not fso.FolderExists(logPath) Then Exit Sub Set folder fso.GetFolder(logPath) expireDate DateAdd(d, -keepDays, Date()) On Error Resume Next For Each f In folder.Files If LCase(fso.GetExtensionName(f.Name)) log Then If f.DateLastModified expireDate Then f.Delete True End If End If Next Err.Clear On Error GoTo 0 Set folder Nothing Set fso Nothing End Sub这里用DateLastModified而不是从文件名解析日期因为文件名格式可能变、可能有多个来源。用文件系统的修改时间更稳。代价是如果有人用工具批量 touch 过文件判断会失真但这个概率远低于文件名格式不一致的概率。清理逻辑什么时候调用不要每次写日志都调遍历目录是有开销的。我的做法是放在一个每天执行一次的独立页面里或者用Application变量记上次执行时间距离上次超过 24 小时才执行If IsEmpty(Application(lastClean)) Or _ DateDiff(h, Application(lastClean), Now()) 24 Then Application.Lock Application(lastClean) Now() Application.Unlock CleanOldLogs Server.MapPath(/logs), 30 End If判空用IsEmpty而不是 因为未赋值的Application变量取出来是Empty而不是空字符串用 判断会漏掉这种情况。5.3 一个能直接跑的记事本小页面最后给一个完整页面能读取、能保存。这个页面本身没什么技术含量但它把前面的读写、编码、原子写入、XSS 防护全串起来了适合直接拿来改。% LanguageVBScript CodePage65001 % % Option Explicit Response.CharSet utf-8 Response.ContentType text/html Dim noteFile, action, content noteFile Server.MapPath(/data/note.txt) action Request.Form(action) If action save Then content Request.Form(content) If SafeWriteText(noteFile, content, utf-8) Then Response.Write p已保存/p Else Response.Write p保存失败请检查目录权限/p End If End If Dim current current ReadUtf8Text(noteFile) % form methodpost input typehidden nameaction valuesave textarea namecontent rows20 cols80%Server.HTMLEncode(current)%/textarea brinput typesubmit value保存 /form这个页面有三处值得单独说。第一Response.CharSet utf-8和页面首行的CodePage65001必须配套。只设其中一个中文在某些浏览器上还是会乱。CodePage告诉 ASP 引擎用什么编码解析脚本里的字符串字面量CharSet告诉浏览器用什么编码解析响应。两者不一致就会出现“页面是 UTF-8 但中文变问号”的情况。第二Server.HTMLEncode(current)绝对不能省。用户上次存进去的内容里如果含、、不编码直接输出到textarea里会把 HTML 结构搞坏严重的话就是一个存储型 XSS。这个例子中文和符号混排时特别容易触发测试时务必拿scriptalert(1)/script试一遍。第三Option Explicit放在页面顶部强制所有变量Dim声明。VBScript 默认允许不声明直接用变量拼错一个变量名不会报错只会得到一个Empty然后一路传下去最后在完全无关的地方报错。老项目里因为拼错变量名查半天的事我一年能碰上好几回。加上Option Explicit这类问题在第一次运行时就暴露了。6. 报错速查与排查手法文件操作失败的报错信息往往很短很含糊但错误号是准确的。这一节把常见的错误号整理成表再讲讲从现象倒推原因的几条路径最后分享几个我亲身踩过的坑。6.1 ASP 文件操作错误号对照表错误号文字描述最常见的三个原因800A0035文件未找到路径拼错、MapPath用了物理路径、文件确实不存在800A0039路径未找到上级目录不存在、CreateFolder只建了一级800A003A文件已存在CreateTextFile第二参数传了False800A0046权限被拒绝应用程序池标识没有写权限、文件被占用、只读属性800A003E输入超出文件尾ReadLine前没判断AtEndOfStream800A0005无效的过程调用或参数OpenTextFile参数越界、Charset拼错800A01A8缺少对象对象创建失败或已置Nothing后继续调用0174Server.MapPath无效路径传了物理路径或以http://开头看这张表的时候有个技巧错误号能区分“路径问题”和“权限问题”但不能区分“路径不存在”和“路径写错”。800A0035 和 800A0039 都属于路径类实际排查时先把Server.MapPath的结果Response.Write出来看一眼这一步能解决一大半问题。很多人凭想象判断路径实际拼出来的根本不是那个位置。6.2 从现象倒推原因的三条路径第一条路径能读不能写。读文件成功说明路径对、文件在、编码也没大问题那问题九成在权限或占用。先用fso.GetFile(path).Attributes看是不是只读属性再去 IIS 里确认应用程序池标识有没有写权限最后看看是不是有别的进程比如杀毒软件、另一个请求没关句柄占着这个文件。第二条路径本机能跑服务器上不行。这种环境差异十有八九是三个原因路径映射不同服务器上站点根目录位置不一样、权限不同本机用管理员身份跑服务器上是受限身份、区域设置不同影响TristateUseDefault和日期格式。第二点最容易被忽略因为在本地开发机上用管理员账户调试什么权限都有永远测不出权限问题。第三条路径偶尔失败不是每次都失败。这基本可以锁定到并发或者资源没释放。先查所有OpenTextFile之后有没有配对的Close再查有没有在循环里反复打开文件而不关闭。我遇到过一次代码在For Each循环里打开文件异常分支里忘了Close跑几十次之后句柄耗尽报的错是“文件未找到”完全误导人。6.3 我踩过的几个坑第一个坑On Error Resume Next之后忘了Err.Clear。这个错误的隐蔽之处在于Err对象是全局的一旦被赋值后面的代码只要检查Err.Number就会一直看到这个旧错误。我当时写了一个“先检查文件、再检查权限、最后写内容”的流程结果前一步的错误残留下来导致后面每一步都判成失败日志里全是“写入失败”但文件其实写进去了。排查了半天才发现是Err没清。现在我的习惯是每一个可能产生错误的调用后面紧跟一行If Err.Number 0的处理处理完立刻Err.Clear。第二个坑用ForWriting写配置文件程序中途报错配置被清空。这个前面提过解决办法就是 3.3 节的原子写入。补充一点如果配置文件是站点运行必需的比如数据库连接串改用它之前一定要先备份一份。我当时的做法是在写入前CopyFile一份.bak真出事了还能手工恢复。第三个坑中文文件名。TXT 文件叫“配置.txt”而不是config.txt在某些语言环境下FileExists会返回False明明文件就在那。这跟 ANSI/Unicode 编码在文件系统层面的处理有关。规避办法很朴素所有程序读写的文件一律用英文名加数字下划线中文只放在文件内容里。这个约定在跨平台、跨语言的项目里尤其重要。第四个坑Response.Buffer和文件写入的顺序。ASP 默认开启缓冲Response.Write的内容先攒着页面结束才发出去。如果你的代码一边往文件里写、一边往页面输出中间还因为超时被 IIS 中断了就会出现“文件写了一半、页面也只输出了一半”的情况。排查这类问题时先看 IIS 日志里的超时记录默认是 110 秒超过这个时间的请求会被强制掐断而w3wp.exe里的代码不会立刻停止。对于可能耗时长的文件操作把处理逻辑放在一个独立的、由后台定时触发的页面里而不是跟着用户的请求走。最后分享一个小技巧关于验证文件是不是真的写成功了。不要只看Err.Number那只能说明调用没抛异常。稳妥的验证是写完Close之后用fso.GetFile(path).Size看文件大小是否符合预期再用ReadUtf8Text读回来比对头尾几十个字符。多这两步能把“写成功但内容不对”这类问题在测试阶段全部拦下来。我在一个数据导出功能里就是靠这个习惯提前发现了一个编码参数写错导致中文全变问号的 bug否则等到业务方抱怨就晚了。