最近有一個保存郵件內容到數據庫的需求,由於數據庫中對應字段是有長度限制的,我就到百度上尋找對長度比較大的字符串做分段保存的實現,但搜索引擎上往往只出現一些使用String.split的方法,而這種分割,是根據字符串內的字符分割的,如果是UTF-8編碼,一個中文是4字節,String.split后實際上長度並不是字節長度。
想必有人會認為:數據庫中直接保存字節數組不就好了嗎?
答案是肯定的,但考慮到查看的時候並不方便,而且郵件內容為html格式,有時候出現線上問題想手工修改的時候往往就麻煩,而且以前也遇到過想把字符串按字節截取出來的問題,所以就自己動手寫了一下,在這里也希望能分享給大家,如果有誤請指正。
首先第一個問題,按字節截取字符串分割子串還不簡單嗎?直接str.getBytes()后對字節數組下標分段為子字節數組就好啦,我相信大家都會這么覺得。
但是按字節截取字符串實際上是存在一個問題,就是如果截取的位置正好是該字符編碼的中間位置就會導致這個字符成為亂碼。以下代碼就是闡述該問題,字符串是17個中文‘一’:
public class Test { public static void main(String[] args) throws UnsupportedEncodingException { int len = 5; String str = "一一一一一一一一一一一一一一一一一"; byte[] bt = str.getBytes(StandardCharsets.UTF_8); byte[] br = new byte[len]; System.arraycopy(bt, 0, br, 0, len); String res = new String(br, StandardCharsets.UTF_8); System.out.println(res); }
輸出的結果為:一�。因為一個中文4字節,而我們要該字符串的5個字節,當然就會把第二個‘一’的一部分字節切割得到亂碼了。
這時候大家也會想說:沒關系吧,截取之后,就算亂碼拼起來不就沒事了。我也是天真這么以為的,所以我byte數組轉字符串后,進行拼接,發現,更亂了,代碼如下:
public static void main(String[] args) throws UnsupportedEncodingException { int len = 5; String str = "一一一一一一一一一一一一一一一一一"; byte[] bt = str.getBytes(StandardCharsets.UTF_8); byte[] br = new byte[len]; System.arraycopy(bt, 0, br, 0, len); String res = new String(br, StandardCharsets.UTF_8); System.arraycopy(bt, 5, br, 0, len); String resnext = new String(br, StandardCharsets.UTF_8); System.out.println(res + resnext); }
輸出結果:一��一�
以前遇到這個按字節截位的問題的時候,我查了很多的文章,大部分方法是使用Charset先設置好編碼集,再用CharBuffer封裝原字符串后做截取,由於一直都沒怎么使用過這兩個類,所以當時也沒有看懂,也就放棄使用這個了。
后面看到了一些大佬的實現,他們解決辦法非常巧妙,如下:
1.先按字節數組進行截取,獲得一個長度為固定截取長度的子字節數組,
2.把字節數組轉字符串得到一個新String子串,
3.再次把新String子串轉byte數組,兩數組長度進行比較(因為新String子串再轉byte數組時,
會對截取了一半的字符進行補全為對應編碼集一個字符的長度),
4.如果 新String子串的字節數組 比 步驟1中按長度截取的子串字節數組 長,
說明存在截取一半的字符,這個字符會在最后一個位置,要舍棄,
所以把新String子串按字符串長度截取減少1位,得到的字符串就是沒有截取一半的字符,
且長度小於等於需要的字節長度的子串。
上面只是從字符串中截取一個不超過固定字節長度的子串,那么我們如何把一個長字符串分割為不超過固定字節長度的子串字符數組列表呢?
具體代碼如下:
/** * 方法:字符串按字節固定長度分割數組 * startPos 子串在原字符串字節數組的開始截取下標 * startStrPos 子串在原字符串開始截取的下標 * strLen 原字符串字節數組長度 * 背景:由於編碼格式不同,直接截取可能會拿到一個被砍一半的亂碼,如utf-8 4byte 一個中文,如果截取的時候是5byte,就會出現亂碼 * 原理:1、先按字節數組進行截取,獲得一個長度不大於固定截取長度的字節數組 * 2、把字節數組轉字符串得到一個新子串,再轉byte數組后,兩數組長度進行比較(新子串再轉byte數組時,會對截取了一半的字符進行補全為對應編碼集一個字符的長度), * 如果新子串的字節數組比按長度截取的子串字節數組長,說明存在截取一半的字符,這個字符會在最后一個位置,要舍棄 * 所以,新子串按字符串長度截取減少1位,得到的字符串就是沒有截取一半的字符,且長度小於等於需要的字節長度的子串。 * * 1.當 子串字節數組開始截取下標 小於 原字符串字節數組長度 一直循環 * 2.子串字節數組大小 需要根據 當前父串字節數組的截取下標和長度差值 與 預想截取的字節長度 比較來創建(否則用System.arraycopy會報錯) * 3.根據 子串在原字符串字節數組的開始截取下標 拷貝父字節數組的內容到子字節數組 * 4.根據 子串在原字符串開始截取的下標 與 子字節數組轉為字符串的長度 在父字符串截取一個偽子串(可能最后一個字符被截取一半是亂碼) * 5.比較偽子串轉字節數組后長度 與 預想截取的字節數組長度,大於,則偽子串截取字符串長度-1 * 6.子串字節數組開始截取下標 + 得到的子串字節長度;子串在原字符串開始截取的下標 + 得到子串的字符長度 * @param str 原字符串 * @param len 分割字串字節長度 * @param charSet 編碼字符集 * @return List<String> 分割后的子串 * @throws UnsupportedEncodingException */ public static final List<String> divideStrByBytes(String str,int len, String charSet) throws UnsupportedEncodingException{ List<String> strSection = new ArrayList<>(); byte[] bt = str.getBytes(charSet); int strLen = bt.length; int startPos = 0; int startStrPos = 0; while (startPos < strLen) { Integer subSectionLen = len; if (strLen - startPos < len) { subSectionLen = strLen - startPos; } byte[] br = new byte[subSectionLen]; System.arraycopy(bt, startPos, br, 0, subSectionLen); String res = new String(br, charSet); int resLen = res.length(); if (str.substring(startStrPos, startStrPos + resLen).getBytes(charSet).length > len) { res = res.substring(0, resLen - 1); } startStrPos += res.length(); strSection.add(res); startPos += res.getBytes(charSet).length; } return strSection; }
下面我們試試效果:
public static void main(String[] args) throws UnsupportedEncodingException { int len = 5; String str = "一一一一一一一一一一一一一一一一一"; List<String> stringList = divideStrByBytes(str,len,"UTF-8"); stringList.forEach(item -> { System.out.println(item); }); } }
輸出結果:17個中文‘一’