TextSegmentationUtil.java

package com.yumu.noveltranslator.util;

import java.util.ArrayList;
import java.util.List;

/**
 * 文本分段工具类
 * 将长文本拆分为符合字符限制的片段,支持按句子、段落边界分割
 */
public class TextSegmentationUtil {

    /** 每段最大字符数(统一限制,不再按引擎区分) */
    private static final int DEFAULT_MAX_CHARS_PER_SEGMENT = 2000;

    // 句子结束标点符号(中英文)
    private static final String SENTENCE_ENDINGS = ".!?。!?…\n\r";

    /**
     * 根据指定的翻译引擎分割文本
     *
     * @param text 待分割的文本
     * @param engineName 翻译引擎名称(如 "google", "deepl", "mymemory", "baidu", "libre", "youdao")
     * @return 分割后的文本片段列表
     */
    public static List<String> segmentByTextEngine(String text, String engineName) {
        if (text == null || text.isEmpty()) {
            return new ArrayList<>();
        }

        return segmentText(text, DEFAULT_MAX_CHARS_PER_SEGMENT);
    }

    /**
     * 按指定字符数限制分割文本
     * 优先在句子边界处分割,其次在段落边界处分割,最后按字符数硬分割
     *
     * @param text 待分割的文本
     * @param maxChars 每段最大字符数
     * @return 分割后的文本片段列表
     */
    public static List<String> segmentText(String text, int maxChars) {
        if (text == null || text.isEmpty() || maxChars <= 0) {
            return new ArrayList<>();
        }

        List<String> segments = new ArrayList<>();

        // 如果文本长度不超过限制,直接返回
        if (text.length() <= maxChars) {
            segments.add(text);
            return segments;
        }

        int startPos = 0;
        while (startPos < text.length()) {
            int endPos = Math.min(startPos + maxChars, text.length());

            // 如果到达文本末尾,添加最后一段
            if (startPos >= text.length()) {
                break;
            }

            // 如果当前段已经很短(接近或等于最大限制),直接截取
            if (endPos - startPos < maxChars) {
                segments.add(text.substring(startPos));
                break;
            }

            // 尝试在句子边界处分割
            int sentenceBreak = findSentenceBreak(text, startPos, endPos);
            if (sentenceBreak > startPos) {
                segments.add(text.substring(startPos, sentenceBreak));
                startPos = sentenceBreak;
                continue;
            }

            // 尝试在段落边界处分割
            int paragraphBreak = findParagraphBreak(text, startPos, endPos);
            if (paragraphBreak > startPos) {
                segments.add(text.substring(startPos, paragraphBreak));
                startPos = paragraphBreak;
                continue;
            }

            // 按单词边界分割,避免在单词中间切断
            int wordBreak = findWordBreak(text, startPos, endPos);
            if (wordBreak > startPos) {
                segments.add(text.substring(startPos, wordBreak));
                startPos = wordBreak;
                continue;
            }

            // 如果以上都找不到合适的分割点,则强制按字符数分割
            segments.add(text.substring(startPos, endPos));
            startPos = endPos;
        }

        return segments;
    }

    /**
     * 查找句子边界分割点
     * 优化:避免重复创建 Pattern 和 Matcher,直接遍历字符查找
     */
    private static int findSentenceBreak(String text, int startPos, int endPos) {
        // 从后往前找句子结束符,优先在最靠近 endPos 的位置分割
        for (int i = endPos - 1; i >= startPos; i--) {
            char c = text.charAt(i);
            if (isSentenceEnding(c)) {
                // 返回句子结束符之后的位置,保留换行符在当前段末尾
                return i + 1;
            }
        }
        return -1; // 未找到合适的分割点
    }

    /**
     * 判断字符是否为句子结束符
     */
    private static boolean isSentenceEnding(char c) {
        return SENTENCE_ENDINGS.indexOf(c) >= 0;
    }

    /**
     * 查找段落边界分割点
     */
    private static int findParagraphBreak(String text, int startPos, int endPos) {
        // 查找换行符作为段落边界
        for (int i = endPos - 1; i >= startPos; i--) {
            if (text.charAt(i) == '\n') {
                // 返回换行符之后的位置,保留换行在当前段末尾
                return i + 1;
            }
        }
        return -1; // 未找到合适的分割点
    }

    /**
     * 查找单词边界分割点
     */
    private static int findWordBreak(String text, int startPos, int endPos) {
        // 从后往前找空格,避免在单词中间分割
        for (int i = endPos - 1; i >= startPos; i--) {
            if (Character.isWhitespace(text.charAt(i))) {
                return i + 1; // 返回空格后位置,这样不会把单词分开
            }
        }
        return -1; // 未找到合适的分割点
    }

    /**
     * 获取指定引擎的最大字符数
     *
     * @param engineName 翻译引擎名称
     * @return 最大字符数限制
     */
    public static int getMaxCharsForEngine(String engineName) {
        return DEFAULT_MAX_CHARS_PER_SEGMENT;
    }

    /**
     * 计算文本的分段数量(预估)
     *
     * @param text 文本内容
     * @param engineName 翻译引擎名称
     * @return 预计分段数量
     */
    public static int estimateSegmentsCount(String text, String engineName) {
        if (text == null || text.isEmpty()) {
            return 0;
        }

        int maxChars = getMaxCharsForEngine(engineName);
        if (maxChars <= 0) {
            return 1; // 如果没有限制,当作一段
        }

        int baseCount = (int) Math.ceil((double) text.length() / maxChars);

        // 考虑智能分割可能减少段落数量
        List<String> segments = segmentByTextEngine(text, engineName);
        return Math.min(baseCount, segments.size());
    }
}