使用 JavaScript 實現對 PDF 的全文索引

jopen 11年前發布 | 23K 次閱讀 PDF工具包 JavaScript

使用 JavaScript 實現對 PDF 的全文索引

我曾今在一個售賣法律和財務數據庫訪問方案(他們稱之為“智能信息”)的公司工作。大多數法庭記錄都是通過PACER以PDF形式提供的,一個站點被特地開發出來用于發布法庭記錄。基于這個數據集的一個意義重大的數據庫產品需要建立一條處理管道,它能夠從超過兩億分份PDF文檔中提取文本并對其進行索引,展示美國超過20年的訴訟記錄。這些處理過程將花費數月的機器時間,使得軟件工作組在構建它們時的面臨很大的壓力。在這一處理過程中的早期有個一步驟是從電子文檔化的PDF中提取出內容,其在稍后的將會被送入一個NLP處理階段——顯示關鍵字,標注部分詞類,識別實體,而然后發出報告(如果你對此感興趣,可以檢索 Python中的自然語言處理 作為入門 - 再讀一讀 這里我的評論  

Mozilla實驗室最近已經收到了許多為一個項目做出的嘗試,這一項目的野心令人印象深刻:在一個瀏覽器中僅僅使用Javascript來對PDF進行渲染。PDF文檔的結構令人難以置信的復雜,因此要祝pdf.js工作組的兄弟們好運了!在另外一條不同的嘗試道路上,Oliver Nightingale使用Javascript實現了一個的Javascript全文索引裝置——將這兩個項目結合起來,就可以在web瀏覽器中完全再現PDF處理管道。

站在一名新手的角度來看,全文索引能用戶可以搜索非結構化的文檔,也可以依據由詞頻決定的相關度分值來對結果文檔進行排名。索引裝置會計算每一個份文檔中每一個詞出現的次數,并且對文本進行最輕微的修改,以移除內容中跟搜索無關的一些文本語法特性。例如,它可能會提取出“-ing”,將元音部分變更為一般的表示形式。如果一個詞語頻繁出現在整個文檔集中,索引裝置會自動將其識別為不那么重要的關鍵詞,而它對排名結果的影響將會被最小化。這同Google PageRank背后的基本概念是不同的,后者是基于一個引征圖來提升文檔排名的。

大多數數據庫軟件都提供了對全文索引的支持,但如果是大規模安裝的話,通常會使用功能更加強大的工具來進行處理。開源產品中主要是Solr/Lucene,Solr是圍繞Lucene庫封裝的一個web應用。它們都是用Java編寫的。

構造一個Javascript全文索引裝置使得搜索在諸如Phonegap引用,終端用戶機或者加密存儲的用戶數據這些之前很難實現搜索功能的地方成為可能。有一整個領域只研究加密的搜索指數,而在客戶機上對數據進行索引和加密看上去像是圍繞這個天生具有挑戰性的問題想出的一個好辦法。

為了測試這個處理管道,我們首先來看看如何從PDF中提取文本,這些文本將在稍后被插入到一個全文索引中。pdf.js的代碼是很有啟發性的,其中Mozilla的開發者們使用了一些并不常用的瀏覽器特性,舉個例子,Web工作者,會要你設置后臺的處理線程。

pdf.js 的 API大量使用約定來持有代碼中未完成操作的引用。你會使用回調來對它們進行操作:

var pdf = PDFJS.getDocument('http://www.pacer.gov/documents/pacermanual.pdf');

var pdf = PDFJS.getDocument('pacermanual.pdf');
pdf.then(function(pdf) {
 // this code is called once the PDF is ready
});

這樣的API看起還不怎么成熟——理想情況下你應該能夠寫出 promise.then(f(x)).then(g(x)).then(h(x)) 等等代碼,但現在那還是不可用的。

約定模式在渲染PDF方面起了很大的作用,因為它為并行的渲染處理留下了空間。對于只是從一份PDF中提取出文本感覺上好像有大量的工作要做——你必須相信你的回調會按照秩序運行并且跟蹤到哪個是在最后。

下面的示例代碼演示了提取PDF內容,并在瀏覽器中控制臺日志中輸出:

‘use strict’;
var pdf = PDFJS.getDocument('http://www.pacer.gov/documents/pacermanual.pdf');

var pdf = PDFJS.getDocument('pacermanual.pdf');
pdf.then(function(pdf) {
 var maxPages = pdf.pdfInfo.numPages;
 for (var j = 1; j <= maxPages; j++) {
    var page = pdf.getPage(j);

    // the callback function - we create one per page
    var processPageText = function processPageText(pageIndex) {
      return function(pageData, content) {
        return function(text) {
          // bidiTexts has a property identifying whether this
          // text is left-to-right or right-to-left
          for (var i = 0; i < text.bidiTexts.length; i++) {
            str += text.bidiTexts[i].str;
          }

          if (pageData.pageInfo.pageIndex === 
              maxPages - 1) {
            // later this will insert into an index
            console.log(str);
          }
        }
      }
    }(j);

    var processPage = function processPage(pageData) {
      var content = pageData.getTextContent();

      content.then(processPageText(pageData, content));
    }

    page.then(processPage);
 }
});

這并不會識別頁眉和圖片.如何識別這些內容需要使用渲染代碼,需要非常理解PDF命令(PDF可能使用流渲染命令,類似于RTF)

Lunr

創建一個Lunr函數直接添加字段-所有的API都使用JSON類型,以下是一個簡單的AIP示例

doc1 = {
    id: 1,
    title: 'Foo',
    body: 'Foo foo foo!'
  };

doc2 = {
    id: 2,
    title: 'Bar',
    body: 'Bar bar bar!'
  } 

doc3 = {
    id: 3,
    title: 'gary',
    body: 'Foo Bar bar bar!'
  }

index = lunr(function () {
    this.field('title', {boost: 10})
    this.field('body')
    this.ref('id')
  })

// Add documents to the index
index.add(doc1)
index.add(doc2)
index.add(doc3)
搜索也很方便,一個簡單的方法可以查詢索引,因為它只是一個JS對象:
// Run a search
index.search(“foo”)

// Inspect the actual index to see which docs match a term
index2.tokenStore.root.f.o.o.docs

當我第一次接觸全文索引,我對他所謂的"文檔"有所迷惑-它包括了一個PDF或者一個辦公文檔以及任何一個數據庫,很可能包括大堆的文本.

如果你不得不時刻構建索引,全文索引將會是愚蠢的,而Lunr則使索引自身的序列化和反序列化變得真正簡單起來:

var serializedIndex = JSON.stringify(index1.toJSON())
var deserializedIndex = JSON.parse(serializedIndex)
var index2 = lunr.Index.load(deserializedIndex)
Index.toJSON也會返回一個“bean”風格的對象(而不是一個string)。我從來沒有見過像這樣的API,但是我喜歡這個創意——它給了你一個干凈的Javascript對象,只帶有需要被序列化的數據。

下面是索引的屬性:

  • corpusTokens – 已經排好序的token列表
  • documentStore – 每一份文檔的列表 – 系
  • fields – 用來描述每一份文檔的域 (類似于數據庫中列)
  • pipeline – 用來處理token的管道對象
  • tokenStore – 每一份文檔中關鍵詞出現的位置和頻率

這種索引最棒的一個特性是作業可以并行完成,然后作為一個map-reduce作業被整合。上述對象只有三個條目需要被整合,因為“域”和“管道”是靜態的。下面就展示了再現步驟的實現(注意jQuery被引入了):

(function reduce(a, b) { 
  var j1 = a.toJSON(); 
  var j2 = b.toJSON();

  // The "unique" function does uniqueness by sorting,
  // which we need here.
  var corpusTokens = 
      $.unique(
          $.merge(
              $.merge([], j1.corpusTokens), 
                           j2.corpusTokens));

  // It's important to create new arrays and
  // objects throughout, or else you modify 
  // the source indexes, which is disastrous.
  var documentStore = 
     {store: $.extend({}, 
                      j1.documentStore.store,
                      j2.documentStore.store),
      length: j1.documentStore.length + j2.documentStore.length};

  var jt1 = j1.tokenStore;
  var jt2 = j2.tokenStore;

  // The 'true' here triggers a deep copy
  var tokenStore = {
    root: $.extend(true, {}, jt1.root, jt2.root),
    length: jt1.length + jt2.length
  };

  return {version: j1.version,
          fields: $.merge([], j1.fields), 
          ref: j1.ref, 
          documentStore: documentStore, 
          tokenStore: tokenStore,
          corpusTokens: corpusTokens, 
          pipeline: $.merge([], j1.pipeline)}; 
})(index1, index2)
通過創建三個索引我測試了這段代碼:index1,index2和index3。index1是{doc1},index2是{doc2,doc3},而index3則是{doc1,doc2,doc3}。為了測試這段代碼,你需要簡單的改變:
JSON.stringify(index3.toJSON())

JSON.stringify(combine(index1, index2))

可能性

總的來說這項技術很浪費網絡I/O,使得這看起來很傻。從另外一方面來看,ebay和fiberr上待售清單上充斥著“網絡交通流量”的叫賣,通常來自背后彈出式廣告,僵尸網絡,隱藏的iframe等等。你能很容易的發現像“3美元20000次點擊”的列表,小批量的。因為沒有多少商業價值它通常是很便宜的,此外還得犯下各種形式的欺詐行為。

你需要一個便宜點的VM負載作為一個代理的帶寬,以及公共可用的數據——你不能將其作為一項針對瀏覽器跨域請求保護的搜刮技術。你也需要使用一種獨特的方式來生成單獨的文檔ID,也許要使用到原生的URL。

如果一種交通資源在現代瀏覽器上面運行,某些人就可能會將其作為一種潛在的廉價且擁有無限制處理能力的資源來加以利用,即使是出于整合索引的目的,盡管必須針對系統的自然不穩定性做出規定。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!