跳转至

把 XML 文件装进数据库 — T-SQL 游标、SSIS 循环与 CLR 三条路

这一章讲什么: 和上一篇(导出)相反的方向:磁盘上一批 .xml 文件,要逐个读进数据库表。 「不止一个文件」是难点所在——列目录、逐个处理、失败兜底、处理完归档,每件都是新课题。 书里给了三条路:存储过程 + 游标的纯 T-SQL 方案、六个组件的 SSIS 包、以及用 C# 写 SQLCLR 函数的方案。读完你会拿到那条贯穿三条路的共同骨架,和各自的适用边界。

1. 先看现象:为什么「导入」比「导出」难一档

导出时,一次查询产出一个 XML、写一个文件,是一对一。导入时,源文件夹里有几个文件事先不知道——书里把 T-SQL 方案要解的问题列成三条:拿到符合条件的文件名(比如所有 .xml)、逐个访问并读出内容、装进表1。围绕这三条,才长出了「列目录 → 循环 → 读文件 → 归档」这套结构。

2. 顶层全景:主线例子与共同骨架

本章主线例子: C:\TEMP 文件夹里躺着若干个 .xml 文件(Categories_*.xml 等,就是上一篇导出的那种),要把每个文件的完整内容装进 _XML 表:

CREATE TABLE _XML (
    ID          int IDENTITY(1,1) PRIMARY KEY,   -- 自增编号
    XMLFileName nvarchar(300),                    -- 文件从哪来
    XML_LOAD    XML,                              -- 文件内容(XML 类型,入库即过格式检查)
    Created     datetime                          -- 装载时间
);

(表结构来自原书配方 3-3。)三条路走的是同一条流水线,只是实现工具不同:

  ① 列出候选文件      ② 逐个循环        ③ 读出内容          ④ 写进表        ⑤ 事后处理
  ─────────────────────────────────────────────────────────────────────────────
  T-SQL:  dir /B       T-SQL: 游标       OPENROWSET BULK      INSERT…SELECT   错误处理表
  SSIS:   脚本探测     Foreach 循环容器   OPENROWSET BULK      Execute SQL     File System Task 归档
  CLR:    (C# 枚举)    (C# 内部循环)    FileStream 读取      函数返回值      —

  图说:三条路的形状一样,差别在每格用谁的方言实现,以及权限从哪来。

3. 核心原理

3.1 纯 T-SQL 方案:五步走完一批文件

解决什么问题: 不装任何外部工具,一段存储过程把一批 XML 文件装进表。

怎么做,五个构件:

构件一,目标表自保逻辑:存储过程开头检查 _XML 表在不在,不在就建(上面那段 DDL),在就 TRUNCATE(清空重来)——按业务要求二选一2。

构件二,列目录:用 xp_cmdshell 执行 Windows 的 dir 命令,拿到文件名清单。两个开关是关键:/B(bare,只要文件名,不要 dir 默认那些日期大小信息)和 /O:-D(按创建日期倒序)3:

SET @DOS = 'dir /B /O:-D ' + @DirBaseLocation;
INSERT @files EXEC master..xp_cmdshell @DOS;

构件三,逐个循环:对文件名清单声明游标(cursor)——T-SQL 里逐行处理结果集的机制——挑出扩展名是 .xml 的,一个一个来4。

构件四,读文件,这是全章的技术枢纽,一行 SQL:

INSERT INTO _XML
SELECT 'C:\TEMP\Categories.xml', X, GETDATE()
FROM OPENROWSET(BULK N'C:\TEMP\Categories.xml', SINGLE_BLOB) AS tempXML(X);

OPENROWSET 是 SQL Server 里「访问数据源之外的数据」的函数;BULK 选项让它直接读文件;SINGLE_BLOB 规定整份文件作为一个值返回,数据类型是 VARBINARY(MAX)5。别名必须写成 表别名(列别名) 的格式:AS tempXML(X)。

构件五,动态执行与兜底:文件名拼进 INSERT 语句后,用 sp_executesql 执行(因为 OPENROWSET 的文件路径必须是字面值,没法参数化);外面包一层错误处理,某个文件读失败时记录错误细节、不炸掉整批6。

为什么 SINGLE_BLOB 而不是文本选项——这一章最反直觉的选择: OPENROWSET 还有 SINGLE_CLOB(返回 varchar(max))和 SINGLE_NCLOB(返回 nvarchar(max))两个选项,看起来更像「读文本文件」该用的。书里明确排除了它们:只有 SINGLE_BLOB 支持所有 Windows 编码转换——文件可能是 UTF-8、UTF-16 或带 BOM 的其他编码,先按最原始的字节流收进来,再由 XML 类型自己解析,才不会在编码这一层翻车7。主线例子里 X 列是 VARBINARY,INSERT 进 XML 列时由 XML 解析器完成从字节到树的转换。

边界(书里自己点破的一个坑): 直接 EXEC dbo.usp_LoadXMLFromFile 'C:\Temp' 当然能跑,但那样就丢了错误处理日志——任何一个文件装载失败,整批中断,你拿不到是哪个文件、为什么8。生产用法应该把错误处理打开。

3.2 SSIS 方案:六个组件的生产化流水线

解决什么问题: 同一批文件的装载,要变成可调度、可传参、有归档纪律的生产流程。

怎么做,按包的执行顺序走六个组件9:

  [Script Task: Check If File Exists]
      C# 一行:System.IO.Directory.GetFiles(源目录, "*.xml").Length != 0
      结果写进变量 FlagIsFileExist(true/false)
        │  Precedence Constraint 表达式:@[User::FlagIsFileExist] == true
        │  (false → 整个包停)
        ▼
  [Execute SQL Task: Truncate Table]  ← 建表/清空 _XML
        ▼
  [Foreach Loop Container: Load XML Content]  ← 配好目录表达式与 "*.xml" 模式
      每轮把文件名写进变量 FileName
        ▼
  [Execute SQL Task: Insert XML Data]  ← 执行变量 SQLScript 拼好的 INSERT…OPENROWSET
        ▼
  [File System Task: Archive File]  ← 处理完的文件挪进 Archive 文件夹

  图说:绿色箭头上的表达式充当红绿灯;循环容器负责「逐个」,File System Task 负责「不重复处理」。

三个值得停一下的设计:

  1. 存在性检查独立成一个组件:没有任何现成组件能做「检查目录里有没有 .xml」,所以用 Script Task 写 Directory.GetFiles——返回值不是 0 就是「有文件」,转成布尔塞进变量10。这是「SSIS 缺什么就补一段 C#」的典型姿势。
  2. SQL 语句放在变量里、用表达式拼:INSERT 语句的文件名部分来自循环变量,SSIS 的做法是把整条 SQL 作为变量 SQLScript 的表达式,Foreach 每轮赋新文件名,表达式就自动重拼;Execute SQL Task 里执行的是 exec sp_executesql ?,问号是这个变量的占位符11。
  3. DelayValidation 属性:File System Task 设计时会去验证源文件存在,若加载时默认文件名恰好不在源目录会直接报错;把 DelayValidation 设为 True,把这个设计期的预检跳过12。

归档的意义:处理完的文件挪进 Archive,下次调度再跑时不会重复装载——幂等性(重复执行结果不变)是靠文件移动而不是靠数据库去重实现的。

边界: 作者也给了不喜欢 File System Task 的人一条退路——循环里放一个 Script Task,三行 C#(System.IO.File.Move(from, to))照样归档,「对那些更想要掌控感的人,我建议用 Script Task」13。调度方面,包可以通过 SQL Server Agent 作业定时跑,也可以用 DTEXEC 命令从存储过程里带参数执行14。

3.3 CLR 方案:绕开 xp_cmdshell,换一套权限模型

解决什么问题: 前两条路都碰 xp_cmdshell(或等价的进程执行)。有的环境就是不给。能不能把「读写文件」做成数据库自己的函数?

怎么做: 能。SQL Server 支持 CLR(Common Language Runtime)集成:把 C# 代码编译成 .dll 文件,注册进数据库,就能像普通函数一样用 T-SQL 调用15。书里写了一个 C# 类,内含两个函数:

函数 参数 返回
WriteXMLFile XML 内容、目标目录、文件名、是否加时间戳 成功 → 完整文件路径;失败 → 错误信息
ReadXMLFile 文件路径 成功 → 文件内容;失败 → 错误信息

读文件用的是 FileStream 建连接、StreamReader 一次读完的经典 .NET 组合16。注册分四步:编译(csc.exe /target:library 生成 dll)→ 服务器开 clr enabled → 数据库设 TRUSTWORTHY ON → CREATE ASSEMBLY 挂 dll 并声明权限级 → CREATE FUNCTION … EXTERNAL NAME 把 T-SQL 函数名绑到 C# 方法上17。

为什么有效: EXTERNAL NAME 的三段引用(程序集名.类名.方法名)把数据库对象和 .NET 代码焊死;函数签名(参数类型、返回类型)必须和 C# 一致18。

权限级——这个设计的灵魂: CREATE ASSEMBLY 的 PERMISSION_SET 三档,书里给了清晰定义19:

级别 能做什么
SAFE(首选) 不能碰操作系统资源:注册表、文件、环境变量(操作系统里的全局配置值)、网络都不行
EXTERNAL_ACCESS 可以碰注册表、文件、环境变量,但出不了 SQL Server 实例
UNSAFE 无限制

读写文件的 dll 必须挂 EXTERNAL_ACCESS——这正是它和 xp_cmdshell 的本质区别:权限跟着程序集走、由 DBA 签发,而不是跟着调用者走20。代价是要会写 C#,书里原话:「处理 CLR 过程时最好有编程技能」21。

边界: SET TRUSTWORTHY ON 本身是个安全放宽,很多环境同样会被安全部门拒绝——CLR 不是「绕过审批」的密道,只是另一套审批对象(补充判断,不在书里)。

4. 作者的判断与证据

有完整可跑代码支撑的: 三条路的全部构件都有逐行讲解的清单和可执行代码——T-SQL 方案拆成十步6,SSIS 方案拆成六个组件9,CLR 方案从 C# 源码到注册语句完整给出17。

作者明说的偏好: 归档用 Script Task 而非 File System Task(「更想要掌控感」)13;SSIS 写文件偏爱 Script Task(上篇已述);本章小结还主动交代:「这不是唯一方案」——PowerShell、.NET 应用同样能干这些活,书里这套的价值在于「用 T-SQL 编译 SSIS 包」式的纯粹 SQL 视角22。

需要打问号的: 「CLR 提供了一种扩展 SQL Server 功能的安全方式」21——相对于 xp_cmdshell 成立(权限收敛到程序集),但 TRUSTWORTHY + EXTERNAL_ACCESS 的组合在严格审计下照样过不了关,这一句宜读作「另一种安全模型」,不宜读作「更安全的方案」。

5. 边界与局限

  • OPENROWSET BULK 要求特殊权限:读文件需要 ADMINISTER BULK OPERATIONS 或 BULK ADMIN 等服务器级权限,书里没展开(补充,不在书里,来自通用知识)。
  • 游标在超大文件数下的性能:几千个文件逐个 INSERT 的事务开销,书里没测。
  • SSIS 组件的界面细节会过时,六组件的结构和变量设计不会。
  • 归档目录满了怎么办、同名文件覆盖策略,File System Task 设了 OverwriteDestination=True,但磁盘满、权限丢失等运维场景未谈。

6. 可带走的

本章主线例子,一行走完: C:\TEMP 里一批 .xml → (T-SQL)dir /B /O:-D 列名 → 游标逐文件 → OPENROWSET(BULK 路径, SINGLE_BLOB) 读字节流 → INSERT 进 _XML(XML 列自动解析);(SSIS)六组件流水线 + 归档 + Agent 调度;(CLR)C# 双函数 + EXTERNAL_ACCESS 程序集,权限跟 dll 走。

  1. 导入的核心矛盾是「文件数量未知」,一切结构(列目录、循环、归档)都从它长出来1。
  2. 读 XML 文件用 OPENROWSET(BULK …, SINGLE_BLOB)——字节流收进来,编码交给 XML 解析器;SINGLE_CLOB/NCLOB 不支持全部 Windows 编码,是陷阱7。
  3. OPENROWSET 的文件路径不能参数化,所以拼 SQL + sp_executesql;这也意味着必须自己防注入(路径来自内部变量时风险低,来自外部输入时必须白名单)6。
  4. 错误处理别省:裸 EXEC 存储过程,一个坏文件炸掉整批还没日志8。
  5. SSIS 装载流水线 = 存在性检查(脚本) → 清表 → Foreach 循环 → INSERT(变量表达式拼 SQL) → 归档911。
  6. Precedence Constraint 的表达式就是包里的红绿灯:@[User::FlagIsFileExist] == true,false 即停10。
  7. File System Task 记得 DelayValidation=True,否则设计期预检会在文件缺席时报错12。
  8. 归档换幂等:文件挪走,下次调度自然跳过;数据库侧不必去重。
  9. CLR 三步注册:开 clr enabled → TRUSTWORTHY → CREATE ASSEMBLY(PERMISSION_SET=EXTERNAL_ACCESS 才能碰文件)→ CREATE FUNCTION EXTERNAL NAME 三段绑定1719。
  10. 三条路的选型依据是权限模型,不是技术优劣:xp_cmdshell 跟调用者走、CLR 跟程序集走、SSIS 跟服务账户走——先问安全部门认哪一种,再谈实现20。

7. 原文地图

主题 原书章 原文位置
导入要解的三个问题 Chapter 3: Manipulating XML Files text/07-ch03-chapter-3-manipulating-xml-files.txt:489(搜「loading the XML」)
目标表自保(建/截) 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:526(搜「destination table」)
dir /B /O:-D 列目录 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:571(搜「bare format」)
游标循环 .xml 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:584(搜「cursor」)
OPENROWSET BULK SINGLE_BLOB 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:602(搜「SINGLE_BLOB」) · text/07-ch03-chapter-3-manipulating-xml-files.txt:617(搜「not a good fit」)
sp_executesql 与错误处理 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:620(搜「sp_executesql」) · text/07-ch03-chapter-3-manipulating-xml-files.txt:625(搜「error handler」)
裸 EXEC 丢日志的坑 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:632(搜「lose the error handler」)
SSIS 业务规则与六组件 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:643(搜「business rules」) · text/07-ch03-chapter-3-manipulating-xml-files.txt:654(搜「composed of」)
存在性检查脚本 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:721(搜「GetFiles」)
SQLScript 变量表达式 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:693(搜「SQLScript」) · text/07-ch03-chapter-3-manipulating-xml-files.txt:812(搜「Parameter Mapping」)
DelayValidation 与归档替代 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:836(搜「DelayValidation」) · text/07-ch03-chapter-3-manipulating-xml-files.txt:872(搜「alternative to the File System Task」)
DTEXEC 从存储过程执行包 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:900(搜「SQL Server Agent job」)
CLR 类与读写函数 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:923(搜「extend T-SQL functionality」) · text/07-ch03-chapter-3-manipulating-xml-files.txt:1044(搜「ReadXMLFile function returns」)
注册流程与 PERMISSION_SET 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:1068(搜「clr enabled」) · text/07-ch03-chapter-3-manipulating-xml-files.txt:1080(搜「three options」) · text/07-ch03-chapter-3-manipulating-xml-files.txt:1096(搜「EXTERNAL NAME」)
本章小结与替代技术 同上 text/07-ch03-chapter-3-manipulating-xml-files.txt:1154(搜「other technologies」)

  1. 出处:「Chapter 3: Manipulating XML Files」第 487–498 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:489,搜「loading the XML」)。原文:「When the file is written to storage, we deal with a single XML output per file. However, when we load files, one or more files could be in the source location」;三个问题:「Obtain the file names with particular criteria」「Access each file and read the file content」「Load (INSERT) the file content into a table」。 ↩↩

  2. 出处:「Chapter 3: Manipulating XML Files」第 526–542 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:526,搜「destination table」)。原文:「We need to make sure that the destination table (_XML) exists…When the table exists, depending on the business requirements, the data can be truncated or the table can retain historical data」,并给出建表 DDL(含 IDENTITY 自增主键、XMLFileName、XML_LOAD XML、Created 四列)与 TRUNCATE TABLE _XML。 ↩

  3. 出处:「Chapter 3: Manipulating XML Files」第 567–574 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:571,搜「bare format」)。原文:「"dir /B /O:-D"…"/B" indicates the use of the bare format (file name only), and "/O:-D" specifies the sort order by date created, descending」。 ↩

  4. 出处:「Chapter 3: Manipulating XML Files」第 583–594 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:584,搜「cursor」)。原文:「we need to iterate through each file with the extension ".xml". This can be accomplished by declaring a cursor for the table variable and establishing a loop over the cursor」。 ↩

  5. 出处:「Chapter 3: Manipulating XML Files」第 600–619 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:602,搜「SINGLE_BLOB」)。原文示例:「FROM OPENROWSET(BULK N'C:\TEMP\Categories.xml', SINGLE_BLOB) AS tempXML(X)」;「The BULK option specifies that we will be reading all of the contents from the source file in bulk」「The SINGLE_BLOB option specifies that the file content is returned as a single column with the data type VARBINARY(MAX)」;别名「must be in table(column) format」。 ↩

  6. 出处:「Chapter 3: Manipulating XML Files」第 620–627 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:620,搜「sp_executesql」)与第 625–627 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:625,搜「error handler」)。原文:「The system stored procedure sp_executesql executes the composed SQL」;「The error handler allows the process to run and returns error details with problematic SQL」。 ↩↩↩

  7. 出处:「Chapter 3: Manipulating XML Files」第 616–619 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:617,搜「not a good fit」)。原文:「Other OPENROWSET options, SINGLE_CLOB (returns varchar(max)) and SINGLE_NCLOB (returns nvarchar(max)), are not a good fit for XML import data, because only SINGLE_BLOB supports all Windows encoding conversions」。 ↩↩

  8. 出处:「Chapter 3: Manipulating XML Files」第 628–633 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:632,搜「lose the error handler」)。原文:「However, the disadvantage is that you will lose the error handler logging, and if one of the files that is being loaded fails, then the file will not be loaded」。 ↩↩

  9. 出处:「Chapter 3: Manipulating XML Files」第 644–671 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:643,搜「business rules」)与第 654–671 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:654,搜「composed of」)。五条业务规则(检查存在→无文件则停→截断→装载全部 .xml→归档)与六个组件清单(存在性脚本任务、优先约束、清表任务、Foreach 循环、INSERT 任务、归档任务)。 ↩↩↩

  10. 出处:「Chapter 3: Manipulating XML Files」第 707–722 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:709,搜「can provide this functionality」)。原文:「No other task can provide this functionality」;代码:「Directory.GetFiles(…, "*.xml").Length != 0」赋给 FlagIsFileExist。优先约束表达式见第 726–733 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:729,搜「Evaluation operation」):「For the Expression property, type the expression: @[User::FlagIsFileExist] == true」。 ↩↩

  11. 出处:「Chapter 3: Manipulating XML Files」第 693–700 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:693,搜「SQLScript」)与第 793–811 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:785,搜「question mark」)。SQLScript 表达式拼 INSERT…OPENROWSET;执行侧:「type "exec sp_executesql ?". The question mark specifies that the task will have an input variable as a parameter」,参数映射选 User::SQLScript、方向 Input、类型 NVARCHAR。 ↩↩

  12. 出处:「Chapter 3: Manipulating XML Files」第 835–839 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:836,搜「DelayValidation」)。原文:「I recommend setting the File System Task property…DelayValidation to True in order to prevent an error in case the package loads and the default file name does not exist in the source location」。 ↩↩

  13. 出处:「Chapter 3: Manipulating XML Files」第 872–899 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:872,搜「alternative to the File System Task」)。原文:「An alternative to the File System Task is the Script Task, which is my personal preference」「For those who prefer more control over the process of moving the files, I would suggest implementing the Script Task instead of the File System Task」。 ↩↩

  14. 出处:「Chapter 3: Manipulating XML Files」第 900–916 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:900,搜「SQL Server Agent job」)。原文:「The SSIS package can be deployed through the SQL Server Agent job, which will run automatically on a customized schedule」,并用 DTEXEC 从存储过程执行(Listing 3-7)。 ↩

  15. 出处:「Chapter 3: Manipulating XML Files」第 917–928 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:923,搜「extend T-SQL functionality」)。原文:「The CLR (Common Language Runtime) functions could extend T-SQL functionality and operate the same way as a SQL Server user-defined object」。 ↩

  16. 出处:「Chapter 3: Manipulating XML Files」第 1044–1059 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:1044,搜「ReadXMLFile function returns」)。原文:「When the FileStream function establishes a connection to the XML file, then the StreamReader function reads the entire file」。 ↩

  17. 出处:「Chapter 3: Manipulating XML Files」第 951–1001 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:962,搜「csc.exe」)与第 1067–1077 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:1068,搜「clr enabled」)。编译命令 csc.exe /target:library /out:…ReadWriteXML.dll;注册三步:「Make sure that the 'clr enabled' option is enabled」「Switch to the user database and SET TRUSTWORTHY ON」「Create an assembly that sets the reference to dll」。 ↩↩↩

  18. 出处:「Chapter 3: Manipulating XML Files」第 1098–1108 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:1096,搜「EXTERNAL NAME」)。原文:「you need to list all of the parameters to match the function in the C# dll…The EXTERNAL NAME has three references」:程序集名、CLR 类名、CLR 函数名。 ↩

  19. 出处:「Chapter 3: Manipulating XML Files」第 1079–1090 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:1080,搜「three options」)。原文:SAFE「used when a dll cannot access external system resources」;EXTERNAL_ACCESS「The dll can access the registry, files, and environment variables. However, these cannot be accessed outside an instance of SQL Server」;UNSAFE「Unrestricted access」;「The ReadWriteXML.dll accesses files; therefore, the EXTERNAL_ACCESS option is set」。 ↩↩

  20. 出处:「Chapter 3: Manipulating XML Files」第 1147–1148 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:1147,搜「secure way」)。原文:「The CRL procedures provide a secure way to extend SQL Server functionality」。权限收敛到程序集这一对比是本拆解的判断(基于 PERMISSION_SET 定义与 xp_cmdshell 模型的差异)。 ↩↩

  21. 出处:「Chapter 3: Manipulating XML Files」第 1147–1148 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:1148,搜「programming skills」)。原文:「However, programming skills are preferred when dealing with CLR procedures」。 ↩↩

  22. 出处:「Chapter 3: Manipulating XML Files」第 1151–1156 段(text/07-ch03-chapter-3-manipulating-xml-files.txt:1154,搜「other technologies」)。原文:「Please be aware that this is not the only solution, since in today's world, such tasks could be completed using other technologies, such as PowerShell, .NET applications (either C# or VB.NET), among others」。 ↩