这篇文章上次修改于 2526 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
读取目录,从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
没有评论