我在从二进制消息中提取字段时有点挣扎。原始邮件如下所示:
<<1,0,97,98,99,100,0,0,0,3,0,0,0,0,0,0,0,0,0,3,32,3,0,0,88,2,0,0>>
我知道字段的顺序,类型和静态大小,有些人考虑了仲裁规模,所以我尝试做类似以下的事情:
newobj(Data) ->
io:fwrite("NewObj RAW ~p~n",[Data]),
NewObj = {obj,rest(uint16(string(uint16({[],Data},id),type),parent),unparsed)},
io:fwrite("NewObj ~p~n",[NewObj]),
NewObj.
uint16 / 2 , string / 2 , rest / 2 实际上是提取功能,如下所示:
uint16(ListData, Name) ->
{List, Data} = ListData,
case Data of
<<Int:2/little-unsigned-unit:8, Rest/binary>> ->
{List ++ [{Name,Int}], Rest};
<<Int:2/little-unsigned-unit:8>> ->
List ++ [{Name,Int}]
end.
string(ListData, Name) ->
{List, Data} = ListData,
Split = binary:split(Data,<<0>>),
String = lists:nth(1, Split),
if
length(Split) == 2 ->
{List ++ [{Name, String}], lists:nth(2, Split)};
true ->
List ++ [{Name, String}]
end.
rest(ListData, Name) ->
{List, Data} = ListData,
List ++ [{Name, Data}].
这有效,看起来像:
NewObj RAW <<1,0,97,98,99,100,0,0,0,3,0,0,0,0,0,0,0,0,0,3,32,3,0,0,88,2,0,0>>
NewObj {obj,[{id,1},
{type,<<"abcd">>},
{parent,0},
{unparsed,<<3,0,0,0,0,0,0,0,0,0,3,32,3,0,0,88,2,0,0>>}]}
这个问题的原因是,将{List,Data}作为ListData传递然后在函数中将其与{List,Data} = ListData分开,感觉很笨 - 所以有更好的方法吗?我认为我不能使用静态匹配,因为“未解析”和“类型”部分具有任意长度,因此不可能定义它们各自的大小。
谢谢!
---------------更新-----------------
尝试将以下评论纳入考虑范围 - 代码现在如下所示:
newobj(Data) ->
io:fwrite("NewObj RAW ~p~n",[Data]),
NewObj = {obj,field(
field(
field({[], Data},id,fun uint16/1),
type, fun string/1),
unparsed,fun rest/1)},
io:fwrite("NewObj ~p~n",[NewObj]).
field({List, Data}, Name, Func) ->
{Value,Size} = Func(Data),
case Data of
<<_:Size/binary-unit:8>> ->
[{Name,Value}|List];
<<_:Size/binary-unit:8, Rest/binary>> ->
{[{Name,Value}|List], Rest}
end.
uint16(Data) ->
case Data of
<<UInt16:2/little-unsigned-unit:8, _/binary>> ->
{UInt16,2};
<<UInt16:2/little-unsigned-unit:8>> ->
{UInt16,2}
end.
string(Data) ->
Split = binary:split(Data,<<0>>),
case Split of
[String, Rest] ->
{String,byte_size(String)+1};
[String] ->
{String,byte_size(String)+1}
end.
rest(Data) ->
{Data,byte_size(Data)}.
答案 0 :(得分:3)
代码是非惯用的,有些部分无法编译:-)以下是一些评论:
newobj/1
函数引用未绑定的NewObj
变量。可能真正的代码类似于NewObj = {obj,rest(...
?
代码多次使用list append(++
)。如果可能,应该避免这种情况,因为它执行的内存副本太多。惯用的方法是根据需要多次添加到列表的头部(即:L2 = [NewThing | L1]
)并在最后调用lists:reverse/1
。请参阅任何Erlang书籍或免费了解自己一些Erlang的详细信息。
同样,应避免使用lists:nth/2
并替换为模式匹配或构造列表或解析二进制文件的不同方式
Dogbert关于直接在函数参数中进行模式匹配的建议是一种很好的习惯方法,允许从代码中删除一些行。
关于调试方法的最后建议,考虑用适当的单元测试替换fwrite函数。
希望这给出了一些关于要看什么的提示。随意附上代码更改的问题,我们可以从那里继续。
修改强>
它看起来更好。让我们看看我们是否可以简化。请注意,我们正在向后工作,因为我们在编写生产代码后添加测试,而不是进行测试驱动开发。
我也改变了列表的顺序,因为它看起来更自然。
-include_lib("eunit/include/eunit.hrl").
happy_input_test() ->
Rest = <<3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 32, 3, 0, 0, 88, 2, 0, 0>>,
Input = <<1, 0,
97, 98, 99, 100, 0,
0, 0,
Rest/binary>>,
Expected = {obj, [{id, 1}, {type, <<"abcd">>}, {parent, 0}, {unparsed, Rest}]},
?assertEqual(Expected, binparse:newobj(Input)).
我们可以使用rebar3 eunit
以及其他方式运行此项(请参阅rebar3文档;我建议从rebar3 new lib mylib
开始创建骨架)。
您的描述不足以了解哪些字段是必填字段,哪些字段是可选字段以及obj
后是否还有其他字段。
在最简单的情况下, all 您的代码可以简化为:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
[Type, Rest2] = binary:split(Rest, <<0>>),
<<Parent:16/little-unsigned, Rest3/binary>> = Rest2,
{obj, [{id, Id}, {type, Type}, {parent, Parent}, {unparsed, Rest3}]}.
相当紧凑: - )
我发现字符串的编码非常奇怪:二进制编码,其中字符串是NUL终止的(因此强制遍历二进制文件)而不是用2或4个字节编码来表示长度,然后是字符串本身。
由于我们正在解析二进制文件,这可能来自我们系统的外部。因此,让它崩溃的理念并不适用,我们必须执行完整的输入验证。
我假设除了unparsed
之外所有字段都是必填字段,可以为空。
missing_unparsed_is_ok_test() ->
Input = <<1, 0,
97, 98, 99, 100, 0,
0, 0>>,
Expected = {obj, [{id, 1}, {type, <<"abcd">>}, {parent, 0}, {unparsed, <<>>}]},
?assertEqual(Expected, binparse:newobj(Input)).
上面的简单实现传递了它。
我们添加测试并做出API决定:该函数将返回错误元组。
missing_parent_is_error_test() ->
Input = <<1, 0,
97, 98, 99, 100, 0>>,
?assertEqual({error, bad_parent}, binparse:newobj(Input)).
malformed_parent_is_error_test() ->
Input = <<1, 0,
97, 98, 99, 100, 0,
0>>,
?assertEqual({error, bad_parent}, binparse:newobj(Input)).
我们更改实现以通过测试:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
[Type, Rest2] = binary:split(Rest, <<0>>),
case Rest2 of
<<Parent:16/little-unsigned, Rest3/binary>> ->
{obj, [{id, Id}, {type, Type}, {parent, Parent}, {unparsed, Rest3}]};
Rest2 ->
{error, bad_parent}
end.
新测试:
missing_type_is_error_test() ->
Input = <<1, 0>>,
?assertEqual({error, bad_type}, binparse:newobj(Input)).
malformed_type_is_error_test() ->
Input = <<1, 0,
97, 98, 99, 100>>,
?assertEqual({error, bad_type}, binparse:newobj(Input)).
我们可能会尝试按如下方式更改实施:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
case binary:split(Rest, <<0>>) of
[Type, Rest2] ->
case Rest2 of
<<Parent:16/little-unsigned, Rest3/binary>> ->
{obj, [
{id, Id}, {type, Type},
{parent, Parent}, {unparsed, Rest3}
]};
Rest2 ->
{error, bad_parent}
end;
[Rest] -> {error, bad_type}
end.
这是一个难以理解的混乱。只是添加功能并不能帮助我们:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
case parse_type(Rest) of
{ok, {Type, Rest2}} ->
case parse_parent(Rest2) of
{ok, Parent, Rest3} ->
{obj, [
{id, Id}, {type, Type},
{parent, Parent}, {unparsed, Rest3}
]};
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end.
parse_type(Bin) ->
case binary:split(Bin, <<0>>) of
[Type, Rest] -> {ok, {Type, Rest}};
[Bin] -> {error, bad_type}
end.
parse_parent(Bin) ->
case Bin of
<<Parent:16/little-unsigned, Rest/binary>> -> {ok, Parent, Rest};
Bin -> {error, bad_parent}
end.
这是Erlang中使用嵌套条件的典型问题。
这是我的方法,非常通用,因此适用于许多领域(我认为)。总体思路来自回溯,如http://rvirding.blogspot.com/2009/03/backtracking-in-erlang-part-1-control.html
中所述我们为每个解析步骤创建一个函数,并将它们作为列表传递给call_while_ok/3
:
newobj(Bin) ->
Parsers = [fun parse_id/1,
fun parse_type/1,
fun parse_parent/1,
fun(X) -> {ok, {unparsed, X}, <<>>} end
],
case call_while_ok(Parsers, Bin, []) of
{error, Reason} -> {error, Reason};
PropList -> {obj, PropList}
end.
功能call_while_ok/3
与lists:foldl
和lists:filter
有某种关联:
call_while_ok([F], Seed, Acc) ->
case F(Seed) of
{ok, Value, _NextSeed} -> lists:reverse([Value | Acc]);
{error, Reason} -> {error, Reason}
end;
call_while_ok([F | Fs], Seed, Acc) ->
case F(Seed) of
{ok, Value, NextSeed} -> call_while_ok(Fs, NextSeed, [Value | Acc]);
{error, Reason} -> {error, Reason}
end.
以下是解析功能。请注意,它们的签名始终相同:
parse_id(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
{ok, {id, Id}, Rest}.
parse_type(Bin) ->
case binary:split(Bin, <<0>>) of
[Type, Rest] -> {ok, {type, Type}, Rest};
[Bin] -> {error, bad_type}
end.
parse_parent(Bin) ->
case Bin of
<<Parent:16/little-unsigned, Rest/binary>> ->
{ok, {parent, Parent}, Rest};
Bin -> {error, bad_parent}
end.
列表[{id, 1}, {type, <<"abcd">>}, {parent, 0}, {unparsed, Rest}]
是proplist
(参见Erlang文档),它早于Erlang地图。
查看地图文档,看看是否有意义返回地图。