用 Erlang 做檔案文字搜尋、替換、轉換

學習函數式語言之後,我一直在幫這種語言找出路。在國外有些工作可以用得到這些,但是,在台灣卻都只有當紅、或是教科書上的語言。對於這種冷門語言,除了沒聽過就是沒聽過,連具體討論其優缺點的文章都相當少。所以,我觀望了台灣軟體環境之後,覺得基本上是因為台灣軟體學習環境及人們的學習態度不太夠,所以只有幾種語言特別風行。而且因為只有幾種語言特別風行,人們就以為全世界只有幾種語言可以用。

抱怨完了。這篇要說說我幫 Erlang 在台灣找到的一項立足點:雖然不能做主流開發、製造軟體的語言,但起碼做一個快速輔助工具是很合適的。有時您只是想要算個數字或數學模型而已,如果使用 Java 之類的語言還得先把程式框架布局好,這些額外的工作經常會模糊了焦點。而使用了 Erlang ,從寫第一個函數開始,我就是在做解決問題的核心工作。按照輔助工具的觀點來使用 Erlang ,我已經受惠許多。

這篇要談的例子是,我有一個工作項目要用 Erlang 程式從一批檔案中找到一個關鍵詞,先把關鍵詞存在的位置整理成 .csv (逗點分格數值格式) 。然後用 Excel 開啟 .csv 人工作業,將每個檔案找到的位置判斷是不是要替換的值,把不符合的記錄刪掉。最後再用另一個 Erlang 程式讀取 .csv ,將指定位置的詞彙代換為另一個詞彙。這些工作分為三個檔案:

  1. finder.erl :開啟檔案,尋找詞彙,然後匯出 .csv 文字。 (之後人工作業:將 .csv 文字貼到 Microsoft Excel 表格,調整格式,然後轉存為 XML 檔案。)
  2. xslt.erl :讀取 XML 檔案,轉換為自己需要的文字格式,提供替換程式的參數讀取。
  3. operation.erl :讀取替換文字參數 (含檔案名稱、原詞彙、行數) ,檔案備份,檔案內容替換。

尋找關鍵字行數 (find.erl)

首先是開啟檔案並讀出檔案內容。開啟大批檔案時,需要注意的是讀完資料要關掉檔案,否則,已經開啟的IO裝置變數會一直累積,使 Erlang 系統滿載而最後造成程式執行失敗。

read(Fname) ->
    FD = case file:open(Fname,[read]) of
	     {ok, Dev} ->
		 Dev;
	     {error, Reason} ->
		 erlang:error(io:format("Cannot open the file `~s': ~s.~n",
					[Fname, Reason])
			      )
	 end,
    Result = fread_all(FD),
    file:close(FD),
    Result.

fread_all(IODev) ->
    case file:read_line(IODev) of
	eof ->
	    [];
	{ok, Line} ->
	    [Line|fread_all(IODev)];
	{error, Reason} ->
	    erlang:error(io:format("File read fault: ~s.~n",
				   [Reason])
			)
    end.

接著是要了解 regexp:match/2 的運作。regexp 模組實作正規表示法,但 Erlang HiPE 建議改用 re 模組,後者實作 Perl 風格的正規表示法。呼叫 regexp:match/2 可能會得到 {match,Start,Length} 或 nomatch 或 {error,Reason} 。所以,接著可以寫一個函數,從指定檔案的檔案內容的指定行數,尋找關鍵詞,如果找到了就將關鍵詞的檔名、詞彙、行數列入函數結果。

find(_fname, [], _cnt, _keyword) ->
        [];
find(Fname, [Line|Rest], N, Kw) ->
        case regexp:match(string:to_lower(Line),
               lists:flatten(
                       io_lib:format(
                               "[^_a-zA-Z0-9.>#='\"]([_a-zA-Z0-9]*[.])*~s[^_a-zA-Z0-9.<#=:'[\"]",
                               [Kw]
                       )
               )
        ) of
                       {match,_,_} ->
                               [lists:flatten(io_lib:format("~s,~s,~w", [Fname,Kw,N]))|find(Fname,Rest,N+1,Kw)];
                       nomatch ->
                               find(Fname,Rest,N+1,Kw);
                       {error,Desct} ->
                              erlang:error(io:format("Finding and Error: ~s.~n", [Desct]))
        end.

find/3 將 find/4 的輸出資料印成文字格式,使人工用滑鼠選取作業方便。

find(Mname, Fname, Keyword) ->
        lists:foldl(
                fun (X, _) -> io:format("~s~n",[X])  end,
                ok,
                find(path(Mname,Fname), read(file(Mname,Fname)), 1, Keyword)
        ).

為了更方便,來個 find/2 做 dir 檔案列表指令,把一個目錄之下的全部檔案都檢查一次。

find(Mname, Keyword) ->
        {ok,FL} = file:list_dir(folder(Mname)),
        lists:map(
                fun (X) ->
                        find(Mname, X, Keyword)
                end,
                FL
        ).

最前面,將介面函數整理好,就盡可能自動化了。

-module(finder).
-export([find/0]).

find() ->
        {Mname, KWList} = list(),
        find_all(Mname, KWList).

list() ->
        {
                "prefix"
                , ["file_1"
	           , "file_2"
	           , "file_3"
                   , ...
                  ]
        }.

find_all(Mname, KWList) ->
        lists:map(
                fun(X) ->
                        find(Mname, X)
               end,
               KWList
        ).

使用 XSLT 轉換檔案 (xslt.erl)

XSLT 是很不錯的函數式 XML 資料轉換語言。 Erlang 的 Xmerl 程式庫實作 XSLT 可以非常直接,因為 Erlang 和 XSLT 都符合函數式風格。首先,定義 Erlang 程式標頭時,要引用 Xmerl 的標頭檔案 xmerl.hrl 。如以下引用並包含了 Xmerl 的幾個函數: xslapply/2 對應 xsl:template , value_of/1 對應 xsl:value-of , select/2 對應 XSLT select 屬性 (使用 XPath 查詢 XML 元素) , built_in_rules/2 提供 Xmerl 程式的預設規則。

-module(xslt).
-compile(export_all).
-include("C:\\Program Files\\erl5.8\\lib\\xmerl-1.2.5\\include\\xmerl.hrl").
-import(xmerl_xs, [xslapply/2, value_of/1, select/2, built_in_rules/2]).

輸入檔案是一個 XML 檔。檔案交由 xmer_scan:file/1 讀取並整理成值組的結構。這樣輸入的資料就準備好了。

filename() ->
    "C:\\Documents and Settings\\yauhsien\\My Documents\\Reference_Tables_revised.xml".

scan() ->
    xmerl_scan:file(filename()).

轉換的部份,首先要寫好一條基本規則 template/1 ,使用到 xmerl:built_in_rules/2 ,遇到任何結構,就套用各種特定的轉換規則。其他的轉換規則也會寫成 template/1 ,列在本條規則之上。

template(E) ->
    built_in_rules(fun template/1, E).

接著在基本規則上寫出幾條 Excel XML 的轉換規則。目的是把每個格子 (cell) 的資料按每一列列出為逗點分隔格式:

template(E = #xmlElement{name = 'Workbook'}) ->
    xslapply(fun template/1, select("//Row", E));
template(E = #xmlElement{name = 'Row'}) ->
    lists:foldr(
      fun (X,Y) ->
	      case Y of
		  [] ->
		      X;
		  _ ->
		      string:concat(string:concat(X,","),Y)
	      end
      end,
      [],
      xslapply(fun template/1, select("Cell", E))
     );
template(E = #xmlElement{name = 'Cell'}) ->
    [V] = value_of(select("Data",E)),
    V;
template(E) ->
    built_in_rules(fun template/1, E).

最後把主程式 transform/0 整理好,做完完整的讀檔、轉換、印出等動作。

transform() ->
    {E,_} = scan(),
    io:format("~s~n", [lists:flatten(xslapply(fun template/1, E))]).

替換檔案文字 (operation.erl)

這次的工作內容是要用正規表示法取出關鍵詞,將關鍵詞的前面加上前綴字。一樣,先把讀檔案內容的程式寫好:

read(Fname) ->
    FD = case file:open(Fname,[read]) of
	     {ok, Dev} ->
		 Dev;
	     {error, Reason} ->
		 erlang:error(io:format("Cannot open the file `~s': ~s.~n",
					[Fname, Reason])
			      )
	 end,
    Result = fread_all(FD),
    file:close(FD),
    Result.
fread_all(IODev) ->
    case file:read_line(IODev) of
	eof ->
	    [];
	{ok, Line} ->
	    [Line|fread_all(IODev)];
	{error, Reason} ->
	    erlang:error(io:format("File read fault: ~s.~n",
				   [Reason])
			)
    end.

對每一個檔案,我需要一個 save/2 函數,將檔案內容備份起來。這個函數的輸入是,假設已經先把一個檔案的內容讀出來,所以 save/2 的參數是檔案內容和檔名。

save({FContent, FName}) ->
    FOrigin = file(FName),
    FBackup = string:concat(FOrigin, ".bak"),
    FOD = case file:open(FOrigin,[read,write]) of
	      {ok,Dev} -> Dev;
	      {error,Reason} ->
		  erlang:error(io:format("Cannot open the file `~s': ~s.~n",
					 [FOrigin,Reason])
			      )
	  end,
    FBD = case file:open(FBackup,[write]) of
	      {ok, Dev1} -> Dev1;
	      {error,Reason1} ->
		  erlang:error(io:format("Cannot open the file `~s': ~s.~n",
					 [FBackup,Reason1])
			      )
	  end,
    FOContent = fread_all(FOD),
    io:fwrite(FBD, "~s", [FOContent]),
    file:close(FBD),
    file:position(FOD, bof),
    io:fwrite(FOD, "~s", [FContent]),
    file:close(FOD).

替換工作的程式是讀出檔案內容、在檔案內容中尋找關鍵字 (不以底線和其他文字相連) 、必要時將關鍵字的特定前綴字 (“fbr.") 移掉,最後整理為替換之後的檔案內容。

approach(GList = [[FName,_,_]|_]) ->
    FContent = read(file(FName)),
    replace(FContent, GList).

replace(FContent, []) ->
    FContent;
replace(FContent, [[_,Term,LCount]|Rules]) ->
    replace(
      replace(FContent, Term, LCount, string:concat(prefix(),Term))
      , Rules
      ).
replace([], _, _, _) ->
    [];
replace([Line|FContent], Term, 1, NewTerm) ->
    case re:run(string:to_lower(Line), string:concat(Term, "[^_]")) of
	{match,List} ->
	    [replace1(Line, List, NewTerm)|FContent];
	_ ->
	    [Line|FContent]
    end;
replace([Line|FContent], Term, LCount, NewTerm) when LCount > 1 ->
    [Line|replace(FContent, Term, LCount-1, NewTerm)].
replace1(Line, [], _) ->
    Line;
replace1(Line, [{M,N}|List], Term) ->
    HStr = string:substr(Line, 1, M),
    HStr1 = erase_suffix(HStr, "fbr."),
    TStr = string:substr(Line, M+N+1),
    case TStr of
	[32|_] ->
	    TStr1 = TStr;
	_ ->
	    TStr1 = [32|TStr]
    end,
    replace1(lists:append([HStr1,string:to_upper(Term),TStr1]), List, Term).

erase_suffix(Str, Term) ->
    case re:run(string:to_lower(Str), Term) of
	{match, List} ->
	    {M, N} = lists:last(List),
	    if
		M + N == length(Str) ->
		    string:substr(Str, 1, length(Str)-N);
		true ->
		    Str
	    end;
	_ ->
	    Str
    end.

我還需要有 table/0 將逗點分隔文字檔轉換成 `檔名’ 、`關鍵字’ 、`行數’ 的列表,其中 `檔名’ 、`關鍵字’ 是文字, `行數’ 應當是整數。在此用了字串分割及轉換函數。

table() ->
    List = read("replacement.txt"),
    lists:map(
      fun(Str) ->
	      [A,B,C|_] = string:tokens(Str, ",\n"),
	      {N, _} = string:to_integer(C),
	      [A,B,N]
      end,
      clean(List)
      ).

clean([]) ->
    [];
clean([Str|Ss]) ->
    case re:run(Str, "^\s*$") of
	{match, _} ->
	    clean(Ss);
	_ ->
	    [Str|clean(Ss)]
    end.

最後把主程式寫出來,使用到以上寫好的幾個程式單元,把工作做完。這個部份的工作是備份檔案,然後將原檔案的指定行數關鍵字替換為加了前綴詞的關鍵字。

-module(operation).
-export([start/0]).

prefix() ->
    "#Application.HW_User#.".

file(Filename) ->
    string:concat("C:/Inetpub/wwwroot", Filename).

start() ->
    GTable = group(table()),
    RawResult = 
	lists:map(
	  fun lists:append/1,
	  lists:map(
	    fun approach/1,
	    GTable
	   )
	 ),
    Result =
	lists:map(
	  fun({S,[[F,_,_]|_]}) ->
		  {S,F}
	  end,
	  lists:zip(RawResult, GTable)
	 ),
    lists:map(
      fun save/1,
      Result
     ).

group([A]) ->
    [[A]];
group([H,H1|T]) ->
    [[H2|T2]|T1] = group([H1|T]),
    [A,_,_] = H,
    [A1,_,_] = H2,
    if
	A == A1 ->
	    [[H,H2|T2]|T1];
	true ->
	    [[H],[H2|T2]|T1]
    end.

以上,在我的工作場合三個階段的工作項目中,我用 Erlang 分別做出三個支援用的程式幫助工作進行。這些程式看起來不起眼,但是以同樣的程式類型,不同的語言來看,當別人使用別的程式語言仍在敲打許多程式框架的時候,我已經在專攻程式的核心工作了。

下期預告:前一陣子我有另一個工作項目需要一些動態規劃的工作模型,是處理線上工作項目指派的問題。我用 Erlang 很快做了一個簡單的模擬環境,檢查一個貝式定理基礎的權重公式執行起來是不是夠穩定。撰寫這個程式也是將 Erlang 應用在工作支援的範例,是讓我近期比較覺得自豪的一個例子。

廣告

About 黃耀賢 (Yau-Hsien Huang)

熱愛 Erlang ,並且有相關工作經驗。喜歡程式語言。喜歡邏輯。目前用 Python 工作。
本篇發表於 Application, Erlang。將永久鏈結加入書籤。

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s