这篇文章上次修改于 2460 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

读取目录,从PDF文件开始着手

有四种资源格式,epub,pdf,mobi,tif,要读取文本和图片,还要得到排版的页码,pdf自然是首选。

iText

从来没接触过读取pdf,搜索得到推崇的工具iText,到官网下了jar包,当前版本7.0.2

编目工具首先考虑的是读取目录

public static void itext() throws IOException {
    PdfReader reader = new PdfReader(path);
       List list = SimpleBookmark.getBookmark(reader);
       for (Iterator i = list.iterator(); i.hasNext(); ) {
        showBookmark((Map) i.next());
    }
}
    
private static void showBookmark(Map bookmark) {
    System.out.println(bookmark.get("Title")+"---"+bookmark.get("Page"));
    ArrayList kids = (ArrayList) bookmark.get("Kids");
    if (kids == null)
        return;
    for (Iterator i = kids.iterator(); i.hasNext(); ) {
        showBookmark((Map) i.next());
    }
}

然后再读取内容

PdfReader reader = new PdfReader(path);

PdfReaderContentParser parser = new PdfReaderContentParser(reader);

for (int i = 1;i<5;i++) {

    TextExtractionStrategy strategy = parser.processContent(i, new SimpleTextExtractionStrategy());

    String textFromPage = strategy.getResultantText();
      System.out.println(textFromPage);
}

读了5页,看看效果,问题就出来了,首先页面顺序错乱,页面中掺有页眉页脚和页码,而且没有规律的掺在其中,有时占一行,两行或者三行,还有个致命的问题,文字重复,想口吃一样,经常同一个字好几个连在一起。网上搜出来的基本都是靠这个API取文本,可能他还有其它API可以研究,在这个时候,同事和我说...

同事力挺的PdfBox

同事说他搞过pdf,用的是PdfBox,PdfBox是apache的项目,很强很好用,于是就放弃研究iText,去找了PdfBox的jar包,PdfBox有两个版本,开始下了最新的2.0.7,网上的代码跑不起来,API方法丢失,于是又下了1.8.13。

封装方法(来源于网络,支持PdfBox版本1.8)

public class PdfBoxReader {
    /**
     * 获取格式化后的时间信息
     * @param calendar   时间信息
     * @return
     */
    public static String dateFormat( Calendar calendar ){
        if( null == calendar )
            return null;
        String date = null;
        String pattern = "yyyy-MM-dd HH:mm:ss";
        SimpleDateFormat format = new SimpleDateFormat( pattern );
        date = format.format( calendar.getTime() );
        return date == null ? "" : date;
    }
    
    /**打印纲要**/
    public static void getPDFOutline(String file){
        try {
            //打开pdf文件流
            FileInputStream fis = new   FileInputStream(file);
            //加载 pdf 文档,获取PDDocument文档对象
            PDDocument document=PDDocument.load(fis);
            //获取PDDocumentCatalog文档目录对象
            PDDocumentCatalog catalog=document.getDocumentCatalog();
            //获取PDDocumentOutline文档纲要对象
            PDDocumentOutline outline=catalog.getDocumentOutline();
            //获取第一个纲要条目(标题1)
            PDOutlineItem item=outline.getFirstChild();
            if(outline!=null){
                //遍历每一个标题1
                while(item!=null){
                    //打印标题1的文本
                    System.out.println("Item:"+item.getTitle());
                    //获取标题1下的第一个子标题(标题2)
                    PDOutlineItem child=item.getFirstChild();
                    //遍历每一个标题2
                    while(child!=null){
                        //打印标题2的文本
                        System.out.println("    Child:"+child.getTitle());
                        //指向下一个标题2
                        child=child.getNextSibling();
                    }
                    //指向下一个标题1
                    item=item.getNextSibling();
                }
            }
            //关闭输入流
            document.close();
            fis.close();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    
    /**打印一级目录**/
    public static void getPDFCatalog(String file){
        try {
            //打开pdf文件流
            FileInputStream fis = new   FileInputStream(file);
            //加载 pdf 文档,获取PDDocument文档对象
            PDDocument document=PDDocument.load(fis);
            //获取PDDocumentCatalog文档目录对象
            PDDocumentCatalog catalog=document.getDocumentCatalog();
            //获取PDDocumentOutline文档纲要对象
            PDDocumentOutline outline=catalog.getDocumentOutline();
            //获取第一个纲要条目(标题1)
            if(outline!=null){
                PDOutlineItem item=outline.getFirstChild();
                //遍历每一个标题1
                while(item!=null){
                    //打印标题1的文本
                    System.out.println("Item:"+item.getTitle());
                    //指向下一个标题1
                    item=item.getNextSibling();
                }
            }
            //关闭输入流
            document.close();
            fis.close();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    
    /**获取PDF文档元数据**/
    public static void getPDFInformation(String file){
        try {
            //打开pdf文件流
            FileInputStream fis = new   FileInputStream(file);
            //加载 pdf 文档,获取PDDocument文档对象
            PDDocument document=PDDocument.load(fis);
            /** 文档属性信息 **/
            PDDocumentInformation info = document.getDocumentInformation();
    
            System.out.println("页数:"+document.getNumberOfPages());
    
            System.out.println( "标题:" + info.getTitle() );
            System.out.println( "主题:" + info.getSubject() );
            System.out.println( "作者:" + info.getAuthor() );
            System.out.println( "关键字:" + info.getKeywords() );
    
            System.out.println( "应用程序:" + info.getCreator() );
            System.out.println( "pdf 制作程序:" + info.getProducer() );
    
            System.out.println( "Trapped:" + info.getTrapped() );
    
            System.out.println( "创建时间:" + dateFormat( info.getCreationDate() ));
            System.out.println( "修改时间:" + dateFormat( info.getModificationDate()));
    
            //关闭输入流
            document.close();
            fis.close();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    
    /**提取pdf文本**/
    public static void extractTXT(String file){
        try{
            //打开pdf文件流
            FileInputStream fis = new   FileInputStream(file);
            //实例化一个PDF解析器
            PDFParser parser = new PDFParser(fis);
            //解析pdf文档
            parser.parse();
            //获取PDDocument文档对象
            PDDocument document=parser.getPDDocument();
            //获取一个PDFTextStripper文本剥离对象
            PDFTextStripper stripper = new PDFTextStripper();
            //获取文本内容
            String content = stripper.getText(document);
            //打印内容
            System.out.println( "内容:" + content );
            document.close();
            fis.close();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    
    /**
     * 提取部分页面文本
     * @param file pdf文档路径
     * @param startPage 开始页数
     * @param endPage 结束页数
     */
    public static void extractTXT(String file,int startPage,int endPage){
        try{
            //打开pdf文件流
            FileInputStream fis = new   FileInputStream(file);
            //实例化一个PDF解析器
            PDFParser parser = new PDFParser(fis);
            //解析pdf文档
            parser.parse();
            //获取PDDocument文档对象
            PDDocument document=parser.getPDDocument();
            //获取一个PDFTextStripper文本剥离对象
            PDFTextStripper stripper = new PDFTextStripper();
            // 设置起始页
            stripper.setStartPage(startPage);
            // 设置结束页
            stripper.setEndPage(endPage);
            //获取文本内容
            String content = stripper.getText(document);
            //打印内容
            System.out.println( "内容:" + content );
            document.close();
            fis.close();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    
    /**
     * 提取图片并保存
     * @param file PDF文档路径
     * @param imgSavePath 图片保存路径
     */
    public static void extractImage(String file,String imgSavePath){
        try{
            //打开pdf文件流
            FileInputStream fis = new   FileInputStream(file);
            //加载 pdf 文档,获取PDDocument文档对象
            PDDocument document=PDDocument.load(fis);
            /** 文档页面信息 **/
            //获取PDDocumentCatalog文档目录对象
            PDDocumentCatalog catalog = document.getDocumentCatalog();
            //获取文档页面PDPage列表
            List pages = catalog.getAllPages();
            int count = 1;
            int pageNum=pages.size();   //文档页数
            //遍历每一页
            for( int i = 0; i < pageNum; i++ ){
                //取得第i页
                PDPage page = ( PDPage ) pages.get( i );
                if( null != page ){
                    PDResources resource = page.findResources();
                    //获取页面图片信息
                    Map<String,PDXObjectImage> imgs = resource.getImages();
                    for(Map.Entry<String,PDXObjectImage> me: imgs.entrySet()){
                        //System.out.println(me.getKey());
                        PDXObjectImage img = me.getValue();
                        //保存图片,会自动添加图片后缀类型
                        img.write2file( imgSavePath + count );
                        count++;
                    }
                }
            }
            document.close();
            fis.close();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    
    /**
     * 提取文本并保存
     * @param file PDF文档路径
     * @param savePath 文本保存路径
     */
    public static void extractTXT(String file,String savePath){
        try{
            //打开pdf文件流
            FileInputStream fis = new   FileInputStream(file);
            //实例化一个PDF解析器
            PDFParser parser = new PDFParser(fis);
            //解析pdf文档
            parser.parse();
            //获取PDDocument文档对象
            PDDocument document=parser.getPDDocument();
            //获取一个PDFTextStripper文本剥离对象
            PDFTextStripper stripper = new PDFTextStripper();
            //创建一个输出流
            Writer writer=new OutputStreamWriter(new FileOutputStream(savePath));
            //保存文本内容
            stripper.writeText(document, writer);
            //关闭输出流
            writer.close();
            //关闭输入流
            document.close();
            fis.close();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {ex.printStackTrace();
        }
    }
    
    /**
     * 提取部分页面文本并保存
     * @param file PDF文档路径
     * @param startPage 开始页数
     * @param endPage 结束页数
     * @param savePath 文本保存路径
     */
    public static void extractTXT(String file,int startPage,
                                  int endPage,String savePath){
        try{
            //打开pdf文件流
            FileInputStream fis = new   FileInputStream(file);
            //实例化一个PDF解析器
            PDFParser parser = new PDFParser(fis);
            //解析pdf文档
            parser.parse();
            //获取PDDocument文档对象
            PDDocument document=parser.getPDDocument();
            //获取一个PDFTextStripper文本剥离对象
            PDFTextStripper stripper = new PDFTextStripper();
            //创建一个输出流
            Writer writer=new OutputStreamWriter(new FileOutputStream(savePath));
            // 设置起始页
            stripper.setStartPage(startPage);
            // 设置结束页
            stripper.setEndPage(endPage);
            //保存文本内容
            stripper.writeText(document, writer);
            //关闭输出流
            writer.close();
            //关闭输入流
            document.close();
            fis.close();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

PdfBox也没有满足需求,读取目录时,没有拿到二级目录,后来得知是因为资源加工时,没有生成二级目录,所以没有拿到二级目录,但iText却可以拿到,猜测iText可能是用搜索的方法拿到的,iText还可以得到目录对应的页数,但PdfBox拿不到,而且页面顺序也是混乱的,也有页眉页脚问题。

FileInputStream fis = new   FileInputStream(path);
//加载 pdf 文档,获取PDDocument文档对象
PDDocument document=PDDocument.load(fis);

int numberOfPages = document.getNumberOfPages();

PDFTextStripper stripper = new PDFTextStripper();

stripper.setSortByPosition(true);
stripper.setStartPage(1);
stripper.setEndPage(10);
String content = stripper.getText(document);
System.out.println(content);

即使使用了stripper.setSortByPosition(true);页面顺序依旧混乱。

放弃PDF,盯上了word

毕竟没有深入研究过,使用的pdf资源也不能保证是最标准的,所以以上对iText和PdfBox的评论,只基于我现有知识结构,做出的判断。决定放弃PDF,放弃PDF后,epub是网页结构不分页,不能用,tif扫描图片也不行,mobi没有研究过,与是就想到生成这几种格式时的源格式是什么,找到加工资源的人员,他们用的是indesign,好像更没有办法读取,好在那边说可以生成word文档。

又是iText

读取word,网上依然推荐iText,网上的资源还是那些固定的内容,要么取图片,要么取文字。

老朋友POI

读取word还可以使用POI,POI我不陌生,一直用它操作Excel,所以也算是老朋友,即使网上不建议用POI读取word,还是试一下。

doc&docx

众所周知,office软件两种格式的文件,2007版之后的文件格式都在原有扩展名后追加"x"。实质,文档的内部结构也发生了变化,所以POI有两套API来操作不同版本的office文件。

操作doc文件(来源于网络)

private static void readDoc() throws Exception {

    InputStream is = new FileInputStream(path);
    HWPFDocument doc = new HWPFDocument(is);
    
    //输出书签信息
    printInfo(doc.getBookmarks());
    //输出文本
    System.out.println(doc.getDocumentText());
    Range range = doc.getRange();
    //this.insertInfo(range);
    printInfo(range);
    //读表格
    readTable(range);
    //读列表
    readList(range);
    //删除range
    Range r = new Range(2, 5, doc);
    r.delete();//在内存中进行删除,如果需要保存到文件中需要再把它写回文件
    //把当前HWPFDocument写到输出流中
    //doc.write(new FileOutputStream("/Users/dean/Desktop/Work/图书编目/加工标准/print.doc"));
    closeStream(is);
    
    findImage(doc);
    
}
    
private static void findImage(HWPFDocument doc) throws IOException {
    
    int length = doc.characterLength();
    PicturesTable pTable = doc.getPicturesTable();
    // int TitleLength=doc.getSummaryInformation().getTitle().length();
    
    //  System.out.println(TitleLength);
    // System.out.println(length);
    for (int i = 0; i < length; i++) {
        Range range = new Range(i, i + 1, doc);
    
        CharacterRun cr = range.getCharacterRun(0);
        if (pTable.hasPicture(cr)) {
            Picture pic = pTable.extractPicture(cr, false);
            String afileName = pic.suggestFullFileName();
    
            System.out.println("IMG:" + afileName);
            //OutputStream out=new FileOutputStream(new File("F:\\test\\"+ UUID.randomUUID()+afileName));
            //pic.writeImageContent(out);
    
        }
    }
}


/**
 * 关闭输入流
 *
 * @param is
 */
private static void closeStream(InputStream is) {
    if (is != null) {
        try {
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
    
/**
 * 输出书签信息
 *
 * @param bookmarks
 */
private static void printInfo(Bookmarks bookmarks) {
    int count = bookmarks.getBookmarksCount();
    System.out.println("书签数量:" + count);
    Bookmark bookmark;
    for (int i = 0; i < count; i++) {
        bookmark = bookmarks.getBookmark(i);
        System.out.println("书签" + (i + 1) + "的名称是:" + bookmark.getName());
        System.out.println("开始位置:" + bookmark.getStart());
        System.out.println("结束位置:" + bookmark.getEnd());
    }
}
    
/**
 * 读表格
 * 每一个回车符代表一个段落,所以对于表格而言,每一个单元格至少包含一个段落,每行结束都是一个段落。
 *
 * @param range
 */
private static void readTable(Range range) {
    //遍历range范围内的table。
    TableIterator tableIter = new TableIterator(range);
    Table table;
    TableRow row;
    TableCell cell;
    while (tableIter.hasNext()) {
        table = tableIter.next();
        int rowNum = table.numRows();
        for (int j = 0; j < rowNum; j++) {
            row = table.getRow(j);
            int cellNum = row.numCells();
            for (int k = 0; k < cellNum; k++) {
                cell = row.getCell(k);
                //输出单元格的文本
                System.out.println(cell.text().trim());
            }
        }
    }
}
    
/**
 * 读列表
 *
 * @param range
 */
private static void readList(Range range) {
    int num = range.numParagraphs();
    Paragraph para;
    for (int i = 0; i < num; i++) {
        para = range.getParagraph(i);
        if (para.isInList()) {
            System.out.println("list: " + para.text());
        }
    }
}
    
/**
 * 输出Range
 *
 * @param range
 */
private static void printInfo(Range range) {
    //获取段落数
    int paraNum = range.numParagraphs();
    System.out.println(paraNum);
    for (int i = 0; i < paraNum; i++) {
//this.insertInfo(range.getParagraph(i));
        System.out.println("段落" + (i + 1) + ":" + range.getParagraph(i).text());
        if (i == (paraNum - 1)) {
            insertInfo(range.getParagraph(i));
        }
    }
    int secNum = range.numSections();
    System.out.println(secNum);
    Section section;
    for (int i = 0; i < secNum; i++) {
        section = range.getSection(i);
        System.out.println(section.getMarginLeft());
        System.out.println(section.getMarginRight());
        System.out.println(section.getMarginTop());
        System.out.println(section.getMarginBottom());
        System.out.println(section.getPageHeight());
        System.out.println(section.text());
    }
}
    
/**
 * 插入内容到Range,这里只会写到内存中
 *
 * @param range
 */
private static void insertInfo(Range range) {
    range.insertAfter("Hello");
}

用上面的api得不到bookmark,明明是有目录的。还好,页面顺序是对的,目录再想办法。

OOM

跑一下资源文件,直接OOM了,看看文件140多M,然后网上推荐使用docx的api解决OOM的问题

读取docx文档(来源于网络,里边有一点错误,原版本的代码是取picture.getPackageRelationship().getId(),不知是版本问题,还是什么,现在没有这个api,XWPFPictureData取不到getPackageRelationship(),所以那里我改成了getFileName(),与后边的map.get(id),对应不上,所以后边那段添加图片无效)

private static void readDocx() throws Exception {

    try {
        FileInputStream inputStream = new FileInputStream(path);
        XWPFDocument xDocument = new XWPFDocument(inputStream);
    
        List<XWPFParagraph> paragraphs = xDocument.getParagraphs();
        List<XWPFPictureData> pictures = xDocument.getAllPictures();
        Map<String, String> map = new HashMap<String, String>();
        for(XWPFPictureData picture : pictures){
            //String id = picture.getPackageRelationship().getId();
            String id = picture.getFileName();
            File folder = new File(absolutePath);
            if (!folder.exists()) {
                folder.mkdirs();
            }
    
            System.out.println("Pic:"+id+"-"+picture.getChecksum());
            String rawName = picture.getFileName();
            String fileExt = rawName.substring(rawName.lastIndexOf("."));
            String newName = System.currentTimeMillis() + UUID.randomUUID().toString() + fileExt;
            File saveFile = new File(absolutePath + File.separator + newName);
            @SuppressWarnings("resource")
            FileOutputStream fos = new FileOutputStream(saveFile);
            fos.write(picture.getData());
            System.out.println(saveFile.getAbsolutePath());
            map.put(id, saveFile.getAbsolutePath());
        }
        String text = "";
        for(XWPFParagraph paragraph : paragraphs){
            System.out.println("--------------------------");
            System.out.println(paragraph.getParagraphText());
            System.out.println(paragraph.getText());
            System.out.println("--------------------------");
            List<XWPFRun> runs = paragraph.getRuns();
            for(XWPFRun run : runs){
                List<XWPFPicture> embeddedPictures = run.getEmbeddedPictures();
    
                if (embeddedPictures!=null){
    
                    for (XWPFPicture picture:embeddedPictures){
    
                        XWPFPictureData pictureData = picture.getPictureData();
    
                        Long checksum = pictureData.getChecksum();
    
                        String fileName = pictureData.getFileName();
    
                        System.out.println("RUN:"+fileName+"-"+checksum);
                    }
                }
    
                if(run.getCTR().xmlText().contains("<w:pict")){
    
                    List<XWPFPictureData> allPictures = run.getDocument().getAllPictures();


                    String runXmlText = run.getCTR().xmlText();
                    int rIdIndex = runXmlText.indexOf("r:id");
                    int rIdEndIndex = runXmlText.indexOf("/>", rIdIndex);
                    String rIdText = runXmlText.substring(rIdIndex, rIdEndIndex);
                    System.out.println(rIdText.split("\"")[1].substring("rId".length()));
                    String id = rIdText.split("\"")[1];
                    System.out.println(map.get(id));
                    text = text +"<img src = '"+map.get(id)+"'/>";
                }else{
                    text = text + run;
                }
            }
        }
        System.out.println(text);
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    
}

运行代码依然OOM,没有什么好的办法,只能把文档切成小文档,怎么切呢?word正好有个功能,在大纲视图下拆分文档,而且是按目录拆分的,保存成标题名为文档名的文件,正好在想怎么解决目录读不出的问题,两者一结合,读取目录也可以解决。

openxml的标签

对于内容的输入需求是每段加一对标签,图片加图片标签与段标签同级,顺序输出。

问题就又来了,图片怎么顺序排列在段标签里,POI提供的读取图片的方法,只有getAllPictures()方法,得到全部图片。

受到上面代码的启发,docx格式的文档其实是封装的openxml文件,解压看看里边的结构,主要文件是document.xml,里边存放的word的内容,比较乱,仔细研究研究,可以得到三种标签都代表图片“<w:pict”,“<w:drawing”,“<pic:”。

根据POI的API

文档中的所有段落

List<XWPFParagraph> paragraphs = document.getParagraphs();

一个相同样式内容的块

paragraphs.get(j).getCTP()
paragraphs.get(j).getCTP().xmlText()

用得到的xml文本去判断是不是图片,就可以按固定的段落顺序插入图片标签了。

取不到bookmark

取不到bookmark,也就是目录,上面已经说过可以用创建文档顺序和文件夹结构来构建目录,当然这一部分需要手动创建,最后要解决的就是页数问题,同样沿着判断图片的思路,继续研究document.xml,创建一个只有两页的word文档,发现了“<w:lastRenderedPageBreak”标签,同时POI有这样一个API

XWPFParagraph.isPageBreak()

这就是翻页时的标志,不过有的word文档没有这个标签,操作word文档,插入-分页。有了翻页,还需要有个起始位置,读不到bookmark,那也只能人工指定。

写在最后

先说说pdf,pdf原来是有语法的,可以参见pdf语法总结,初步了解pdf推荐看看 一个简单PDF文件的结构分析

对于word,document.xml还是很有意思的,如果有时间值得好好研究研究它的各种标签的含义。


2017/8/3.

Dean.King

Beijing