Erlang中二进制字符串的棘手模式匹配

Posted

技术标签:

【中文标题】Erlang中二进制字符串的棘手模式匹配【英文标题】:Tricky pattern matching of a binary string in Erlang 【发布时间】:2020-04-23 02:16:35 【问题描述】:

我正在使用 Erlang 在电子邮件服务器和 Spamassassin 之间发送消息。

我想要实现的是检索 SA 所做的测试以生成报告(我正在做某种邮件测试程序)

当 SpamAssassin 应答(通过原始 TCP)时,它会发送一个二进制字符串,如下所示:

<<"SPAMD/1.1 0 EX_OK\r\nContent-length: 728\r\nSpam: True ; 6.3 / 5.0\r\n\r\nReceived: from localhost by debpub1.cs2cloud.internal\r\n\twith SpamAssassin (version 3.4.2);\r\n\tSat, 04 Jan 2020 18:24:37 +0100\r\nFrom: bibi <bibi@XXXXX.local>\r\nTo: <aZphki8N05@XXXXXXXX>\r\nSubject: i\r\nDate: Sat, 4 Jan 2020 18:24:36 +0100\r\nMessage-Id: <3b68dede-f1c3-4f04-62dc-f0b2de6e980a@PPPPPP.local>\r\nX-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on\r\n\tdebpub1.cs2cloud.internal\r\nX-Spam-Flag: YES\r\nX-Spam-Level: ******\r\nX-Spam-Status: Yes, score=6.3 required=5.0 tests=BODY_SINGLE_WORD,\r\n\tDKIM_ADSP_NXDOMAIN,DOS_RCVD_IP_TWICE_C,HELO_MISC_IP,\r\n\tNO_FM_NAME_IP_HOSTN autolearn=no autolearn_force=no version=3.4.2\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary=\"----------=_5E10CA56.0200B819\"\r\n\r\n">>

我用粗体表示我想捡的物品:

BODY_SINGLE_WORD DKIM_ADSP_NXDOMAIN DOS_RCVD_IP_TWICE_C HELO_MISC_IP NO_FM_NAME_IP_HOSTN

然后我想像这样序列化: [>,>,…]

但这并不容易,术语没有常规的“分隔符”,有 \r\n 或 \r\n\t

我从那个表达式开始(在二进制字符串上分割',')但结果不完整

split(BinaryString, ",", all),
case lists:member(<<"HELO_MISC_IP">>, Data3 ) of
            true -> ; %push the result in a database
            false -> ok
end;

我希望我可以重新开始,并通过递归使用循环(而且因为它是一种干净且很好的循环方式),但对于这种情况,我看起来毫无意义……

split(BinaryString, Idx, Acc) ->
case BinaryString of
    <<"tests=",_This:Idx/binary, Char, Tail/binary>> ->
                case lists:member(Char, BinaryString ) of
                    false ->
                        split(BinaryString, Idx+1, Acc);
                    true -> 
                           case Tail of
                                    <<Y/binary, _Tail/binary>> ->
                                    %doing something
                                    <<_Yop2/binary>> ->
                                    %doing somethin else
                           end
                 end;

问题是我不明白如何以可接受和干净的方式实现这一目标

如果有人能帮我一把,那将非常感激。

你的

【问题讨论】:

【参考方案1】:

一种解决方案是匹配您要查找的二进制文件的各个部分:

Data = <<"SPAMD/1.1 0 EX_OK\r\nContent-length: 728\r\nSpam: True ; 6.3 / 5.0\r\n\r\nReceived: from localhost by debpub1.cs2cloud.internal\r\n\twith SpamAssassin (version 3.4.2);\r\n\tSat, 04 Jan 2020 18:24:37 +0100\r\nFrom: bibi <bibi@XXXXX.local>\r\nTo: <aZphki8N05@XXXXXXXX>\r\nSubject: i\r\nDate: Sat, 4 Jan 2020 18:24:36 +0100\r\nMessage-Id: <3b68dede-f1c3-4f04-62dc-f0b2de6e980a@PPPPPP.local>\r\nX-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on\r\n\tdebpub1.cs2cloud.internal\r\nX-Spam-Flag: YES\r\nX-Spam-Level: ******\r\nX-Spam-Status: Yes, score=6.3 required=5.0 tests=BODY_SINGLE_WORD,\r\n\tDKIM_ADSP_NXDOMAIN,DOS_RCVD_IP_TWICE_C,HELO_MISC_IP,\r\n\tNO_FM_NAME_IP_HOSTN autolearn=no autolearn_force=no version=3.4.2\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary=\"----------=_5E10CA56.0200B819\"\r\n\r\n">>,
Matches = binary:compile_pattern([<<"BODY_SINGLE_WORD">>,<<"DKIM_ADSP_NXDOMAIN">>,<<"DOS_RCVD_IP_TWICE_C">>,<<"HELO_MISC_IP">>,<<"NO_FM_NAME_IP_HOSTN">>]),
[binary:part(Data, PosLen) || PosLen <- binary:matches(Data, Matches)].

在 Erlang shell 中执行上述三行返回:

[>,>, >,>, >]

这提供了所需的结果,但它可能不安全,因为它不会尝试验证输入是否有效或匹配是否发生在有效边界上。

一种可能更安全的方法依赖于输入二进制文件类似于 HTTP 结果这一事实,因此可以使用内置的 Erlang 解码器对其进行部分解析。下面的parse/1,2 函数使用erlang:decode_packet/3 从输入中提取信息:

parse(Data) ->
    ok, Line, Rest = erlang:decode_packet(line, Data, []),
    parse(Line, Rest).
parse(<<"SPAMD/", _/binary>>, Data) ->
    parse(Data, []);
parse(<<>>, Hdrs) ->
    Result = [Key,Value || http_header, _, Key, _, Value <- Hdrs],
    process_results(Result);
parse(Data, Hdrs) ->
    case erlang:decode_packet(httph, Data, []) of
        ok, http_eoh, Rest ->
            parse(Rest, Hdrs);
        ok, Hdr, Rest ->
            parse(Rest, [Hdr|Hdrs]);
        Error ->
            Error
    end.

parse/1 函数最初使用line 解码器对输入的第一行进行解码,然后将结果传递给parse/2parse/2 的第一个子句匹配输入数据的初始行的 "SPAMD/" 前缀只是为了验证我们在正确的位置,然后递归调用 parse/2 传递剩余的 Data 和一个空的累加器列表. parse/2 的第二和第三子句将数据视为 HTTP 标头。 parse/2的第二个子句在输入数据用完时匹配;它将累积的标头列表映射到Key,Value 对列表,并将其传递给process_results/1 函数,如下所述,以完成数据提取。 parse/2 的第三个子句尝试通过httph HTTP 标头解码器对数据进行解码,累积每个匹配的标头并忽略任何http_eoh 标头结尾标记,这些标记源自嵌入在奇数位置的"\r\n" 序列输入。

对于问题中提供的输入数据,parse/1,2 函数最终将以下键值对列表传递给process_results/1

['Content-Type',"multipart/mixed; boundary=\"----------=_5E10CA56.0200B819\"","Mime-Version","1.0","X-Spam-Status","Yes, score=6.3 required=5.0 tests=BODY_SINGLE_WORD,\r\n\tDKIM_ADSP_NXDOMAIN,DOS_RCVD_IP_TWICE_C,HELO_MISC_IP,\r\n\tNO_FM_NAME_IP_HOSTN autolearn=no autolearn_force=no version=3.4.2","X-Spam-Level","******","X-Spam-Flag","YES","X-Spam-Checker-Version","SpamAssassin 3.4.2 (2018-09-13) on\r\n\tdebpub1.cs2cloud.internal","Message-Id","<3b68dede-f1c3-4f04-62dc-f0b2de6e980a@PPPPPP.local>",'Date',"Sat, 4 Jan 2020 18:24:36 +0100","Subject","i","To","<aZphki8N05@XXXXXXXX>",'From',"bibi <bibi@XXXXX.local>","Received","from localhost by debpub1.cs2cloud.internal\r\n\twith SpamAssassin (version 3.4.2);\r\n\tSat, 04 Jan 2020 18:24:37 +0100","Spam","True ; 6.3 / 5.0",'Content-Length',"728"]

process_results/1,2 函数首先匹配感兴趣的键 "X-Spam-Status",然后从其值中提取所需的数据。下面的三个函数实现process_results/1 来查找该密钥并对其进行处理,如果没有看到这样的密钥,则返回error, not_found。第二个子句匹配所需的键,将其关联值拆分为空格、逗号、回车、换行符、制表符和等号字符,并将拆分结果与空累加器一起传递给process_results/2

process_results([]) ->
    error, not_found;
process_results(["X-Spam-Status", V|_]) ->
    process_results(string:lexemes(V, " ,\r\n\t="), []);
process_results([_|T]) ->
    process_results(T).

对于问题中的输入数据,传递给process_results/2的字符串列表是

["Yes","score","6.3","required","5.0","tests","BODY_SINGLE_WORD","\r\n","DKIM_ADSP_NXDOMAIN","DOS_RCVD_IP_TWICE_C","HELO_MISC_IP","\r\n","NO_FM_NAME_IP_HOSTN","autolearn","no","autolearn_force","no","version","3.4.2"]

下面process_results/2的子句递归遍历这个字符串列表并累积匹配的结果。第二个到第六个子句中的每个子句都匹配我们寻找的值之一,并且每个子句都将匹配的字符串转换为二进制,然后再累加它。

process_results([], Results) ->
    ok, lists:reverse(Results);
process_results([V="BODY_SINGLE_WORD"|T], Results) ->
    process_results(T, [list_to_binary(V)|Results]);
process_results([V="DKIM_ADSP_NXDOMAIN"|T], Results) ->
    process_results(T, [list_to_binary(V)|Results]);
process_results([V="DOS_RCVD_IP_TWICE_C"|T], Results) ->
    process_results(T, [list_to_binary(V)|Results]);
process_results([V="HELO_MISC_IP"|T], Results) ->
    process_results(T, [list_to_binary(V)|Results]);
process_results([V="NO_FM_NAME_IP_HOSTN"|T], Results) ->
    process_results(T, [list_to_binary(V)|Results]);
process_results([_|T], Results) ->
    process_results(T, Results).

最后一个子句忽略不需要的数据。 process_results/2 的第一个子句在字符串列表为空时调用,它只返回反向累加器。对于问题中的输入数据,process_results/2的最终结果是:

好吧,[>,>,>,>,>]

【讨论】:

@lambda79 请将此答案标记为已解决您的问题。

以上是关于Erlang中二进制字符串的棘手模式匹配的主要内容,如果未能解决你的问题,请参考以下文章

模式匹配 Erlang 字符串作为函数中的列表

为什么在erlang中对此字符串进行模式匹配会导致尾部的“字符串”和列表的ascii值?

为啥 OCaml 模式匹配比 Erlang 弱?

在erlang中列出尾部模式匹配

Erlang 模式匹配顺序

Erlang - **异常错误:右侧值不匹配