Erlang 中二进制字符串的棘手模式匹配
Tricky pattern matching of a binary string in Erlang
我正在使用 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
然后我想这样序列化:
[<<"DKIM_ADSP_NXDOMAIN">>,<<"DOS_RCVD_IP_TWICE_C">>,...]
但这并不容易,术语没有正则"delimitors",有\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;
问题是我看不出如何以可接受且干净的方式实现这一目标
如果有人能帮助我,那将是非常非常感谢的。
你的
一种解决方案是匹配您要查找的二进制文件的部分:
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 returns:
[<<"BODY_SINGLE_WORD">>,<<"DKIM_ADSP_NXDOMAIN">>, <<"DOS_RCVD_IP_TWICE_C">>,<<"HELO_MISC_IP">>, <<"NO_FM_NAME_IP_HOSTN">>]
这提供了所需的结果,但它可能不安全,因为它不会尝试验证输入是否有效或匹配是否发生在有效边界上。
一种可能更安全的方法依赖于输入二进制类似于 HTTP 结果的事实,因此可以使用 built-in 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/2
。 parse/2
的第一个子句匹配输入数据初始行的 "SPAMD/"
前缀只是为了验证我们在正确的位置查找,然后递归调用 parse/2
传递剩余的 Data
和一个空的累加器列表。 parse/2
的第二个和第三个子句将数据视为 HTTP headers。 parse/2
的第二个子句在输入数据耗尽时匹配;它将累积的 header 列表映射到 {Key,Value}
对列表,并将其传递给下面描述的 process_results/1
函数以完成数据提取。 parse/2
的第三个子句尝试通过 httph
HTTP header 解码器解码数据,累积每个匹配的 header 并忽略任何 http_eoh
end-of-headers来自 "\r\n"
序列的标记,这些序列嵌入在输入中的奇数位置。
对于问题中提供的输入数据,parse/1,2
函数最终将以下 key-value 对列表传递给 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
来查找那个键并处理它,或者 return {error, not_found}
如果没有看到这样的键。第二个子句匹配所需的键,在 space、逗号、回车符 return、换行符、制表符和等号字符上拆分其关联值,并将拆分结果与空累加器一起传递给 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
的第一个子句在字符串列表为空时被调用,它只是 return 的反向累加器。对于题中的输入数据,process_results/2
的最终结果为:
{ok, [<<"BODY_SINGLE_WORD">>,<<"DKIM_ADSP_NXDOMAIN">>,<<"DOS_RCVD_IP_TWICE_C">>,<<"HELO_MISC_IP">>,<<"NO_FM_NAME_IP_HOSTN">>]}
我正在使用 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
然后我想这样序列化: [<<"DKIM_ADSP_NXDOMAIN">>,<<"DOS_RCVD_IP_TWICE_C">>,...]
但这并不容易,术语没有正则"delimitors",有\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;
问题是我看不出如何以可接受且干净的方式实现这一目标
如果有人能帮助我,那将是非常非常感谢的。
你的
一种解决方案是匹配您要查找的二进制文件的部分:
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 returns:
[<<"BODY_SINGLE_WORD">>,<<"DKIM_ADSP_NXDOMAIN">>, <<"DOS_RCVD_IP_TWICE_C">>,<<"HELO_MISC_IP">>, <<"NO_FM_NAME_IP_HOSTN">>]
这提供了所需的结果,但它可能不安全,因为它不会尝试验证输入是否有效或匹配是否发生在有效边界上。
一种可能更安全的方法依赖于输入二进制类似于 HTTP 结果的事实,因此可以使用 built-in 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/2
。 parse/2
的第一个子句匹配输入数据初始行的 "SPAMD/"
前缀只是为了验证我们在正确的位置查找,然后递归调用 parse/2
传递剩余的 Data
和一个空的累加器列表。 parse/2
的第二个和第三个子句将数据视为 HTTP headers。 parse/2
的第二个子句在输入数据耗尽时匹配;它将累积的 header 列表映射到 {Key,Value}
对列表,并将其传递给下面描述的 process_results/1
函数以完成数据提取。 parse/2
的第三个子句尝试通过 httph
HTTP header 解码器解码数据,累积每个匹配的 header 并忽略任何 http_eoh
end-of-headers来自 "\r\n"
序列的标记,这些序列嵌入在输入中的奇数位置。
对于问题中提供的输入数据,parse/1,2
函数最终将以下 key-value 对列表传递给 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
来查找那个键并处理它,或者 return {error, not_found}
如果没有看到这样的键。第二个子句匹配所需的键,在 space、逗号、回车符 return、换行符、制表符和等号字符上拆分其关联值,并将拆分结果与空累加器一起传递给 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
的第一个子句在字符串列表为空时被调用,它只是 return 的反向累加器。对于题中的输入数据,process_results/2
的最终结果为:
{ok, [<<"BODY_SINGLE_WORD">>,<<"DKIM_ADSP_NXDOMAIN">>,<<"DOS_RCVD_IP_TWICE_C">>,<<"HELO_MISC_IP">>,<<"NO_FM_NAME_IP_HOSTN">>]}