Scala練習九檔案和正則表示式
檔案和正則表示式 |
摘要:
在本篇中,你將學習如何執行常用的檔案處理任務,比如從檔案中讀取所有行或單詞,或者讀取包含數字的檔案等。本篇的要點包括:
1. Source.fromFile(...).getLines.toArray輸出檔案的所有行
2. Source.fromFile(...).mkString以字串形式輸出檔案內容
3. 將字串轉換為數字,可以用tolnt或toDouble方法
4. 使用java的PrintWriter來寫入文字檔案
5. "正則",r是一個Regex物件
6. 如果你的正則表示式包含反斜槓或引號的話,用"""…"""
7. 如果正則模式包含分組,你可以用如下語法來提取它們的內容for (regex(變數1,變數n) <- 字串)
讀取行 |
要讀取檔案中的所有行,可以呼叫scala.io.Source物件的getLines方法:
import scala.io.Source
val source=Source.fromFile ("myfile.txt", "UTF-8")
第一個引數可以是字串或者是java.io.File,如果你知道檔案使用的是當前平臺預設的字元編碼,則可以略去第二個字元編碼引數
val linelterator = source.getLines
結果是一個迭代器。你可以用它來逐條處理這些行:
for (l <- linelterator ) 處理l
或者你也可以對迭代器應用toArray或toBuffer方法,將這些行放到陣列或陣列緩衝當中:
val lines=source.getLines.toArray
有時候,你只想把整個檔案讀取成一個字串。那更簡單了:
val contents=source.mkString
需要注意的是,在用完Source的物件後,記得呼叫close
讀取字元 |
要從檔案中讀取單個字元,你可以直接把Source物件當做迭代器,因為Source類擴充套件自Iterator[Char]:
for (c <- source) 處理c
如果你想檢視某個字元但又不處理掉它的話,呼叫source物件的buffered方法。這樣你就可以用head方法檢視下一個字元,但同時並不把它當做是已處理的字元
val source = Source.fromFile ( "myfile.txt ", "UTF-8 ")
val iter=source.buffered
while ( iter.hasNext ) {
if ( iter.head是符合預期的 )
處理iter.next
else
…….
}
Source .close()
或者,如果你的檔案不是很大,你也可以把它讀取成一個字串進行處理:
val contents = source.mkString
讀取詞法單元和數字 |
這裡有一個快而髒的方式來讀取原始檔中所有以空格隔開的詞法單元:
val tokens = source.mkString.split("\\S+")
而要把字串轉換成數字,可以用tolnt或toDouble方法。舉例來說,如果你有一個包含了浮點數的檔案,則可以將它們統統讀取到陣列中:
val numbers=for (w <- tokens) yield w.toDouble
或者
val numbers = tokens.map(_.toDouble)
需要注意的是,我們總是可以使用java.util.Scanner類來處理同時包含文字和數字的檔案。與此同時,你也可以從控制檯讀取數字:
print (" How old are you" ) // 預設情況下系統會自動引入Console,並不需要對print和readlnt使用限定詞
val age=readlnt()
注意:這些方法假定下一行輸入包含單個數字,且前後都沒有空格。否則會報NumberFormatException
從URL或其他源讀取 |
Source物件有讀取非檔案源的方法:
val source1 = Source.fromURL("http://horstamnn.com", "UTF-8")
val source2 = Source.fromString( "Hello, World! " ) // 從給定的字串讀取,這對除錯很有用
val source3 = Source.stdin //從標準輸入讀取
讀取二進位制檔案 |
Scala並沒有提供讀取二進位制檔案的方法。你需要使用Java類庫。以下是如何將檔案讀取成位元組陣列:
val file = new File (filename)
val in = new FileInputStream(file)
val bytes = new Array[Byte](file.length.tolnt)
in.read (bytes)
in.close()
寫入文字檔案 |
Scala沒有內建的對寫入檔案的支援。要寫入文字檔案,可使用java.io.PrintWriter,例如:
val out = new PrintWriter("numbers.txt")
for ( i <- 1 to 100 ) out.println(i)
out.close()
所有的邏輯都像我們預期的那樣,除了printf方法外。當你傳遞數字給printf時,編譯器會抱怨說你需要將它轉換成AnyRef:
out.printf ( "%6d %10.2f",quantity.aslnstanceOf [AnyRef], price.aslnstanceOf[AnyRef] )
為了避免這個麻煩,你也可以用string類的format方法:
out.print( "%6d %10.2f". format (quantity, price))
需要注意的是:Console類的printf沒有這個問題,你可以用來輸出訊息到控制檯。
printf("%6d %10.2f",quantity, price)
訪問目錄 |
自定義處理
目前Scala並沒有"正式的"用來訪問某個目錄中的所有檔案,或者遞迴地遍歷所有目錄的類。下面,我們將探討一些替代方案。編寫產出遍歷某目錄下所有子目錄的函式並不複雜:
import java.io.File
def subdirs (dir: File):Iterator[File] = {
val children = dir.listFiles.filter(_.isDirectory)
children.tolterator ++ children.toIterator.flatMap( subdirs _ )
}
利用這個函式,你可以像這樣訪問所有的子目錄:
for(d <- subdirs (dir))處理d
或者,如果你用的是Java 7,你也可以使用java.nio.file.Files類的walkFileTree方法,該類用到了FileVisitor介面。
函式物件處理
在Scala中,我們通常喜歡用函式物件來指定工作內容,而不是介面。以下隱式轉換讓函式可以與介面相匹配:
import java.nio.file._
implicit def makeFileVisitor( f: (Path)=>Unit ) = new SimpleFileVisitor[Path] {
override def visitFile( p: Path, attrs: attribute.BasicFileAttributes ) = {
f(p)
FileVisitResult.CONTINUE
}
}
這樣一來,你就可以通過以下呼叫來打印出所有的子目錄了:
Files.walkFileTree( dir.toPath, (f:Path) => println(f) )
當然,如果你不僅僅是想要打印出這些檔案,則也可以在傳AwalkFileTree方法的函式中指定其他要執行的動作
序列化 |
在Java中,我們用序列化來將物件傳輸到其他虛擬機器,或臨時儲存。對於長期儲存而言,序列化可能會比較笨拙,因為隨著類的演進更新,處理不同版本間的物件是很煩瑣的一件事。以下是如何在Java和Scala中宣告一個可被序列化的類。
Java
public class Person implements java.io.Serializable {
private static final long serialVersionUID=42L;
}
Scala
@SerialVersionUID(42L) class Person extends Serializable
Serializable特質定義在scala包,因此不需要顯式引入。如果你能接受預設的ID,也可略去@SerialVersionUID註解。你可以按照常規的方式對物件進行序列化和反序列化:
val fred = new Person ()
import java.io._
val out = new ObjectOutputStream(new FileOutputStream ("/tmp/test.obj"))
out.writeObject (fred)
out.close()
val in = new ObjectlnputStream ( new FilelnputStream("/tmp/test.obj")
val savedFred=in.readObject() .aslnstanceOf[Person]
Scala集合類都是可序列化的,因此你可以把它們用做你的可序列化類的成員:
class Person extends Serializable{
private val friends = new ArrayBuffer[Person] // ArrayBuffer是可序列化的
}
程序控制 |
Scala指令碼
按照傳統習慣,程式設計師使用shell指令碼來執行日常處理任務,比如把檔案從一處移動到另一處,或者將一組檔案拼接在一起。shell語言使得我們可以很容易地指定所需要的檔案子集,以及將某個程式的輸出以管道方式作為另一個程式的輸入。話雖如此,從程式語言的角度看,大多數shell語言並不是那麼完美。
Scala的設計目標之一就是能在簡單的指令碼化任務和大型程式之間保持良好的伸縮性。scala.sys.process包提供了用於與shell程式互動的工具。你可以用Scala編寫shell指令碼,利用Scala提供的所有威力。如下是一個簡單的示例:
import sys.process._
"ls -al .." !
這樣做的結果是,Is -al ..命令被執行,顯示上層目錄的所有檔案。執行結果被列印到標準輸出。sys.process包包含了一個從字串到ProcessBuilder物件的隱式轉換。!操作符執行的就是這個ProcessBuilder物件。!操作符返回的結果是被執行程式的返回值:程式成功執行的話就是0,否則就是顯示錯誤的非0值。
操作符和管道
如果你使用! !操作符而不是!操作符的話,輸出會以字串的形式返回:
val result = "ls -al .." ! !
你還可以將一個程式的輸出以管道形式作為輸入傳送到另一個程式,用}}I操作符:
"ls -al .." #| "grep sec" !
正如你看到的,程序類庫使用的是底層作業系統的命令。在本例中,我用的是bash命令,因為bash在Linux、Mac OS X和Windows中都能找到
常用操作符
要把輸出重定向到檔案,使用撐#>操作符:
"ls -al .." #> new File("output.txt") !
要追加到檔案末尾而不是從頭覆蓋的話,使用#>>操作符:
"ls -al .." #>> new File ("output.txt") !
要把某個檔案的內容作為輸入,使用#<操作符:
"grep sec" #< new File("output.txt") !
你還可以從URL重定向輸入:
你可以將程序結合在一起使用,比如p#&&q:如果p成功,則執q;以及p#||q:如果p不成功,則執行q。由上可知,程序庫使用人們熟悉的shell操作符I > >> < && ||,只不過給它們加上了#字首,因此它們的優先順序是相同的。
不同目錄與環境變數執行程序
如果你需要在不同的目錄下執行程序,或者使用不同的環境變數,用Process物件的apply方法來構造ProcessBuilder,給出命令和起始目錄,以及一串(名稱,值)對偶來設定環境變數:
val p=Procass (cmd, new File (dirName), ("_LANG","nen US"))
然後用!操作符執行它:
"ech0 42" #I p !
正則表示式 |
當你在處理輸入的時候,你經常會想要用正則表示式來分析它。scala.util.matching.Regex類讓這件事情變得簡單。要構造一個Regex物件,用String類的r方法即可:
val numPattern="[0-9]+ ".r
如果正則表示式包含反斜槓或引號的話,那麼最好使用"原始"字串語法。例如:
val wsnumwsPattern = """\s+[0-9]+\s+""".r // 和"\\s+[0-9]+\\s+".r相比要更易讀一些
findAllln方法返回遍歷所有匹配項的迭代器。你可以在for迴圈中使用它:
for ( matchString <- numPattern.findAllln( "99 bottles, 98 bottles"))
處理matchString
或者將迭代器轉成陣列:
val matches = numPattern.findAllln("99 bottles, 98 bottles").toArray // Array(99, 98)
要找到字串中的首個匹配項,可使用findFirstln。你得到的結果是一個Option[String]
val ml = wsnumwsPattern.findFirstln("99 bottles, 98 bottles") //Some("98")
要檢查是否某個字串的開始部分能匹配,可用findPrefixOf:
numPattern.findPreflxOf("99 bottlesf 98 bottles") //Some(99)
wSnumwsPattern.findPrefixOf("99 bottles, 98 bottles") // None
你可以替換首個匹配項,或全部匹配項:
numPattern.replaceFirstln("99 bottles, 98 bottles", "XX") // "XX bottles, 98 bottles"
numPattern. replaceAllIn("99 bottles, 98 bottles", "XX") // "XX bottles, XX bottles"
正則表示式組 |
分組可以讓我們方便地獲取正則表示式的子表示式。在你想要提取的子表示式兩側加上圓括號,例如:
val numitemPattern = " ([0-9]+) ([a-z]+) ".r
要匹配組,可以把正則表示式物件當做"提取器"使用,就像這樣:
val numitemPattern (num, item) = "99 bottles" // 將num設為"99",item設為"bottles"
如果你想要從多個匹配項中提取分組內容,可以像這樣使用for語句:
for (numitemPattern (num,item) <- numitemPattern.findAllln("99 bottles, 98 bottles"))
處理num和item