Erlang 从零 · 第 27 课(展开目录)
01 · 先打开 erl02 · 先认常见 term03 · 分清两种文字04 · 变量只接一次05 · 拆开一列,再筛一次06 · 把文字安全变成整数07 · 用 case 选一条路08 · 让函数继续走09 · 把代码放进模块10 · 条件必须说真话11 · 一层一层取数据12 · 文字先说清编码13 · 让函数子句排好队14 · 带着累加器往前走15 · 先看值,再选路16 · 给模块立规矩17 · 让 rebar3 管项目18 · 让结果有固定形状19 · 把磁盘当成会失败的邻居20 · 把函数交给 list21 · record 留在院里22 · 先写模块之间的约定23 · 把 term 形状写出来24 · 小测试与整段路25 · 把应用磨成命令26 · 先把小项目圈住27 · 把真实文件洗干净28 · 公开入口越少越稳29 · 把约定留在源码旁30 · 打包,也留下线索31 · 按最初任务交卷
ERLANG · 作品 · LESSON 2745 分钟

把真实文件洗干净

读取 UTF-8 binary,验证编码,再整理成非空行。

01 · 先看完整例子

保存为 trail_parser.erl

加入 parse_utf8/1 后记得保持它不在 export 列表中。

Erlang
-module(trail_parser).
-export([read/1]).

-spec read(file:filename_all()) -> {ok, [binary()]} | {error, term()}.
read(Path) ->
  case file:read_file(Path) of
    {ok, Text} ->
      parse_utf8(Text);
    {error, Reason} ->
      {error, Reason}
  end.

%% 编码验证和行清洗留在解析模块
parse_utf8(Text) ->
  case unicode:characters_to_list(Text) of
    Chars when is_list(Chars) ->
      Raw = re:split(Text, "\\R", [{return, binary}, unicode]),
      Lines = [Clean || Line <- Raw,
                        Clean <- [string:trim(Line)],
                        Clean =/= <<>>],
      {ok, Lines};
    _ ->
      {error, invalid_utf8}
  end.
先对照结果
  • 有效文件得到 {ok, Lines};无效编码得到 {error,invalid_utf8}。
02 · 回头拆代码

从第一行往下读

  1. 文件读取先处理磁盘边界。
  2. Unicode 转换成功时返回 list,错误时返回特殊 tuple。
  3. 换行切分后去两端空白并丢掉空行。
03 · 认清新符号

符号不是暗号

unicode:characters_to_list/1

验证并解码 Unicode 数据。

re:split/3

按多种换行规则切 binary。

{error, Reason}

把外部失败交给调用者。

04 · 代码里的概念

把名字和意思对上

01

解析层

把外部原始数据变成项目内部稳定 term 的模块。

02

规范化

把多种等价输入整理成一种形式。

05 · 再说清楚

这些写法为什么有用

真实文件可能有空行、不同换行符或坏掉的 UTF-8。解析层先把这些差异收拢。

Parser 只负责从路径得到行 list。它不打印、不退出程序,也不计算统计。

系统文件错误保留原 reason,编码错误统一为 invalid_utf8。

06 · 自己改一次

合上答案,动手

为两行临时文件写 EUnit 断言。

练习起点
Path = filename:join("/tmp", "trail_stats_test.txt"),
ok = file:write_file(Path, <<"A
B
">>),
?assertEqual(____, trail_parser:read(Path)).

目标结果: 期待 {ok,[<<"A">>,<<"B">>]}。

卡住了,再看提示

成功 tuple 第二项是两个 binary。

运行过以后,再看一种答案
一种答案
Path = filename:join("/tmp", "trail_stats_test.txt"),
ok = file:write_file(Path, <<"A
B
">>),
%% Parser 返回清洗后的行
?assertEqual({ok, [<<"A">>, <<"B">>]}, trail_parser:read(Path)).
想一想:Parser 为什么不直接调用 `io:format` 显示错误?

显示属于 CLI 边界。返回 term 后,测试、服务或命令都能选择自己的处理方式。

带走

记住三句

  1. 1

    外部文字先验证,再解析。

  2. 2

    Parser 返回数据,不负责显示。

  3. 3

    系统错误 reason 可以继续向上交。

本课结束代码也跑过一次,就把这一课收好。