在文本處理、編碼檢測、病毒掃描、版本控制等場景中,需要快速區分文本文件與二進制文件。核心判斷邏輯:文本文件僅包含可打印字符及常見控制字符(換行、制表、回車),而二進制文件包含不可打印的控制字符。
public static boolean isBinary(File file) throws IOException {
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[8192]; // 8KB 緩衝區
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
for (int i = 0; i < bytesRead; i++) {
byte b = buffer[i];
// 允許:可打印 ASCII (32-126)、換行(10)、回車(13)、制表(9)、換頁(12)
if (b < 32 && b != 9 && b != 10 && b != 13 && b != 12) {
return true; // 發現不可打印控制字符,判定為二進制
}
// 可選:檢測高位字節 (非 UTF-8/ASCII 文本常見特徵)
// if (b < 0) return true;
}
}
}
return false; // 全文掃描無異常字符,判定為文本
}
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public final class FileTypeDetector {
// 常見文本文件擴展名快速判斷 (可選優化)
private static final Set TEXT_EXTENSIONS = Set.of(
"txt", "java", "py", "js", "ts", "html", "css", "xml", "json", "yaml", "yml",
"md", "properties", "ini", "cfg", "conf", "log", "csv", "sql", "sh", "bat"
);
/**
* 綜合判斷:擴展名預檢 + 內容掃描 + 編碼嘗試
* @return true: 二進制文件, false: 文本文件
*/
public static boolean isBinary(File file) throws IOException {
// 1. 快速擴展名檢查 (可選)
if (hasTextExtension(file)) return false;
// 2. 空文件視為文本
if (file.length() == 0) return false;
// 3. 內容掃描 (讀取前 64KB 或整個文件)
final int MAX_SCAN_BYTES = 64 * 1024;
long bytesToScan = Math.min(file.length(), MAX_SCAN_BYTES);
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[8192];
long totalRead = 0;
int bytesRead;
while (totalRead < bytesToScan && (bytesRead = fis.read(buffer)) != -1) {
int scanLen = (int) Math.min(bytesRead, bytesToScan - totalRead);
for (int i = 0; i < scanLen; i++) {
byte b = buffer[i];
// 核心判斷邏輯
if (isBinaryByte(b)) {
return true;
}
}
totalRead += scanLen;
}
}
// 4. 進階:嘗試以 UTF-8 解碼驗證 (可選,針對 UTF-8 含 BOM 或高位字節情況)
// if (looksLikeUtf8(file)) return false;
return false;
}
/** 判斷單字節是否為二進制特徵字節 */
private static boolean isBinaryByte(byte b) {
// 不可打印控制字符 (排除常見空白字符)
if (b < 32 && b != 9 && b != 10 && b != 13 && b != 12) {
return true;
}
// DEL 字符 (127) 通常也不出現在正常文本中
if (b == 127) return true;
return false;
}
/** 快速擴展名檢查 */
private static boolean hasTextExtension(File file) {
String name = file.getName().toLowerCase();
int dot = name.lastIndexOf('.');
if (dot > 0) {
String ext = name.substring(dot + 1);
return TEXT_EXTENSIONS.contains(ext);
}
return false;
}
/** 嘗試 UTF-8 解碼驗證 (輔助判斷) */
private static boolean looksLikeUtf8(File file) {
try (FileInputStream fis = new FileInputStream(file)) {
byte[] sample = new byte[Math.min(4096, (int) file.length())];
fis.read(sample);
// 簡單驗證:嘗試解碼,不拋異常且無替換字符則可能是 UTF-8
String decoded = new String(sample, StandardCharsets.UTF_8);
return !decoded.contains("\uFFFD"); // 無替換字符
} catch (IOException e) {
return false;
}
}
}
FileChannel.map() 利用 OS 頁面緩存加速public static boolean isBinaryMmap(File file) throws IOException {
if (file.length() == 0) return false;
long size = Math.min(file.length(), 1_000_000); // 掃描前 1MB
try (FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.READ);
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size)) {
for (long i = 0; i < size; i++) {
byte b = buffer.get(i);
if (isBinaryByte(b)) return true;
}
}
return false;
}
public class FileTypeDetectorTest {
public static void main(String[] args) throws IOException {
// 創建測試文件
File textFile = File.createTempFile("test", ".txt");
File binaryFile = File.createTempFile("test", ".bin");
File utf8File = File.createTempFile("test", ".md");
Files.writeString(textFile.toPath(), "Hello World\n中文測試\tTab\r\nNormal text");
Files.write(utf8File.toPath(), "# 標題\n內容\n- 列表".getBytes(StandardCharsets.UTF_8));
// 生成二進制數據 (包含 0x00, 0xFF, 0x7F 等)
byte[] binaryData = new byte[100];
new Random().nextBytes(binaryData);
binaryData[10] = 0; // NULL
binaryData[20] = 127; // DEL
Files.write(binaryFile.toPath(), binaryData);
System.out.println("文本文件: " + FileTypeDetector.isBinary(textFile)); // false
System.out.println("Markdown: " + FileTypeDetector.isBinary(utf8File)); // false
System.out.println("二進制文件: " + FileTypeDetector.isBinary(binaryFile)); // true
// 清理
textFile.delete();
binaryFile.delete();
utf8File.delete();
}
}
| 情況 | 判斷結果 | 說明 |
|---|---|---|
| UTF-8 中文文本 | 文本 | 高位字節 (>=128) 屬於多字節編碼一部分,非控制字符 |
| 含 BOM 的 UTF-8 | 文本 | BOM (EF BB BF) 為高位字節,需編碼驗證輔助判斷 |
| 純 ASCII 控制字符文件 | 二進制 | 如僅含 ESC(27)、BEL(7) 等設備控制字符 |
| 文本文件含偶爾異常字節 | 二進制 | 嚴格模式下單個異常字節即判定為二進制 |
| 加密/壓縮文件 | 二進制 | 熵極高,大量不可打印字符 |
判斷文件類型看似簡單,實則涉及編碼知識、性能權衡、業務容錯。上述實現作為輕量級方案足以應對 90% 場景;對於安全審計、數據遺失防護 (DLP) 等強合規場景,建議引入 Apache Tika 或專業內容檢測引擎。