中文轉拼音是搜索建議、姓名排序、輸入法、數據清洗等場景的基礎需求。本文提供基於區位碼映射的零依賴實現,深度解析多音字處理策略,並給出生產環境性能優化方案。
漢字在 GB2312 編碼中佔兩字節,高字節 0xA1-0xF7 (區號 1-87),低字節 0xA1-0xFE (位號 1-94)。一級漢字 (3755 字) 按拼音排序,區位碼與拼音呈單調關係,可通過區位碼區間二分查找確定拼音。
import java.util.*;
public final class PinyinUtils {
// 拼音區間表:拼音 -> 起始區位碼 (負數表示)
private static final Map PINYIN_RANGES = new LinkedHashMap<>();
// 多音字/生僻字覆蓋表:字符 -> 拼音
private static final Map SPECIAL_PINYIN = new HashMap<>();
static {
// 初始化拼音區間 (節選,完整表含 400+ 條)
PINYIN_RANGES.put("a", -20319);
PINYIN_RANGES.put("ai", -20317);
PINYIN_RANGES.put("an", -20304);
PINYIN_RANGES.put("ang", -20295);
PINYIN_RANGES.put("ao", -20292);
PINYIN_RANGES.put("ba", -20283);
// ... 完整初始化代碼見附錄
// 多音字/生僻字手工維護
SPECIAL_PINYIN.put('重', "chong"); // 非 zhong
SPECIAL_PINYIN.put('長', "chang");
SPECIAL_PINYIN.put('發', "fa");
SPECIAL_PINYIN.put('行', "xing");
// ... 常見多音字
}
/**
* 字符串轉拼音
* @param onlyFirstLetter true=首字母, false=全拼
*/
public static String toPinyin(String input, boolean onlyFirstLetter) {
if (input == null || input.isEmpty()) return "";
StringBuilder sb = new StringBuilder(input.length() * (onlyFirstLetter ? 1 : 3));
for (char c : input.toCharArray()) {
// 1. ASCII 直接保留
if (c < 128) {
sb.append(c);
continue;
}
// 2. 查多音字表
String pinyin = SPECIAL_PINYIN.get(c);
if (pinyin != null) {
sb.append(onlyFirstLetter ? pinyin.charAt(0) : pinyin);
continue;
}
// 3. 計算區位碼
int gbCode = getGbCode(c);
if (gbCode == 0) { // 非漢字符號
sb.append(c);
continue;
}
// 4. 二分查找拼音區間
pinyin = findPinyinByGbCode(gbCode);
if (pinyin == null) {
sb.append(c); // 未知字符保留原字
} else {
sb.append(onlyFirstLetter ? pinyin.charAt(0) : pinyin);
}
}
return sb.toString();
}
private static int getGbCode(char c) {
try {
byte[] bytes = String.valueOf(c).getBytes("GBK");
if (bytes.length != 2) return 0;
int high = bytes[0] & 0xFF;
int low = bytes[1] & 0xFF;
if (high < 0xA1 || high > 0xF7 || low < 0xA1 || low > 0xFE) return 0;
return (high - 0xA0) * 100 + (low - 0xA0); // 區位碼 1101-8794
} catch (Exception e) {
return 0;
}
}
private static String findPinyinByGbCode(int gbCode) {
// 將區位碼轉為負數索引值 (與初始化表一致)
int index = -(gbCode - 1000); // 近似轉換,實際需精確映射
String prevPinyin = null;
int prevIndex = Integer.MIN_VALUE;
for (Map.Entry entry : PINYIN_RANGES.entrySet()) {
int currIndex = entry.getValue();
if (index >= prevIndex && index < currIndex) {
return prevPinyin;
}
prevPinyin = entry.getKey();
prevIndex = currIndex;
}
return prevPinyin; // 最後一組
}
}
| 策略 | 實現方式 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| 首拼優先 | 區位碼映射取首個拼音 | 零依賴、極快 | 錯誤率高 (約 5-10%) | 搜索建議、首字母索引 |
| 詞典標註 | 維護多音字 Map 覆蓋 | 高頻詞準確 | 覆蓋面有限、維護成本 | 姓名、地址、通用詞彙 |
| 上下文消歧 | 結合分詞 + 語言模型 | 準確率最高 (95%+) | 依賴重、延遲高 | 語音合成、精準排序 |
| 返回所有拼音 | 輸出 "chong/zhong" | 無遺漏 | 下游需二次處理 | 輸入法、拼音檢索 |
int[6768],O(1) 查找String.valueOf()、getBytes() 分配,直接操作 char[]// 極致優化版:預計算數組查找
public final class FastPinyin {
// 索引 = 區位碼 - 1601 (1601=0xA1A1)
// 值 = 拼音在 PINYIN_ARRAY 中的索引
private static final short[] GB_TO_PINYIN_INDEX = new short[6768];
private static final String[] PINYIN_ARRAY = {"a","ai","an",...};
static {
// 離線生成填充 GB_TO_PINYIN_INDEX
}
public static String toPinyinFast(char[] chars, boolean firstOnly) {
StringBuilder sb = new StringBuilder(chars.length * (firstOnly ? 1 : 3));
for (char c : chars) {
if (c < 128) { sb.append(c); continue; }
int gb = getGbCode(c); // 同前
if (gb == 0) { sb.append(c); continue; }
int idx = GB_TO_PINYIN_INDEX[gb - 1601];
String py = PINYIN_ARRAY[idx];
sb.append(firstOnly ? py.charAt(0) : py);
}
return sb.toString();
}
}
| 庫 | 特點 | Maven 坐標 | 星標 |
|---|---|---|---|
| pinyin4j | 經典、支援多音字輸出、格式豐富 | com.belerweb:pinyin4j:2.5.0 | 1.2k |
| HanLP | NLP 級、上下文消歧、分詞整合 | com.hankcs:hanlp:2.1.0 | 25k |
| PinyinHelper (Hutool) | 輕量、工具類風格、無依賴 | cn.hutool:hutool-all:5.8.x | 18k |
| ICU4J | Unicode 標準、Transliterator、企業級 | com.ibm.icu:icu4j:73.2 | 企業標準 |
import cn.hutool.core.lang.PinyinUtil;
String text = "中國人民銀行";
System.out.println(PinyinUtil.getPinyin(text)); // zhong guo ren min yin hang
System.out.println(PinyinUtil.getFirstLetter(text, "")); // zg rmyh
System.out.println(PinyinUtil.getPinyin("重慶", " ")); // chong qing (多音字取首拼)