ADF(Azure Data Factory)里的Filter活动,是我在做数据管道时用得最频繁的控制流活动之一。这个活动在官方文档里就几页纸,功能简单到只有两个参数——Items和Condition,但它在实际项目里解决的却是最磨人的问题:面对一堆文件,怎么按条件只处理该处理的那些。我见过太多管道,Get Metadata一拿到文件列表就直接全量塞给ForEach,结果要么把历史文件重复处理一遍,要么把不该动的文件也搬了一遍。其实就是少了Filter这一层过滤。
这篇就围绕ADF中Filter活动条件过滤文件展开,把它的工作原理、实战场景、完整配置步骤和常见坑位一次讲清楚。适合正在用ADF做文件迁移、增量同步、批处理管道的开发者和数据工程师,也适合准备入门ADF控制流的新手。看完你可以直接照着搭建一条“先过滤、再处理”的标准管道。
1. ADF与Filter活动的基础认知
1.1 ADF在数据管道中的定位
ADF是微软Azure上的云数据集成服务,通俗点说,它是一个可视化编排ETL/ELT流程的平台。你不需要自己写一堆调度脚本,用鼠标拖几个活动,再配上表达式,就能把“从A处拉数据、经过清洗、写到B处”整套流程跑起来。它擅长的事情分两类:一类是数据移动,把数据从各种各样的源搬到目标,比如本地文件、数据库、API、云存储;另一类是流程编排,把一系列操作按顺序、按条件执行,支持循环、分支、等待、失败处理这些控制逻辑。
Filter活动归属于第二类,也就是控制流活动。它的作用是,在一个输入数组里,根据你给定的条件表达式,筛选出符合条件的子集,输出给下一个活动。你可以把它想象成水管中间加了一个带筛网的接头——水流过去的时候,只有满足要求的颗粒才通过,其他全拦在外面。ADF管道里常见的数据流是:Get Metadata拿到文件列表 → Filter筛选列表 → ForEach遍历 → Copy拷贝。Filter就是中间那道闸门,决定了后面ForEach会处理哪些文件。
很多人会忽略Filter的价值,觉得直接写个通配符路径也能搞定筛选。但在复杂的业务场景里,文件筛选往往不是“一个文件名前缀”能解决的,它可能是“最近24小时修改过、且非空、且名字匹配某个模式、且不是临时文件”的组合条件。这种时候,通配符就力不从心了,而Filter活动正是为这种场景设计的。
1.2 Filter活动的核心参数与工作方式
配置Filter活动时,核心就看两个属性。
第一个是Items。这个是要过滤的数组,最常见的情况是上游Get Metadata活动的输出,也可以是一个手动指定的数组常量,或者来自管道参数。第二个是Condition。这个是过滤条件,一个布尔表达式,返回true的项会被保留。表达式里通过item()来引用当前遍历到的数组元素。
举个例子,你用Get Metadata拿到了一个容器下所有子项的列表,结构大概是JSON数组,每一项有name、type、lastModified、size等字段。Filter的Condition里就可以写:
json复制@greater(item().lastModified, '2024-01-01T00:00:00Z')
这个表达式的意思是,只保留最后修改时间在2024年1月1日之后的文件。ADF运行时,Filter会遍历Items中的每个元素,逐个代入Condition判断,为true的进入输出结果,为false的直接丢弃。
这个“逐个遍历”的机制很关键,它意味着Filter适合作为一条管道的“中间件”,而不是终点。它的输出可以直接进入ForEach循环,也可以再被其他表达式引用。你可以在Filter之后接一个Set Variable活动,把过滤结果的数量记录下来,方便后续做监控和日志。
1.3 不同技术栈里的Filter对照
可能有读者会问,我明明会JavaScript的filter、会Elasticsearch的filter、会Spring Security的Filter,为什么还要学ADF这个Filter?其实它们思想相通,但作用域完全不同。
| 技术栈 | 过滤对象 | 核心语义 | 典型用途 |
|---|---|---|---|
| ADF Filter活动 | 管道中的数据数组 | 按表达式保留元素 | 文件列表筛选后进入下游活动 |
| JavaScript Array.filter | 内存中的对象数组 | 回调函数返回true保留 | 前端数据局部筛选 |
| Elasticsearch filter context | 文档集合 | 是否匹配,不参与相关度评分 | 精确过滤,结果可缓存 |
| Spring Security Filter | HTTP请求 | 链式拦截,逐层横切 | 鉴权、日志、编码等前置处理 |
JavaScript的Array.prototype.filter是纯内存操作,一个回调函数遍历数组,返回true就留下,比如arr.filter(x => x.age > 18)。它解决的是前端数据处理问题。Elasticsearch里的filter context则是在查询层面做文章,query context会计算相关度评分,而filter context只回答“是否匹配”,不参与评分,所以通常更快、更容易利用缓存。ES里must和should默认参与评分,如果你不需要评分,把它们放进filter子句,语义更清晰,性能也更好。Spring Security的Filter链是基于Servlet规范的责任链模式,请求按顺序经过一个个Filter,每个Filter只做自己那一层横切逻辑,比如鉴权、日志、HTTP头处理。
ADF的Filter活动虽然叫法和别家一样,但它是从“管道编排”视角设计的一个环节,输入输出都是数组,天然和Get Metadata、ForEach、Copy等控制流活动串成一条完整的流水线。理解了这个差异,你就不会把ADF的Filter和别的技术栈里的Filter混为一谈。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实战场景:Filter活动过滤文件的核心用法
2.1 增量文件同步:只处理新文件
我接触到的第一个Filter实战场景就是增量同步。需求很常见:源端有一个FTP目录或者Azure Blob容器,里面每天都会新增一些文件,目标端要同步过去,但历史文件已经同步过了,不该再动。
如果不加过滤,用Get Metadata拿到所有文件,再ForEach逐个Copy,那每次运行都会把全量文件拷一遍。文件少还好,文件一多,耗时和成本立刻上去了。正确姿势是按时间过滤。Get Metadata的Field里勾选ChildItems之后,返回的每个文件项自带LastModified属性。Filter的Condition可以写成:
json复制@greater(item().lastModified, pipeline().parameters.lastSyncTime)
这里的lastSyncTime是管道参数,每次跑完管道后,把参数更新为当前时间,下次运行时只处理在这之后的文件。这就实现了一个最简单的增量窗口。更稳妥的做法是,把上一次的同步时间持久化到数据库表或日志文件里,管道启动时读取这个值作为lastSyncTime参数。这样即使管道临时失败了,重跑时也不会漏文件。第3章我会给一个完整的实现。
2.2 按文件命名规则过滤:只处理特定业务文件
第二个高频场景是,源目录是多个业务共用的,文件命名有约定,比如订单数据文件都以order_开头,用户数据文件都以user_开头,下游处理系统只关心订单文件。
这种场景用Filter做文件名前缀匹配非常顺手。Condition里用startswith函数:
json复制@startswith(item().name, 'order_')
同理还有endswith(后缀匹配)和contains(包含匹配)。如果你要匹配更复杂的规则,ADF表达式对纯正则的支持有限,但可以用组合条件,比如“以order_开头并且以.csv结尾”:
json复制@and(startswith(item().name, 'order_'), endswith(item().name, '.csv'))
这种组合条件基本能覆盖九成以上的命名过滤需求。如果你手里维护着一个白名单前缀列表,想逐个匹配,那可以用or把多个startswith串起来,或者把列表拼进表达式里动态构造。实际项目里,我把这类过滤条件经常写成管道参数,因为业务方过一阵子就可能改文件名规则,参数化之后改起来就不用动管道结构。
2.3 过滤空文件和异常文件
还有一种很实际的需求:上游系统经常生成空文件,或者临时文件(文件名带.tmp后缀)。空文件在真实业务里会干扰下游解析,临时文件如果被拷贝过去就是垃圾数据。这两种都可以用Filter挡掉。
空文件的过滤方式是用size字段:
json复制@greater(item().size, 0)
临时文件的过滤就是排除法,用not函数组合:
json复制@not(endswith(item().name, '.tmp'))
还有一类情况是过滤目录本身。Get Metadata拿到的ChildItems里,子项可能是文件也可能是子目录,字段type可以区分。如果你只需要文件,可以写成:
json复制@equals(item().type, 'File')
如果你的源是嵌套目录结构,需要递归列出所有文件,那要配合“遍历容器内所有子目录”的做法——先用Get Metadata拿第一级,ForEach进去再Get Metadata,层层下探。这时Filter可以在每个层级先过滤掉非目标文件,减少递归层数。不过这种树形遍历属于另一篇文章的范畴,这里点到为止。
2.4 处理文件列表里的“脏数据”
有些情况下,Get Metadata返回的ChildItems里会混入系统生成的隐藏文件,比如Azure Blob里的快照文件,或者某些文件系统里的临时锁文件。虽然不常见,但一旦混进去,下游Copy可能报错或者拷贝出无意义的文件。
我的做法是维护一个“排除特征”列表,用contains、endswith、equals组合把已知的脏数据特征排除掉。比如:
json复制@and(
not(endswith(item().name, '.lock')),
not(contains(item().name, '$'))
)
这种写法看着有点长,但确实能减少很多莫名其妙的运行时报错。脏数据特征是踩过一次坑之后才知道的,我建议你在项目初期就把这类过滤条件写进Filter里,别等出了问题再补。
3. 管道中Filter活动的完整实操复现
3.1 整体管道设计
下面进入完整的实操环节。我们以一个真实项目为例:源端是一个Azure Blob容器,里面每天产生一批csv文件,目标端是另一个存储容器。需求是只同步当天新增、非空、且以order_开头的csv文件。
整条管道设计如下:
- 用Get Metadata获取源容器的文件列表。
- 用Filter按条件过滤出目标文件。
- 用ForEach遍历过滤后的文件列表。
- 在ForEach里用Copy活动,把单个文件拷贝到目标容器。
如果只是单纯拷贝,步骤4用一个Copy活动配合通配符也能完成,但这里故意用ForEach逐项处理的方式,是为了演示通过Filter做精细控制。这种方式的好处是后续还能在ForEach里继续串其他活动,比如“拷贝完成后写日志”或者“给下游发消息”,可扩展性更强。
3.2 获取文件列表:Get Metadata的配置
在管道画布上拖入一个Get Metadata活动,命名为“Get File List”,然后做四步配置。
- Connection:选择你的源存储连接,这里以Azure Blob Storage为例。
- Dataset:选择或新建一个指向源目录的数据集。Dataset的文件夹路径可以配置为参数,方便动态指定目录。
- Field list:勾选ChildItems。这是关键,只有勾选了ChildItems,Get Metadata才会返回目录下的所有子项列表,Filter才能拿到数组作为输入。
- 如果目录是动态的,可以用管道参数来传路径。
配置完成后,点击“调试”运行,可以看Get Metadata的输出大致长这样:
json复制{
"childItems": [
{
"name": "order_20240101.csv",
"itemType": "File",
"lastModified": "2024-01-01T08:30:00Z",
"size": 102400
},
{
"name": "user_20231231.csv",
"itemType": "File",
"lastModified": "2023-12-31T10:00:00Z",
"size": 20480
},
{
"name": "order_20231230.csv",
"itemType": "File",
"lastModified": "2023-12-30T13:00:00Z",
"size": 0
}
]
}
注意不同的连接器返回的字段名可能有差异,比如Blob返回itemType,SFTP可能返回type,但核心的name、lastModified、size基本都有。后续Filter表达式里引用字段名时,要以实际输出为准,最好的办法就是先调试一次看真实输出。
3.3 配置Filter活动与表达式写法
接下来拖一个Filter活动,放在Get Metadata后面,连上Success输出。点开Filter活动后,两个配置项要填。
Items填上游Get Metadata的输出数组:
json复制@activity('Get File List').output.childItems
Condition填过滤条件。需求是三个条件同时满足:文件名以order_开头、以.csv结尾、文件大小大于0。用and函数把三个布尔表达式串起来:
json复制@and(
startswith(item().name, 'order_'),
and(
endswith(item().name, '.csv'),
greater(item().size, 0)
)
)
如果你想把“当天新增”也放进去,可以用日期函数。ADF里取当天零点的一个常用写法是组合convertTimeZone和startOfDay:
json复制@greater(
item().lastModified,
convertTimeZone(startOfDay(utcNow()), 'UTC', 'China Standard Time')
)
这里先拿到UTC当天零点,再转换为中国标准时间。如果你的业务不是中国时区,换成对应的Windows时区ID即可。时区问题在后面会专门展开。
填写完表达式后,建议先别急着连下一环,先在调试模式跑一次,点开Filter活动的输出面板,看筛选后的数组里有哪些文件。调试模式会展示每个活动实际运行时的输入输出,比盲写表达式高效得多。
3.4 串联ForEach与Copy活动
Filter的输出是过滤后的文件数组。现在拖一个ForEach活动,它的Items设为Filter的输出:
json复制@activity('Filter files').output
这里直接引用Filter活动的输出即可,因为Filter的输出本身就是一个数组。如果你在Filter输出里能看到count和value两个字段,value才是真正的数组,但ForEach的Items可以直接接@activity('Filter files').output,ADF会自动处理。
在ForEach里拖一个Copy活动,配置源数据为源存储,目标为目标存储。Copy的Source里,我们要把文件路径动态指定到当前遍历到的文件。这里需要一个小技巧:把Dataset的文件名设置为参数,然后在Source的Dataset属性里用表达式绑定:
json复制@{item().name}
具体操作是把Dataset的folderPath留成固定目录,fileName配置为数据集参数,然后在Copy活动的Source里点击“Dataset属性”,将文件名参数绑定为@{item().name}。这样每个循环迭代,Copy只拷当前这个文件。
ForEach还有个重要配置项:Sequential和Batch count。如果文件之间无依赖,可以不开Sequential,设置Batch count为5或10,让ForEach并行处理多个文件,速度会快很多。但如果下游系统对并发有要求,或者同一时间只能处理一个文件,就保持Sequential。这个问题要结合业务来定,没有标准答案。
3.5 参数化与动态过滤进阶
生产环境里的过滤条件经常是动态的,比如“只同步昨天之后修改的文件”或者“只同步指定前缀的文件”。这就有必要把Filter条件和查询时间做成参数。
首先在管道定义里添加管道参数:
- lastSyncTime(string):上次同步时间,默认值可以写"2024-01-01T00:00:00Z"。
- fileNamePrefix(string,默认值order_):文件名前缀。
然后在Filter活动的Condition里引用这些参数:
json复制@and(
startswith(item().name, pipeline().parameters.fileNamePrefix),
greater(item().lastModified, pipeline().parameters.lastSyncTime)
)
这样在触发管道时,可以通过Azure DevOps发布、控制台调用或者调度触发器动态传入参数值。配合调度触发器里的“参数”配置,甚至可以让每次调度自动传入上次执行时间。
实现自动传入上次执行时间,我常用一个轻量方案:在管道里加一个Lookup活动,从一张配置表里读last_sync_time字段,用Set Variable赋给一个变量,然后在Filter的Condition里引用变量。管道跑完后,再用一个Copy活动把当前时间写回配置表。这个方案本质上是一个简单的状态管理机制,不需要引入额外的调度系统,但很实用。
3.6 数据流里的Filter转换(补充)
除了控制流里的Filter活动,ADF还有数据流(Mapping Data Flow)组件,里面也有一个Filter转换,作用是对数据流里的行做条件过滤。控制流Filter和数据流Filter的区别要分清楚:
- 控制流Filter:在活动级别处理“文件列表”,不读取文件内容。
- 数据流Filter:在处理数据内容时按行过滤,比如“只保留金额大于100的订单”。
如果你在做数据清洗,要在数据流里用Filter转换,拖进来后在Filter on里写行级条件,ADF的表达式同样丰富,比如:
json复制toInteger(column('amount')) > 100
两个Filter不冲突,实际项目里经常一起用:先用控制流Filter选出目标文件,再用数据流Filter清洗文件内容。这个组合我几乎每条生产管道都在用。
4. 常见问题排查与避坑经验
4.1 表达式解析失败:最常见的报错
Filter是ADF表达式函数里最容易出错的地方,你经常会遇到类似这样的报错:
code复制The function 'and' was called with invalid parameters.
这类报错绝大多数是表达式语法问题。排查思路我总结为四步:
- 检查函数的参数个数和类型。比如greater要求两个参数,如果第一个是字符串、第二个是数值,类型不匹配就会报错。
- 检查嵌套括号是否匹配。表达式一旦括号不齐,ADF解析直接失败。建议写复杂表达式时把括号分层展开,一层层核对。
- 检查字符串引号。ADF表达式里字符串用单引号,不是双引号。
- 检查item()的引用方式。在Filter的Condition里,item()代表当前项,不能再加@前缀,也不能写成this。
还有一个很隐蔽的问题:ADF的函数名是驼峰式,比如startOfDay、convertTimeZone、greaterOrEquals,大小写敏感。把startofday写成小写都会解析失败。
如果实在不知道错在哪,打开调试模式,把Get Metadata的输出粘贴为样本数据,一步步试表达式。ADF的调试器会返回具体哪一行解析出错,比来回瞎猜效率高很多。
4.2 空数组与空引用的坑
当目录下没有任何文件时,Get Metadata返回的childItems是空数组。Filter处理空数组时,理论上返回空数组,不会报错。但这里有一个隐藏的坑:如果Get Metadata的输出里根本没有childItems键,比如目录路径不存在,那么Filter的Items引用就会失败。
解决办法是在Get Metadata之后加一个校验条件。用If Condition活动判断childItems是否为空:
json复制@greater(length(activity('Get File List').output.childItems), 0)
不为空走真分支,真分支里放Filter和后续处理;为空走假分支,可以做日志记录或者直接跳过。这个模式在生成环境里非常重要,能避免无谓的运行失败。
还有一个常见情况:不同连接器返回的ChildItems字段名可能不统一,有的返回childItems,有的返回value。调试时看真实输出确认字段名,再写表达式,不要凭文档猜。
4.3 时区问题:过滤时间不准
很多做ADF的人都被时区坑过。ADF表达式里utcNow()返回的是UTC时间,而业务系统通常在中国时区(UTC+8)。如果直接用utcNow()和lastModified比较,过滤条件会产生8小时的偏差。
举个例子,你在北京时间早上9点跑管道,想处理“今天”的文件,如果直接用“大于今天UTC零点”这种写法,实际过滤的边界是UTC零点,也就是北京时间早上8点,边界处很容易漏文件。
正确做法是先把今天零点转为指定时区:
json复制@greater(
item().lastModified,
convertTimeZone(startOfDay(utcNow()), 'UTC', 'China Standard Time')
)
convertTimeZone函数接收三个参数:时间值、源时区、目标时区。把UTC时间转换为中国标准时间后再比较,就和你本地的业务时间对齐了。如果你的源文件时间本身就是UTC,那转换的目标时区就写UTC,一定要先搞清楚源端存储的时间格式和时区。
4.4 文件数量太多导致性能问题
ADF的Filter活动理论上可以处理较大的数组,但实际使用中,如果Get Metadata返回成千上万个文件,Filter的表达式计算会变慢,而且ForEach每个文件一次Copy,总量一大,管道运行时间会非常长。
两个优化方向值得尝试。第一个是在源头缩小范围,Get Metadata的Dataset路径可以配置到更细的目录层级,比如按日期分目录存储(/2024/01/01/xxx.csv),只Get Metadata当天的目录,文件数量大幅下降。第二个是用批量Copy替代逐文件Copy,如果场景允许,直接用一个Copy活动,在Source的“通配符路径”里填文件匹配模式,比如“/order_*.csv”,让Copy自己扫描匹配文件,省掉Get Metadata、Filter、ForEach这条链,性能最好。
不过,如果你的过滤条件很复杂,比如“只要order前缀、非空、且最近修改的文件”,那通配符方案做不到这么细,还是得用Filter。这时候我建议在Get Metadata层面先尽量缩小范围,再让Filter做精细筛选。两者结合,是性能和精度的最佳平衡点。
4.5 调试技巧与监控手段
最后分享几个调试和监控的经验。
每个活动都要命名清晰。ADF里表达式引用活动输出时,会用到活动名称,比如@activity('Get File List').output。给Get Metadata命名“Get File List”,给Filter命名“Filter files”,命名规范一点,引用时就不会找错。
善用“调试”按钮。在管道画布顶部点“调试”,ADF会以你选中的方式实际运行管道,你可以逐个活动点开看输入输出。第一次写Filter表达式,强烈建议先调试再发布。
监控视图里看状态。ADF的Monitor页面可以看到每次管道运行的详细信息,包括每个活动的状态、耗时、输入输出大小。如果Filter结果不符合预期,去Monitor里点开“Filter files”活动,看输出数组的大小和内容,能很快定位问题。
加日志活动。在Filter之后加一个Append Variable或者直接输出到日志表,把符合条件的文件数量记录下来,方便事后审计。别小看这步,生产环境出问题时,这些日志往往是你唯一的排查线索。
4.6 常见错误速查表
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
| The function 'and' was called with invalid parameters | 参数类型不匹配 | 检查greater/and/startswith等函数的参数类型,数值与日期不可混用 |
| 表达式解析失败 | 括号不齐或函数名大小写错误 | 用调试模式分步试表达式,核对驼峰式函数名 |
| Filter输出为空数组 | Condition条件过严或字段名引用错误 | 检查实际输出JSON中的字段名,确认条件逻辑 |
| 时间过滤结果偏差8小时 | 时区未转换 | 用convertTimeZone将UTC转换为业务时区再比较 |
| Get Metadata输出中没有childItems | 目录路径错误或Field list未勾选ChildItems | 检查路径,勾选ChildItems,调试看真实输出 |
| ForEach处理的文件数量不符合预期 | Filter的Items引用错误或字段名错 | 在调试模式看Filter活动的输出数组 |
我个人在实际操作中的体会是,ADF的表达式系统刚开始确实有点别扭,但用多了你会发现它其实很有章法。Filter活动是我每条数据管道里几乎都会用到的环节,它帮你把“处理什么”这个决策从下游的Copy、存储过程里拿了出来,放到最直观的控制流层面。调试几回,把时区、空数组、命名这些问题都摸透了,后面写条件过滤就是一个套路的事。
最后再分享一个实用小技巧:如果你不确定Filter的表达式能不能算出预期结果,可以在Filter条件里先写一个恒真条件,比如@equals(item().type, item().type),先把整条链路跑通,确认下游配置没问题,再逐步把条件改严格。这样排查问题时,能区分是Filter写错了还是下游配置有问题,避免一上来就钻进表达式调试的泥潭里出不来。
