之前用过jacob 合并.doc,但是是有jacob有弊端:

  • 服务器必须是Windows操作系统 —— 目前之所以web项目多用Java开发,就是因为服务器可以是Linux、Unix等非Windows的系统来降低项目的成本。
  • 服务器上必须安装Office —— Jacob的意思就是: Java COM Bridge,java中调用office提供的com接口来实现对Office文件的操作。
  • 并发问题 —— 如果多用户同时在线生成word文件就必须处理此并发问题,稍有不慎,就会在服务器端产生Office的死进程,死锁服务器的内存资源。
 
我遇到的问题是下载并合并附件,这里的附件大多是doc文件,也包含少量的docx文件,但是文件路径是从数据库中读取出来的,均不带后缀名,传统的xwpfdocument和hwpfdocument不能完全解决我的问题;尤其是将文件合并,不能轻易办到,需要对文档进行解析。
这里我采用的方法是将Word文件转换为HTML文件,把Word的合并转化为HTML的合并;这样一来就减少了难度,不过就是还需要再把HTML文件转化为doc文件或者docx文件(此时,你就可以指定是哪种文件了)。
在转换的时候,分两个方向,一个是doc文件转换,另一个是docx文件转换;这里的转换时必须包含文档中的格式的(图片和表格我这里没有进行测试)。如果是没有文件后缀,那么就需要先判断是doc文件还是docx文件,这里用到了一个工具类,就是通过文件的文件头来判断文件类型,因为我这里只是为了区别doc和docx,所以就比较了前四位的16进制数,按照if和else来走两条转换路线。(具体文件的文件头可上网查资料,各种文件都有)。
转换doc文件的时候,是按照字符读取的,判断每个字符的字体颜色和样式,将其转换为HTML的代码,最后应该是整个稳当的HTML的字符串的累加,因为我这里是合并,所以我使用for循环进行了文件主体的叠加,最后在循环的外面加上HTML的头部和尾部信息即可。除此之外,根据需求,需要在不同文档之间插入分页符,分页符用HTML代码可以表示,"<br clear=all style='page-break-before:always' mce_style='page-break-before:always'> ";需要的直接添加即可。
然后是docx文件的转换,这里的就不像doc文件可以按照每个字符来读了,而是将整个文档直接转换为HTML文件,通过打印字符串可以得知,转换出来的HTML代码就是一个大的div,如果直接使用这个代码,合并的时候格式就不统一了,所以需要将图中的style样式去掉,直接用字符串截取,并加上一个空的<div>即可。

最后,再将HTML文件转为doc或者docx,可以将路径放在服务器上面,并且实现下载就行(建议每次下载完之后将该文件清空,可以循环利用)。详细代码如下。

 
 
package com.landray.kmss.km.doc.util;
 
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
 
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.model.PicturesTable;
import org.apache.poi.hwpf.usermodel.CharacterRun;
import org.apache.poi.hwpf.usermodel.Paragraph;
import org.apache.poi.hwpf.usermodel.Picture;
import org.apache.poi.hwpf.usermodel.Range;
import org.apache.poi.hwpf.usermodel.Table;
import org.apache.poi.hwpf.usermodel.TableCell;
import org.apache.poi.hwpf.usermodel.TableIterator;
import org.apache.poi.hwpf.usermodel.TableRow;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
 
public class WordExcelToHtml {
 
    /**
     * 回车符ASCII码
     */
    private static final short ENTER_ASCII = 13;
 
    /**
     * 空格符ASCII码
     */
    private static final short SPACE_ASCII = 32;
 
    /**
     * 水平制表符ASCII码
     */
    private static final short TABULATION_ASCII = 9;
 
    // public static String htmlText = "";
    public static String mainText = "";
    public static String htmlTextTbl = "";
    public static int counter = 0;
    public static int beginPosi = 0;
    public static int endPosi = 0;
    public static int beginArray[];
    public static int endArray[];
    public static String htmlTextArray[];
    public static boolean tblExist = false;
 
    public static void main(String argv[]) {
        try {
            String htmlText = "<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8' />"
                    + "</head><body>"; //将每一个Word中的主体部分拿出来,合并之后加上HTML的头和尾,但是要注意编码
            List<String> list = new ArrayList<String>();
            String file1 = "D://file8";
            String file2 = "D://file9";
            String file3 = "D://file11";
            list.add(file1);
            list.add(file2);
            list.add(file3);
 
            // String mainText1 = "";
            for (int i = 0; i < list.size(); i++) {
                htmlText += getWordAndStyle(list.get(i))
                        + "<br clear=all style='page-break-before:always' mce_style='page-break-before:always'> ";

//每一个文档读取完之后,加上一个分页符,继续累加
            }
            htmlText += "</body></html>";
            String filePath = "D://1.html";
            writeFile(htmlText, filePath);
            new HtmlToDoc().writeWordFile(filePath, "D://file10.doc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 读取每个文字样式
     *
     * @param fileName
     * @throws Exception
     */
 
    public static String getWordAndStyle(String fileName) throws Exception {
        String htmlText = "";
        FileInputStream in = new FileInputStream(new File(fileName));
        //根据文本内容判断是doc还是docx
        byte[] b = new byte[4];
        in.read(b, 0, b.length);
        in.close();
        FileInputStream in1 = new FileInputStream(new File(fileName));
        System.out.println(bytesToHexString(b) + ";;;");
        if (bytesToHexString(b).equalsIgnoreCase("d0cf11e0")) {//"d0cf11e0"代表的是doc文件
            HWPFDocument doc = new HWPFDocument(in1);
 
            Range rangetbl = doc.getRange();// 得到文档的读取范围
            TableIterator it = new TableIterator(rangetbl);
            int num =1;
 
            beginArray = new int[num];
            endArray = new int[num];
            htmlTextArray = new String[num];
 readTable(it, rangetbl);
            // 取得文档中字符的总数
            int length = doc.characterLength();
            // 创建图片容器;
            PicturesTable pTable = doc.getPicturesTable();
 
            int cur = 0;
            String tempString = "";
            for (int i = 0; i < length - 1; i++) {
                // 整篇文章的字符通过一个个字符的来判断,range为得到文档的范围
                Range range = new Range(i, i + 1, doc);
 
                CharacterRun cr = range.getCharacterRun(0);
 
                if (tblExist && cur < beginArray.length) {
                    if (i == beginArray[cur]) {
                        htmlText += tempString + htmlTextArray[cur];
                        tempString = "";
                        i = endArray[cur] - 1;
                        cur++;
                        continue;
                    }
                }
                if (pTable.hasPicture(cr)) {

//htmlText += tempString;
// 读写图片
tempString = readPicture(pTable, cr);
//tempString = "";
htmlText += tempString;
                } else {
 
                    Range range2 = new Range(i + 1, i + 2, doc);
                    // 第二个字符
                    CharacterRun cr2 = range2.getCharacterRun(0);
                    char c = cr.text().charAt(0);
 
                    // 判断是否为回车符
                    if (c == ENTER_ASCII) {
                        tempString += "<br/>";
 
                    }
                    // 判断是否为空格符
                    else if (c == SPACE_ASCII)
                        tempString += " ";
                    // 判断是否为水平制表符
                    else if (c == TABULATION_ASCII)
                        tempString += "    ";
                    // 比较前后2个字符是否具有相同的格式
                    boolean flag = compareCharStyle(cr, cr2);
                    String fontStyle = "<span class='text' style=\"font-family:"
                            + cr.getFontName()
                            + ";font-size:"
                            + cr.getFontSize()
                            / 2
                            + "pt;color:"
                            + ColorUtils.getHexColor(cr.getIco24()) + ";";
 
                    if (cr.isBold())
                        fontStyle += "font-weight:bold;";
                    if (cr.isItalic())
                        fontStyle += "font-style:italic;";
 
                    htmlText += fontStyle + "\" mce_style=\"font-family:"
                            + cr.getFontName() + ";font-size:"
                            + cr.getFontSize() / 2 + "pt;";
 
                    if (cr.isBold())
                        fontStyle += "font-weight:bold;";
                    if (cr.isItalic())
                        fontStyle += "font-style:italic;";
 
                    htmlText += fontStyle + "\">" + tempString + cr.text()
                            + "</span>";
                    tempString = "";
                }
            }
 
            htmlText += tempString;
            return htmlText;
        } else {
            Word2007ToHtml w = new Word2007ToHtml();
            String filepath = "";
            String fileName1 = fileName;
            String htmlName = "D://3.html";
            w.Word2007ToHtml(fileName1, htmlName);
            String result = w.readFileByBytes(htmlName);
            int i = result.indexOf('>');
            String realreasult = "<div>"+result.substring(i+1);
            System.out.println(realreasult);
            htmlText += realreasult;
            return htmlText;
        }
 
    }
 
    /**
     * 读写文档中的表格
     *
     * @param pTable
     * @param cr
     * @throws Exception
     */
    public static void readTable(TableIterator it, Range rangetbl)
            throws Exception {
 
        htmlTextTbl = "";
        // 迭代文档中的表格
 
        counter = -1;
        while (it.hasNext()) {
            tblExist = true;
            htmlTextTbl = "";
            Table tb = (Table) it.next();
            beginPosi = tb.getStartOffset();
            endPosi = tb.getEndOffset();
 
            System.out.println("............" + beginPosi + "...." + endPosi);
            counter = counter + 1;
            // 迭代行,默认从0开始
            beginArray[counter] = beginPosi;
            endArray[counter] = endPosi;
 
            htmlTextTbl += "<table border='1'>";
            for (int i = 0; i < tb.numRows(); i++) {
                TableRow tr = tb.getRow(i);
 
                htmlTextTbl += "<tr >";
                // 迭代列,默认从0开始
                for (int j = 0; j < tr.numCells(); j++) {
                    TableCell td = tr.getCell(j);// 取得单元格
                    int cellWidth = td.getWidth();
 
                    // 取得单元格的内容
                    for (int k = 0; k < td.numParagraphs(); k++) {
                        Paragraph para = td.getParagraph(k);
                        String s = para.text().toString().trim();
                        if (s == "") {
                            s = " ";
                        }
                        System.out.println(s);
                        htmlTextTbl += "<td class='text' width=" + cellWidth
                                + ">" + s + "</td>";
                        System.out.println(i + ":" + j + ":" + cellWidth + ":"
                                + s);
                    } // end for
                } // end for
            } // end for
            htmlTextTbl += "</table>";
            htmlTextArray[counter] = htmlTextTbl;
 
        } // end while
    }
 
    /**
     * 读写文档中的图片
     *
     * @param pTable
     * @param cr
     * @throws Exception
     */
    public static void readPicture(PicturesTable pTable, CharacterRun cr)
            throws Exception {
        // 提取图片
        Picture pic = pTable.extractPicture(cr, false);
        // 返回POI建议的图片文件名
        String afileName = pic.suggestFullFileName();
        OutputStream out = new FileOutputStream(new File("e://test"
                + File.separator + afileName));
        pic.writeImageContent(out);
        // htmlText += "<img src=\"e://test//" + afileName
        // + "\" mce_src=\"e://test//" + afileName + "\"/>";
    }
 
    public static boolean compareCharStyle(CharacterRun cr1, CharacterRun cr2) {
        boolean flag = false;
        if (cr1.isBold() == cr2.isBold() && cr1.isItalic() == cr2.isItalic()
                && cr1.getFontName().equals(cr2.getFontName())
                && cr1.getFontSize() == cr2.getFontSize()) {
            flag = true;
        }
        return flag;
    }
 
    /**
     * 写文件
     *
     * @param s
     */
    public static void writeFile(String s, String filePath) {
        FileOutputStream fos = null;
        BufferedWriter bw = null;
        try {
            File file = new File(filePath);
            fos = new FileOutputStream(file);
            bw = new BufferedWriter(new OutputStreamWriter(fos));
            bw.write(s);
        } catch (FileNotFoundException fnfe) {
            fnfe.printStackTrace();
        } catch (IOException ioe) {
            ioe.printStackTrace();
        } finally {
            try {
                if (bw != null)
                    bw.close();
                if (fos != null)
                    fos.close();
            } catch (IOException ie) {
            }
        }
    }
 
    // 判断文件类型
    public static String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder();
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }
}
 
 
获取字体颜色的工具类:
package com.landray.kmss.km.doc.util;
public class ColorUtils { 
 
    public static int  red(int c) { 
        return c & 0XFF; 
    } 
     
    public static int green(int c) { 
        return (c >> 8) & 0XFF; 
    } 
     
    public static int blue(int c) { 
        return (c >> 16) & 0XFF; 
    } 
     
    public static int rgb(int c) { 
        return (red(c) << 16) | (green(c) <<8) | blue(c); 
    } 
 
    public static String rgbToSix(String rgb) { 
        int length = 6 - rgb.length(); 
        String str = ""; 
        while(length > 0){ 
            str += "0"; 
            length--; 
        } 
        return str + rgb; 
    } 
     
    public static String getHexColor(int color) { 
        color = color == -1 ? 0 : color; 
        int rgb = rgb(color); 
        return "#" + rgbToSix(Integer.toHexString(rgb)); 
    } 
}
 
将HTML文件转换为doc文件:
 package com.landray.kmss.km.doc.util;
 
import java.io.BufferedReader; 
import java.io.ByteArrayInputStream;
import java.io.File; 
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; 
import java.io.InputStreamReader;
import java.nio.charset.Charset;   
import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.POIFSFileSystem; 
 
 
//将docx文件转为HTML
package com.landray.kmss.km.doc.util;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
 
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.converter.core.FileImageExtractor;
import org.apache.poi.xwpf.converter.core.FileURIResolver;
import org.apache.poi.xwpf.converter.xhtml.XHTMLConverter;
import org.apache.poi.xwpf.converter.xhtml.XHTMLOptions;
import org.junit.Test;
 
public  class Word2007ToHtml{
    @Test
       public void Word2007ToHtml(String fileName,String htmlName) throws IOException {
 
           final String file = fileName;
           File f = new File(file); 
           if (!f.exists()) { 
               System.out.println("Sorry File does not Exists!"); 
           } else { 
 
 
                   // ) 加载word文档生成 XWPFDocument对象 
                   InputStream in = new FileInputStream(f); 
                   XWPFDocument document = new XWPFDocument(in); 
 
                   // ) 解析 XHTML配置 (这里设置IURIResolver来设置图片存放的目录) 
                   File imageFolderFile = new File("D://"); 
                   XHTMLOptions options = XHTMLOptions.create().URIResolver(new FileURIResolver(imageFolderFile)); 
                   options.setExtractor(new FileImageExtractor(imageFolderFile)); 
                   options.setIgnoreStylesIfUnused(false); 
                   options.setFragment(true); 
 
                   // ) 将 XWPFDocument转换成XHTML 
                   File file1 = new File(htmlName);
                   OutputStream out = new FileOutputStream(file1); 
                   XHTMLConverter.getInstance().convert(document, out, options);
 
 
                   //也可以使用字符数组流获取解析的内容
   //                ByteArrayOutputStream baos = new ByteArrayOutputStream();
   //                XHTMLConverter.getInstance().convert(document, baos, options); 
   //                String content = baos.toString();
   //                System.out.println(content);
   //                 baos.close();
 
           } 
       } 
    public static void main(String[] args) throws IOException {
        Word2007ToHtml w = new Word2007ToHtml();
        String fileName = "D://file1.docx";
        String htmlName = "D://3.html";
        w.Word2007ToHtml(fileName,htmlName);
        String result = readFileByBytes(htmlName);
        System.out.println(result);
    }
     public static String readFileByBytes(String fileName) {
 
             String s="";
            File file = new File(fileName);
            Reader reader = null;
            try {
                //System.out.println("以字符为单位读取文件内容,一次读一个字节:");
                // 一次读一个字符
                reader = new InputStreamReader(new FileInputStream(file),"utf-8");
                int tempchar;
                while ((tempchar = reader.read()) != -1) {
                    // 对于windows下,\r\n这两个字符在一起时,表示一个换行。
                    // 但如果这两个字符分开显示时,会换两次行。
                    // 因此,屏蔽掉\r,或者屏蔽\n。否则,将会多出很多空行。
                    if (((char) tempchar) != '\r') {
                        s +=(char) tempchar;
                    }
                }
                reader.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return s;
}
}
/**    * 将html文档转为doc    * @author soildwang   *   */   
public class HtmlToDoc {
    /**
               * 读取html文件到word           *
               * @param filepath html文件的路径           * @return           
               * * @throws Exception           */         
    public boolean writeWordFile(String filepath,String outfile) throws Exception {               
        boolean flag = false;                 
        ByteArrayInputStream bais = null;
        FileOutputStream fos = null;                 
        //String outfile = "D://file8.doc";  //根据实际情况写路径                 
        try {                       
            if (!"".equals(outfile)) {                               
                File fileDir = new File(outfile);                               
                if (fileDir.exists()) {                                     
                    String content = readFile(filepath);                                     
                    byte b[] = content.getBytes();                                     
                    bais = new ByteArrayInputStream(b);                                     
                    POIFSFileSystem poifs = new POIFSFileSystem();                                     
                    DirectoryEntry directory = poifs.getRoot();                                     
                    DocumentEntry documentEntry =  directory.createDocument("WordDocument", bais);                                     
                    fos = new FileOutputStream(outfile);                                                                         
                    poifs.writeFilesystem(fos);                                     
                    bais.close();                                     
                    fos.close();                               
                    }                       
                }                   
            } catch (IOException e) {                       
                e.printStackTrace();                 
                } finally {
                    if(fos != null) fos.close();                       
                    if(bais != null) bais.close();                 
                    }                 return flag;         
                    }             
    /**           
     * * 读取html文件到字符串           * @param filename           
     * * @return           * @throws Exception           
     * */         
    public String readFile(String filename) throws Exception {                 
        StringBuffer buffer = new StringBuffer("");                 
        BufferedReader br = null;               
        try {                       
            br = new BufferedReader(new InputStreamReader(new  FileInputStream(new File(filename)),Charset.forName("utf-8")));                       
            buffer = new StringBuffer();                       
            while (br.ready())                               
                buffer.append((char) br.read());                 
            } catch (Exception e) {
                e.printStackTrace();                 
                } finally {                       
                    if(br!=null) br.close();                 
                    }                 
        return buffer.toString();         
        }  
//局部测试                
    public static void main(String[] args) throws Exception {                 
        new HtmlToDoc().writeWordFile("d://1.html","D://file8.doc");//根据实际情况写文件路径         
        } 
    }
 

基于java 合并.doc和docx格式的Word文件的更多相关文章

  1. Python:读取 .doc、.docx 两种 Word 文件简述及“Word 未能引发事件”错误

    概述 Python 中可以读取 word 文件的库有 python-docx 和 pywin32. 下表比较了各自的优缺点.   优点 缺点 python-docx 跨平台 只能处理 .docx 格式 ...

  2. 利用pdfbox和poi抽取pdf、doc以及docx格式的内容

    使用pdfbox1.5.0抽取pdf格式文档内容,使用poi3.7抽取doc及docx文档内容: /** * Created by yan.shi on 2017/9/25. */ import or ...

  3. 基于springboot的freemarker创建指定格式的word文档

    在web或其他应用中,经常我们需要导出或者预览word文档,比较实际的例子有招聘网站上预览或者导出个人简历,使用POI导出excel会非常的方便,但是如果想导出word,由于其格式控制非常复杂,故而使 ...

  4. 基于java处理.docx格式的word合并

    如下实例是将 2.docx和3.docx合并,写到empty.docx中,不适用于.doc格式,public static void main(String[] args) { File file1 ...

  5. 完美解决doc、docx格式word转换为Html

    http://blog.csdn.net/renzhehongyi/article/details/48767597

  6. java导出2007版word(docx格式)freemarker + xml 实现

    http://blog.csdn.net/yigehui12/article/details/52840121 Freemarker+xml生成docx 原理概述:word从2003版就支持xml格式 ...

  7. word文档转pdf,支持.doc和.docx,另附抽取pdf指定页数的方法

    公司有个需求,需要将word转成pdf并且抽取首页用以展示,word文档有需要兼容.doc和.docx两种文档格式.其中.docx通过poi直接就可以将word转成pdf,.doc则无法这样实现,上网 ...

  8. java生成带html样式的word文件

    参考:http://blog.csdn.net/xiexl/article/details/6652230 最近在项目中需要将通过富文本编辑器处理过的文字转换为Word,查了很久,大家通常的解决办法是 ...

  9. SpringBoot集成文件 - 如何基于POI-tl和word模板导出庞大的Word文件?

    前文我们介绍了通过Apache POI通过来导出word的例子:那如果是word模板方式,有没有开源库通过模板方式导出word呢?poi-tl是一个基于Apache POI的Word模板引擎,也是一个 ...

随机推荐

  1. Java Web大作业——编程导航系统

    title: Java Web大作业--编程导航系统 categories: - - 计算机科学 - Java abbrlink: 40bc48a1 date: 2021-12-29 00:37:35 ...

  2. Max-Mahalanobis Linear Discriminant Analysis Networks

    目录 概 主要内容 Pang T, Du C, Zhu J, et al. Max-Mahalanobis Linear Discriminant Analysis Networks[C]. inte ...

  3. Java初学者作业——编写Java程序,简单判断“王者荣耀”英雄收到攻击后是否死亡

    返回本章节 返回作业目录 需求说明: 判断"王者荣耀"中英雄受到攻击后是否死亡? 计算"王者荣耀"中怪物攻击英雄的伤害,做出英雄死亡的判断. 如果英雄受到过量伤 ...

  4. 使用 jQuery 选择器获取页面元素后,利用 jQuery 对象的 css() 方法设置其样式。

    查看本章节 查看作业目录 需求说明: 使用 jQuery 选择器获取页面元素后,利用 jQuery 对象的 css() 方法设置其样式. 要求如下: 点击页面的"更改样式"按钮后, ...

  5. 射频FEM介绍

    FEM介绍 1. 什么是FEM 1.1 FEM简介 FEM,Front-end Modules,即就是前端模块.硬件电路中的前端模块完成射频信号的发送放大以及接收放大(with bypass).滤波, ...

  6. C# double类型精度丢失问题

    我们先看一段代码,可以在控制台程序中执行看看结果 { double d = 500; double d1 = 233.84; double d2 = d - d1; //d2=266.15999999 ...

  7. Swoole 中使用 Table 内存表实现进程间共享数据

    背景 在多进程模式下进程之间的内存是相互隔离的,在一个工作进程中的全局变量和超全局变量,在另一个工作进程中是无法读取和操作的. 如果只有一个工作进程,则不存在进程隔离问题,可以使用全局变量和超全局变量 ...

  8. Selenium_使用execute_script执行JavaScript(11)

    selenium的包含的方法已能完全满足UI自动化,但是有些时候又不得不用到执行JS的情况,比如在一个富文本框中输入1W个字,使用send_keys方法将经历漫长的输入过程,如果换成使用JS的inne ...

  9. PPT制作手机滑动粗糙效果

    原文链接:https://www.toutiao.com/i6495291974680052238/ 选择"插入"选项卡,"插图"功能组."形状&qu ...

  10. 论文翻译:2021_AEC IN A NETSHELL: ON TARGET AND TOPOLOGY CHOICES FOR FCRN ACOUSTIC ECHO CANCELLATION

    论文地址:https://ieeexploreieee.53yu.com/abstract/document/9414715 Netshell 中的 AEC:关于 FCRN 声学回声消除的目标和拓扑选 ...