基於 Java 中的模式修改檔案內容
1.概述
在維護或轉換文字資料時,我們經常需要修改現有文件的內容。在 Java 中,有多種方法可以實現此目的,具體取決於檔案大小和效能需求。小檔案可以輕鬆地在記憶體中處理,而大檔案則最好使用流逐行處理。
在本教程中,我們將探索兩種基於模式修改文件內容的方法。這兩種方法都依賴 Java 的現代 Java NIO API。
2.問題介紹
在開始編寫程式碼之前,讓我們先定義一下我們想要解決的問題。
2.1. 定義問題
假設我們有一個包含各種程式語言內容的文字檔案。它看起來像這樣:
Both JAVA and KOTLIN applications can run on the JVM.
But python is a simpler language
PYTHON application is also platform independent.
java and kotlin are statically typed languages.
On the other hand, python is a dynamically typed language.
我們希望透過執行幾個文字轉換來清理和規範化這些內容:
- 刪除第二行。
- 規範化大小寫——將所有出現的“java”、“kotlin”和“python”轉換為正確的大寫形式“
Java
”、“Kotlin
”和“Python
” - 擴充縮寫-在出現「
JVM”
地方,在其後加入其完整形式「(Java Virtual Machine)”
。 - 將更改寫回原始文件。
最後,我們修改後的文件應如下所示:
Both Java and Kotlin applications can run on the JVM (Java Virtual Machine).
Python application is also platform independent.
Java and Kotlin are statically typed languages.
On the other hand, Python is a dynamically typed language.
像往常一樣,我們將使用單元測試方法來示範每種方法。接下來,讓我們設定單元測試類別。
2.2. 設定測試
我們先來看看單元測試設定:
public class ModifyFileByPatternUnitTest {
@TempDir
private File fileDir;
private File myFile;
private static final List<String> ORIGINAL_LINES = List.of(
"Both JAVA and KOTLIN applications can run on the JVM.",
"But python is a simpler language",
"PYTHON application is also platform independent.",
"java and kotlin are statically typed languages.",
"On the other hand, python is a dynamically typed language.");
private static final List<String> EXPECTED_LINES = List.of(
"Both Java and Kotlin applications can run on the JVM (Java Virtual Machine).",
"Python application is also platform independent.",
"Java and Kotlin are statically typed languages.",
"On the other hand, Python is a dynamically typed language.");
@BeforeEach
void initFile() throws IOException {
myFile = new File(fileDir, "myFile.txt");
Files.write(myFile.toPath(), ORIGINAL_LINES);
}
// ...
}
如我們所見,為了確保測試不會修改系統上的任何實際文件,我們將利用@TempDir
註釋,它允許我們建立一個僅在測試期間存在的臨時目錄。測試完成後,JUnit 會自動刪除其中的所有檔案。這為每次運行提供了一個乾淨、隔離的環境。
此外, initFile()
方法確保每個測試都以相同的初始檔案內容開始,從而確保我們的修改在受控環境中進行測試。
接下來我們來看看如何解決這個問題。
3. 將整個檔案載入記憶體中
我們的第一種方法是將整個文件載入到記憶體中,修改其內容,然後將其寫回。對於小型或中等大小的文件,這種技術簡單有效。
讓我們檢查一下這是如何做到的:
@Test
void whenLoadTheFileContentToMemModifyThenWriteBack_thenCorrect() throws IOException {
assertTrue(Files.exists(myFile.toPath()));
List<String> lines = Files.readAllLines(myFile.toPath());
lines.remove(1); // remove the 2nd line
List<String> newLines = lines.stream()
.map(line -> line.replaceAll("(?i)java", "Java")
.replaceAll("(?i)kotlin", "Kotlin")
.replaceAll("(?i)python", "Python")
.replaceAll("JVM", "$0 (Java Virtual Machine)"))
.toList();
Files.write(myFile.toPath(), newLines);
assertLinesMatch(EXPECTED_LINES, Files.readAllLines(myFile.toPath()));
}
在這個方法中,我們首先使用Files.readAllLines()
將檔案中的所有行讀入List
。然後,我們刪除第二行並執行模式替換。最後,我們使用Files.write()
將更新的行直接覆寫檔案。
值得一提的是,我們使用了 JUnit 5 的assertLinesMatch()
來驗證文件內容。
這種記憶體方法簡潔易懂。然而,對於需要關注記憶體佔用的大型檔案來說,這種方法並不理想。
那麼接下來,我們假設myFile.txt
是一個巨大的檔案並探索如何解決這個問題。
4. 使用BufferedReader
進行記憶體高效的文件修改
為了有效率地處理較大的文件,我們可以使用BufferedReader
和BufferedWriter.
此方法一次處理一行,而無需將整個文件載入到記憶體中。
接下來我們來看看實作:
@Test
void whenUsingBufferedReaderAndModifyViaTempFile_thenCorrect(@TempDir Path tempDir) throws IOException {
Pattern javaPat = Pattern.compile("(?i)java");
Pattern kotlinPat = Pattern.compile("(?i)kotlin");
Pattern pythonPat = Pattern.compile("(?i)python");
Pattern jvmPat = Pattern.compile("JVM");
Path modifiedFile = tempDir.resolve("modified.txt");
try (BufferedReader reader = Files.newBufferedReader(myFile.toPath());
BufferedWriter writer = Files.newBufferedWriter(modifiedFile)) {
int lineNumber = 0;
String line;
while ((line = reader.readLine()) != null) {
lineNumber++;
if (lineNumber == 2) {
continue; // skip the 2nd line
}
String replaced = line;
replaced = javaPat.matcher(replaced)
.replaceAll("Java");
replaced = kotlinPat.matcher(replaced)
.replaceAll("Kotlin");
replaced = pythonPat.matcher(replaced)
.replaceAll("Python");
replaced = jvmPat.matcher(replaced)
.replaceAll("JVM (Java Virtual Machine)");
writer.write(replaced);
writer.newLine();
}
}
Files.move(modifiedFile, myFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
assertTrue(myFile.exists());
assertLinesMatch(EXPECTED_LINES, Files.readAllLines(myFile.toPath()));
}
接下來我們來了解一下詳細情況。
- 預編譯正規表示式模式 – 我們不再直接在
Strings,
上呼叫replaceAll()
,而是使用Pattern.compile().
對Regex
模式進行一次編譯。當多次應用相同的模式時,這種方法效率更高。 - 使用
BufferedReader
和BufferedWriter
進行串流傳輸-我們逐行讀取文件,修改每一行,然後立即將其寫入臨時輸出檔(modified.txt
)。無論檔案大小如何,這都能保持較低的記憶體佔用率。 - Try-with-resources –
try (…) { … }
語法會在程式碼區塊執行完畢後自動關閉reader
和writer
。相較於在 finally 程式碼區塊中手動關閉串流,這是更安全、更簡潔的替代方案。 - 取代原始檔案-寫入完成後,我們使用
Files.move()
和REPLACE_EXISTING
選項將原始檔案替換為修改後的檔案。
此方法穩健、可擴展,非常適合需要對處理進行細粒度控制的大型檔案或串流場景。
5. 結論
在本文中,我們探討了兩種基於正規表示式模式在 Java 中修改文件內容的實用技術:
- 將整個文件加載到記憶體中——這是一個簡單而富有表現力的解決方案,非常適合較小的文件。
- 使用
BufferedReader
和BufferedWriter
進行串流傳輸-這是一種處理大檔案的有效且可擴展的方法。
這兩種方法都基於 Java 的標準庫,因此它們可以在任何 Java 環境中運行,而無需額外的依賴。透過結合使用這些方法,我們可以安全且有效率地處理各種文件轉換任務。
與往常一樣,範例的完整原始程式碼可在 GitHub 上找到。