[Raku] 如何簡便地讀取文字檔

作者:   發佈於: ,更新於:   #rakulang

將文字檔案內容讀取出來,都需要經過開檔、讀取、關檔這三步曲,但另外也有些較簡短的寫法,可讓人略檔開檔及關檔,讓 raku 內部自動去處理。雖然也因在使用情境上較有限制,但比較容易,也比較不會寫錯。

讀取全檔

可用 slurp 函式一次讀取全檔:

my $content = slurp("example.txt");

slurp 函式會將其參數視為檔名,並假設檔案的文字編碼為 UTF-8。

例如、讓 example.txt 內容為這兩行:

你好
Hello

將全檔讀取進來、存於 $content 中後的寫法是

my $content = slurp("example.txt");

若對 $content 進行基本檢查​,就可看到 $content 內裝了 9 個字符,與預期相同。

say $content.chars;
#=> 9

say $content.comb.Array.raku;
#=> ["你", "好", "\n", "H", "e", "l", "l", "o", "\n"]

一次讀一行、一行一行讀

如果要以行單位讀檔,可用 .lines

從檔名做出 IO::Path 物件的寫法是透過 .IO

"example.txt".IO

這寫法有點像是型別轉換。在此物件消滅時會自動關檔。

將全檔以行單位讀取後全數裝到陣列變數 @content 中:

my @content = "example.txt".IO.lines;

say @content.raku;
#=> ["你好", "Hello"]

.lines 方法其實不會立刻就把全檔讀進來,而是會做個迭代器物件,一次讀一部份,再把讀進來的部份依換行字符切開後提供出來。如果把這迭代器物件存於陣列裡,那使用起來就與普通陣列無異。使用那陣列,就相當於是在讀檔。在取第 $i 個元素時,就會讀檔讀到第 $i 行後記著,而如果之前已經讀過第 $i 行了,就會直接取出之前的記憶,不會再次讀檔。

如果不需讀取全檔,只需要前 10 行​,可透過對 .lines 取切片的方式來完成:

my @content = "example.txt".lines.[0..^10];

say @content.raku;
#=> ["你好", "Hello", Any, Any, Any, Any, Any, Any, Any, Any]

可看到,後面有很多怪怪的東西。這是因為我們的 example.txt 內容只有兩行,不到十行。因此在取切片時得到了很多「未定義值」。可利用 .grep(&defined) 將那些值去掉:

my @content = "example.txt".lines.[0..^10].grep(&defined);

say @content.raku;
#=> ["你好", "Hello"]

如果所需要的是第 10 行​到第 19 行,一樣可透過對 .lines 取切片的方式來完成:

my @content = "example.txt".lines.[10..19];

如果所需要的是「最後 5 行」,可在取切片時使用 * 算符。此時若 example.txt 內容不到 5 行,則 @content 會是空的:

my @content = "example.txt".lines.[*-5..*];

在上述幾種寫法裡,@content 會對應到一個迭代器,就算是取得 .lines 的切片後,也是得到一個只會迭代十個字串的迭代器,在第一次取值時,才會真的進行讀檔。要讓其立刻讀檔,使 @content 內容物裝的是字串,加上 .Array

@content = "example.txt".lines.Array;

若 "example.txt" 本身有 50GB 那麼大,.lines.Array 會讓 raku 立刻把 50GB 讀進來,而 .lines 則是會將讀取的過程稍微往後延。若後方的程式裡只需要看檔案前十行就結束,那這兩種寫法所花費的記憶體量就會差很多。