如何使用JavaScript从PDF中提取文本

73

我想知道是否可能仅使用Javascript获取PDF文件中的文本? 如果是,有人可以向我展示如何吗?

我知道有一些服务器端的Java、C#等库,但我希望不使用服务器。谢谢。

9个回答

94
由于pdf.js多年来一直在不断发展,我想提供一个新的解决方案。也就是说,可以在本地完成而不涉及任何服务器或外部服务。新的pdf.js有一个函数:page.getTextContent()。您可以从中获取文本内容。以下是我使用的成功代码:
1.每个步骤中得到的都是promise。您需要这样编码:.then(function(){...})来进行下一步。
  1. PDFJS.getDocument(data).then(function(pdf){

  2. pdf.getPage(i).then(function(page){

  3. page.getTextContent().then(function(textContent){

2.最终得到的是字符串数组textContent.bidiTexts[]。将它们连接起来以获取第1页的文本。文本块的坐标用于判断是否需要插入换行符或空格。(这可能不完全健壮,但从我的测试中看起来还可以。)
3.输入参数data需要是URL或ArrayBuffer类型数据。我使用了FileReader API中的ReadAsArrayBuffer(file)函数来获取数据。
注意:根据其他用户的说法,库已更新并导致代码出现错误。根据下面async5的评论,您需要将textContent.bidiTexts替换为textContent.items
    function Pdf2TextClass(){
     var self = this;
     this.complete = 0;

    /**
     *
     * @param data ArrayBuffer of the pdf file content
     * @param callbackPageDone To inform the progress each time
     *        when a page is finished. The callback function's input parameters are:
     *        1) number of pages done;
     *        2) total number of pages in file.
     * @param callbackAllDone The input parameter of callback function is 
     *        the result of extracted text from pdf file.
     *
     */
     this.pdfToText = function(data, callbackPageDone, callbackAllDone){
     console.assert( data  instanceof ArrayBuffer  || typeof data == 'string' );
     PDFJS.getDocument( data ).then( function(pdf) {
     var div = document.getElementById('viewer');
    
     var total = pdf.numPages;
     callbackPageDone( 0, total );        
     var layers = {};        
     for (i = 1; i <= total; i++){
        pdf.getPage(i).then( function(page){
        var n = page.pageNumber;
        page.getTextContent().then( function(textContent){
          if( null != textContent.bidiTexts ){
            var page_text = "";
            var last_block = null;
            for( var k = 0; k < textContent.bidiTexts.length; k++ ){
                var block = textContent.bidiTexts[k];
                if( last_block != null && last_block.str[last_block.str.length-1] != ' '){
                    if( block.x < last_block.x )
                        page_text += "\r\n"; 
                    else if ( last_block.y != block.y && ( last_block.str.match(/^(\s?[a-zA-Z])$|^(.+\s[a-zA-Z])$/) == null ))
                        page_text += ' ';
                }
                page_text += block.str;
                last_block = block;
            }

            textContent != null && console.log("page " + n + " finished."); //" content: \n" + page_text);
            layers[n] =  page_text + "\n\n";
          }
          ++ self.complete;
          callbackPageDone( self.complete, total );
          if (self.complete == total){
            window.setTimeout(function(){
              var full_text = "";
              var num_pages = Object.keys(layers).length;
              for( var j = 1; j <= num_pages; j++)
                  full_text += layers[j] ;
              callbackAllDone(full_text);
            }, 1000);              
          }
        }); // end  of page.getTextContent().then
      }); // end of page.then
    } // of for
  });
 }; // end of pdfToText()
}; // end of class

2
古老的问题,但是答案非常好。你有任何想法如何使textLayer不将字符呈现为单独的div,而是将它们呈现为整个单词?我试图使用文本层重叠与绝对定位的div,但由于div数量太多,导致性能受到很大影响。如果您希望这作为一个单独的实际StackOverflow问题,我会创建一个。 - AJP
@gm2008 我一直在尝试使用你的函数从PDF中提取文本。然而,我无法提取文本。full_text最终返回一个空字符串。你能帮忙吗? - suzee
我也无法让它工作(API已更改)。下面添加了我的示例。 - SchizoDuckie
将更多的示例添加到答案中:https://github.com/mozilla/pdf.js/blob/master/examples/text-only/pdf2svg.js 和 https://github.com/mozilla/pdf.js/blob/master/examples/node/getinfo.js - async5
3
使用textContent.items替换textContent.bidiTexts。 - async5
显示剩余3条评论

13

我无法让gm2008的示例运行起来(pdf.js上的内部数据结构显然已经更改),因此我编写了自己的完全基于promise的解决方案,不使用任何DOM元素、查询选择器或画布,使用更新的pdf.js示例在mozilla上。

它需要一个文件路径来上传,因为我将其与node-webkit一起使用。您需要确保下载了cmaps并指向某个位置,并且您需要获取pdf.js和pdf.worker.js才能使其工作。

    /**
     * Extract text from PDFs with PDF.js
     * Uses the demo pdf.js from https://mozilla.github.io/pdf.js/getting_started/
     */
    this.pdfToText = function(data) {

        PDFJS.workerSrc = 'js/vendor/pdf.worker.js';
        PDFJS.cMapUrl = 'js/vendor/pdfjs/cmaps/';
        PDFJS.cMapPacked = true;

        return PDFJS.getDocument(data).then(function(pdf) {
            var pages = [];
            for (var i = 0; i < pdf.numPages; i++) {
                pages.push(i);
            }
            return Promise.all(pages.map(function(pageNumber) {
                return pdf.getPage(pageNumber + 1).then(function(page) {
                    return page.getTextContent().then(function(textContent) {
                        return textContent.items.map(function(item) {
                            return item.str;
                        }).join(' ');
                    });
                });
            })).then(function(pages) {
                return pages.join("\r\n");
            });
        });
    }

使用方法:

 self.pdfToText(files[0].path).then(function(result) {
      console.log("PDF done!", result);
 })

请参阅 https://github.com/mozilla/pdf.js/blob/master/examples/text-only/pdf2svg.js 和 https://github.com/mozilla/pdf.js/blob/master/examples/node/getinfo.js。 - async5
1
"PDFJS.getDocument(...).then不是函数。" - BartusZak

9

这里提供一个完整的可工作样例。

<html>
    <head>
        <script src="https://npmcdn.com/pdfjs-dist/build/pdf.js"></script>
    </head>
    <body>
        <input id="pdffile" name="pdffile" type="file" />
        <button id="btn" onclick="convert()">Process</button>
        <div id="result"></div>
    </body>
</html>

<script>

    function convert() {
        var fr=new FileReader();
        var pdff = new Pdf2TextClass();
        fr.onload=function(){
            pdff.pdfToText(fr.result, null, (text) => { document.getElementById('result').innerText += text; });
        }
        fr.readAsDataURL(document.getElementById('pdffile').files[0])
        
    }

    function Pdf2TextClass() {
        var self = this;
        this.complete = 0;

        this.pdfToText = function (data, callbackPageDone, callbackAllDone) {
            console.assert(data instanceof ArrayBuffer || typeof data == 'string');
            var loadingTask = pdfjsLib.getDocument(data);
            loadingTask.promise.then(function (pdf) {


                var total = pdf._pdfInfo.numPages;
                //callbackPageDone( 0, total );        
                var layers = {};
                for (i = 1; i <= total; i++) {
                    pdf.getPage(i).then(function (page) {
                        var n = page.pageNumber;
                        page.getTextContent().then(function (textContent) {

                            //console.log(textContent.items[0]);0
                            if (null != textContent.items) {
                                var page_text = "";
                                var last_block = null;
                                for (var k = 0; k < textContent.items.length; k++) {
                                    var block = textContent.items[k];
                                    if (last_block != null && last_block.str[last_block.str.length - 1] != ' ') {
                                        if (block.x < last_block.x)
                                            page_text += "\r\n";
                                        else if (last_block.y != block.y && (last_block.str.match(/^(\s?[a-zA-Z])$|^(.+\s[a-zA-Z])$/) == null))
                                            page_text += ' ';
                                    }
                                    page_text += block.str;
                                    last_block = block;
                                }

                                textContent != null && console.log("page " + n + " finished."); //" content: \n" + page_text);
                                layers[n] = page_text + "\n\n";
                            }
                            ++self.complete;
                            //callbackPageDone( self.complete, total );
                            if (self.complete == total) {
                                window.setTimeout(function () {
                                    var full_text = "";
                                    var num_pages = Object.keys(layers).length;
                                    for (var j = 1; j <= num_pages; j++)
                                        full_text += layers[j];
                                    callbackAllDone(full_text);
                                }, 1000);
                            }
                        }); // end  of page.getTextContent().then
                    }); // end of page.then
                } // of for
            });
        }; // end of pdfToText()
    }; // end of class

</script>


2
API使用已过时:未指定“GlobalWorkerOptions.workerSrc”。 - Pavel T

7

以下是使用来自http://hublog.hubmed.org/archives/001948.html的Pdf.js实现您想要的功能的JavaScript代码:

var input = document.getElementById("input");  
var processor = document.getElementById("processor");  
var output = document.getElementById("output");  

// listen for messages from the processor  
window.addEventListener("message", function(event){  
  if (event.source != processor.contentWindow) return;  

  switch (event.data){  
    // "ready" = the processor is ready, so fetch the PDF file  
    case "ready":  
      var xhr = new XMLHttpRequest;  
      xhr.open('GET', input.getAttribute("src"), true);  
      xhr.responseType = "arraybuffer";  
      xhr.onload = function(event) {  
        processor.contentWindow.postMessage(this.response, "*");  
      };  
      xhr.send();  
    break;  

    // anything else = the processor has returned the text of the PDF  
    default:  
      output.textContent = event.data.replace(/\s+/g, " ");  
    break;  
  }  
}, true);

以下是一个示例:

http://git.macropus.org/2011/11/pdftotext/example/

该链接提供了一个示例。

虽然这些链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。仅有链接的答案如果链接页面更改可能会变得无效。 - Nikola K.
1
嗨,我正在尝试这个,但仍需要将文件上传到服务器。如何在本地客户端处理文件? - Ben

4
注意:此代码假定您正在使用nodejs。这意味着您正在解析本地文件,而不是网页上的文件,因为原始问题并没有明确要求解析网页上的pdf文件。
@gm2008的答案是一个很好的起点(请阅读它及其评论以获取更多信息),但需要一些更新(08/19)并且有一些未使用的代码。我也喜欢更全面的示例。还有更多的重构和调整可以完成(例如使用await),但现在它已经尽可能接近原始答案了。
与以前一样,这使用Mozilla的PDFjs库。npmjs包位于https://www.npmjs.com/package/pdfjs-dist根据我的经验,它不能很好地找到空格的位置,但这是另一个时间的问题。 [编辑:我相信使用.transform的更新已经恢复了原来的空白行为。]
// This file is called myPDFfileToText.js and is in the root folder
let PDFJS = require('pdfjs-dist');

let pathToPDF = 'path/to/myPDFfileToText.pdf';

let toText = Pdf2TextObj();
let onPageDone = function() {}; // don't want to do anything between pages
let onFinish = function(fullText) { console.log(fullText) };
toText.pdfToText(pathToPDF, onPageDone, onFinish);

function Pdf2TextObj() {
    let self = this;
    this.complete = 0;

    /**
     *
     * @param path Path to the pdf file.
     * @param callbackPageDone To inform the progress each time
     *        when a page is finished. The callback function's input parameters are:
     *        1) number of pages done.
     *        2) total number of pages in file.
     *        3) the `page` object itself or null.
     * @param callbackAllDone Called after all text has been collected. Input parameters:
     *        1) full text of parsed pdf.
     *
     */
    this.pdfToText = function(path, callbackPageDone, callbackAllDone) {
        // console.assert(typeof path == 'string');
        PDFJS.getDocument(path).promise.then(function(pdf) {

            let total = pdf.numPages;
            callbackPageDone(0, total, null);

            let pages = {};
            // For some (pdf?) reason these don't all come in consecutive
            // order. That's why they're stored as an object and then
            // processed one final time at the end.
            for (let pagei = 1; pagei <= total; pagei++) {
                pdf.getPage(pagei).then(function(page) {
                    let pageNumber = page.pageNumber;
                    page.getTextContent().then(function(textContent) {
                        if (null != textContent.items) {
                            let page_text = "";
                            let last_item = null;
                            for (let itemsi = 0; itemsi < textContent.items.length; itemsi++) {
                                let item = textContent.items[itemsi];
                                // I think to add whitespace properly would be more complex and
                                // would require two loops.
                                if (last_item != null && last_item.str[last_item.str.length - 1] != ' ') {
                                    let itemX = item.transform[5]
                                    let lastItemX = last_item.transform[5]
                                    let itemY = item.transform[4]
                                    let lastItemY = last_item.transform[4]
                                    if (itemX < lastItemX)
                                        page_text += "\r\n";
                                    else if (itemY != lastItemY && (last_item.str.match(/^(\s?[a-zA-Z])$|^(.+\s[a-zA-Z])$/) == null))
                                        page_text += ' ';
                                } // ends if may need to add whitespace

                                page_text += item.str;
                                last_item = item;
                            } // ends for every item of text

                            textContent != null && console.log("page " + pageNumber + " finished.") // " content: \n" + page_text);
                            pages[pageNumber] = page_text + "\n\n";
                        } // ends if has items

                        ++self.complete;

                        callbackPageDone(self.complete, total, page);


                        // If all done, put pages in order and combine all
                        // text, then pass that to the callback
                        if (self.complete == total) {
                            // Using `setTimeout()` isn't a stable way of making sure 
                            // the process has finished. Watch out for missed pages.
                            // A future version might do this with promises.
                            setTimeout(function() {
                                let full_text = "";
                                let num_pages = Object.keys(pages).length;
                                for (let pageNum = 1; pageNum <= num_pages; pageNum++)
                                    full_text += pages[pageNum];
                                callbackAllDone(full_text);
                            }, 1000);
                        }
                    }); // ends page.getTextContent().then
                }); // ends page.then
            } // ends for every page
        });
    }; // Ends pdfToText()

    return self;
}; // Ends object factory


在终端中运行:node myPDFfileToText.js

无法设置未定义的属性“complete”。 - BartusZak

2

更新于2021年2月

<script src="https://npmcdn.com/pdfjs-dist/build/pdf.js"></script>
    <script>
    
function Pdf2TextClass(){
    var self = this;
    this.complete = 0;

    this.pdfToText = function(data, callbackPageDone, callbackAllDone){
    console.assert( data  instanceof ArrayBuffer  || typeof data == 'string' );
    var loadingTask = pdfjsLib.getDocument(data);
    loadingTask.promise.then(function(pdf) {


    var total = pdf._pdfInfo.numPages;
    //callbackPageDone( 0, total );        
    var layers = {};        
    for (i = 1; i <= total; i++){
       pdf.getPage(i).then( function(page){
       var n = page.pageNumber;
       page.getTextContent().then( function(textContent){
       
       //console.log(textContent.items[0]);0
         if( null != textContent.items ){
           var page_text = "";
           var last_block = null;
           for( var k = 0; k < textContent.items.length; k++ ){
               var block = textContent.items[k];
               if( last_block != null && last_block.str[last_block.str.length-1] != ' '){
                   if( block.x < last_block.x )
                       page_text += "\r\n"; 
                   else if ( last_block.y != block.y && ( last_block.str.match(/^(\s?[a-zA-Z])$|^(.+\s[a-zA-Z])$/) == null ))
                       page_text += ' ';
               }
               page_text += block.str;
               last_block = block;
           }

           textContent != null && console.log("page " + n + " finished."); //" content: \n" + page_text);
           layers[n] =  page_text + "\n\n";
         }
         ++ self.complete;
         //callbackPageDone( self.complete, total );
         if (self.complete == total){
           window.setTimeout(function(){
             var full_text = "";
             var num_pages = Object.keys(layers).length;
             for( var j = 1; j <= num_pages; j++)
                 full_text += layers[j] ;
             console.log(full_text);
           }, 1000);              
         }
       }); // end  of page.getTextContent().then
     }); // end of page.then
   } // of for
 });
}; // end of pdfToText()
}; // end of class
var pdff = new Pdf2TextClass();
pdff.pdfToText('PDF_URL');
    </script>

1

@SchizoDuckie的解决方案,更简洁:

import { getDocument as loadPdf } from 'pdfjs-dist';

...

async pdfToTxt(file: File): Promise<string> {

  const pdf = await loadPdf(await file.arrayBuffer()).promise;

  return Promise.all([...Array(pdf.numPages).keys()]
    .map(async num => (await (await pdf.getPage(num + 1)).getTextContent())
      .items.map(item => (<any>item).str).join(' ')))
    .then(pages => pages.join('\n'));

}

我需要导入 "pdfjs-dist/build/pdf.worker.entry" 才能工作。 https://github.com/mozilla/pdf.js/issues/10478#issuecomment-1560704162。 谢谢Tom提供的解决方案。 - undefined

-2

对于所有真正想在节点服务器上使用它的人:

/**
 * Created by velten on 25.04.16.
 */
"use strict";
let pdfUrl = "http://example.com/example.pdf";
let request = require('request');
var pdfParser = require('pdf2json');

let pdfPipe = request({url: pdfUrl, encoding:null}).pipe(pdfParser);

pdfPipe.on("pdfParser_dataError", err => console.error(err) );
pdfPipe.on("pdfParser_dataReady", pdf => {
    //optionally:
    //let pdf = pdfParser.getMergedTextBlocksIfNeeded();

    let count1 = 0;
    //get text on a particular page
    for (let page of pdf.formImage.Pages) {
        count1 += page.Texts.length;
    }

    console.log(count1);
    pdfParser.destroy();
});

"dest.on不是一个函数" - BartusZak
@BartusZak foo.bar 也不是一个函数 ;) - velop

-3

这是可能的,但是:

  • 您仍然需要使用服务器,没有办法在不将文件传输到服务器并返回的情况下获取用户计算机上文件的内容
  • 我认为还没有人编写过这样的库

因此,如果您有空闲时间,可以学习PDF格式并自己编写这样的库,或者当然也可以使用服务器端库。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接