001/* 002 * Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.agentsflex.core.file2text.util; 017 018 019import com.ibm.icu.text.CharsetDetector; 020import com.ibm.icu.text.CharsetMatch; 021 022import java.io.*; 023import java.nio.charset.Charset; 024import java.nio.charset.StandardCharsets; 025 026/** 027 * 工业级编码检测工具类(支持流式读取大文件,自动识别编码并返回 Reader) 028 * 029 * <p>特性: 030 * <ul> 031 * <li>支持 PushbackInputStream,避免依赖 mark/reset</li> 032 * <li>支持 UTF-8, UTF-16LE/BE, UTF-32LE/BE BOM 检测</li> 033 * <li>ICU4J 异常降级处理,默认回退 UTF-8</li> 034 * <li>支持自定义置信度阈值</li> 035 * <li>编码名称标准化</li> 036 * <li>返回的 Reader 关闭时会级联关闭底层 InputStream</li> 037 * </ul> 038 * 039 * <p>使用示例: 040 * <pre>{@code 041 * try (Reader reader = EncodingDetectUtil.getAutoDetectReader(inputStream)) { 042 * BufferedReader br = new BufferedReader(reader); 043 * String line; 044 * while ((line = br.readLine()) != null) { 045 * System.out.println(line); 046 * } 047 * } 048 * }</pre> 049 * 050 * <p>调用方必须关闭返回的 Reader,以释放底层 InputStream。 051 */ 052public class EncodingDetectUtil { 053 054 private static final int DEFAULT_DETECT_BUFFER_SIZE = 1024 * 1024; // 1MB 055 private static final int DEFAULT_CONFIDENCE_THRESHOLD = 60; 056 057 private EncodingDetectUtil() { 058 // 工具类禁止实例化 059 } 060 061 // ========================== 062 // 对外 API 063 // ========================== 064 065 066 /** 067 * 自动读取 InputStream 为文本 068 * 069 * @param inputStream 待读取流 070 * @return 完整文本 071 * @throws IOException IO 异常 072 */ 073 public static String readToString(InputStream inputStream) throws IOException { 074 StringBuilder sb = new StringBuilder(); 075 try (BufferedReader br = new BufferedReader(getAutoDetectReader(inputStream))) { 076 String line; 077 while ((line = br.readLine()) != null) { 078 sb.append(line).append(System.lineSeparator()); 079 } 080 } 081 return sb.toString(); 082 } 083 084 /** 085 * 自动识别 InputStream 编码并返回 Reader(默认置信度阈值 60) 086 * 087 * @param inputStream 待检测流 088 * @return 自动识别编码的 Reader 089 * @throws IOException IO 异常 090 */ 091 public static Reader getAutoDetectReader(InputStream inputStream) throws IOException { 092 return getAutoDetectReader(inputStream, DEFAULT_CONFIDENCE_THRESHOLD); 093 } 094 095 /** 096 * 自动识别 InputStream 编码并返回 Reader,可自定义置信度阈值 097 * 098 * @param inputStream 待检测流 099 * @param confidenceThreshold ICU4J 检测置信度阈值(0-100) 100 * @return 自动识别编码的 Reader 101 * @throws IOException IO 异常 102 */ 103 public static Reader getAutoDetectReader(InputStream inputStream, int confidenceThreshold) throws IOException { 104 if (inputStream == null) { 105 throw new IllegalArgumentException("inputStream cannot be null"); 106 } 107 if (confidenceThreshold < 0 || confidenceThreshold > 100) { 108 confidenceThreshold = DEFAULT_CONFIDENCE_THRESHOLD; 109 } 110 111 PushbackInputStream pbis = new PushbackInputStream(inputStream, DEFAULT_DETECT_BUFFER_SIZE); 112 byte[] detectBytes = new byte[DEFAULT_DETECT_BUFFER_SIZE]; 113 int read = pbis.read(detectBytes); 114 115 String charsetName; 116 if (read <= 0) { 117 charsetName = StandardCharsets.UTF_8.name(); 118 } else { 119 // 1. 检测 BOM 120 String bomCharset = detectBom(detectBytes, read); 121 if (bomCharset != null) { 122 charsetName = bomCharset; 123 } else { 124 // 2. ICU4J 自动检测 125 charsetName = detectCharsetICU(detectBytes, read, confidenceThreshold); 126 } 127 } 128 129 // 推回已读取字节,保证流式读取完整数据 130 if (read > 0) { 131 pbis.unread(detectBytes, 0, read); 132 } 133 134 return new InputStreamReader(pbis, Charset.forName(charsetName)); 135 } 136 137 /** 138 * 自动检测编码(只返回编码名称) 139 * 140 * @param inputStream 待检测流 141 * @return 编码名称(标准 JVM 名称) 142 * @throws IOException IO 异常 143 */ 144 public static String detectCharset(InputStream inputStream) throws IOException { 145 try (Reader r = getAutoDetectReader(inputStream)) { 146 return ((InputStreamReader) r).getEncoding(); 147 } 148 } 149 150 // ========================== 151 // 内部方法 152 // ========================== 153 154 /** 155 * BOM 检测(含 UTF-8/16/32) 156 */ 157 private static String detectBom(byte[] bytes, int length) { 158 if (length >= 3 && (bytes[0] & 0xFF) == 0xEF 159 && (bytes[1] & 0xFF) == 0xBB 160 && (bytes[2] & 0xFF) == 0xBF) { 161 return StandardCharsets.UTF_8.name(); 162 } 163 if (length >= 2 && (bytes[0] & 0xFF) == 0xFF && (bytes[1] & 0xFF) == 0xFE) { 164 return "UTF-16LE"; 165 } 166 if (length >= 2 && (bytes[0] & 0xFF) == 0xFE && (bytes[1] & 0xFF) == 0xFF) { 167 return "UTF-16BE"; 168 } 169 if (length >= 4 && (bytes[0] & 0xFF) == 0xFF 170 && (bytes[1] & 0xFF) == 0xFE 171 && (bytes[2] & 0xFF) == 0x00 172 && (bytes[3] & 0xFF) == 0x00) { 173 return "UTF-32LE"; 174 } 175 if (length >= 4 && (bytes[0] & 0xFF) == 0x00 176 && (bytes[1] & 0xFF) == 0x00 177 && (bytes[2] & 0xFF) == 0xFE 178 && (bytes[3] & 0xFF) == 0xFF) { 179 return "UTF-32BE"; 180 } 181 return null; 182 } 183 184 /** 185 * 使用 ICU4J 检测编码,支持置信度阈值 186 */ 187 private static String detectCharsetICU(byte[] bytes, int length, int confidenceThreshold) { 188 try { 189 CharsetDetector detector = new CharsetDetector(); 190 detector.enableInputFilter(true); // 过滤 HTML/XML 标签提高准确性 191 192 if (length < bytes.length) { 193 // 只取前 length 字节 194 byte[] sample = new byte[length]; 195 System.arraycopy(bytes, 0, sample, 0, length); 196 detector.setText(sample); 197 } else { 198 detector.setText(bytes); 199 } 200 201 CharsetMatch match = detector.detect(); 202 203 if (match != null && match.getConfidence() >= confidenceThreshold) { 204 String name = match.getName(); 205 // GBK 升级为 GB18030 206 if ("GBK".equalsIgnoreCase(name)) { 207 return "GB18030"; 208 } 209 return Charset.forName(name).name(); // 标准化 210 } 211 } catch (Exception e) { 212 // ICU4J 异常降级到 UTF-8 213 } 214 return StandardCharsets.UTF_8.name(); 215 } 216}