用JavaScript實(shí)現(xiàn)對PDF的全文索引

2013-10-14 09:20:05來源:oschina作者:

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

我曾今在一個售賣法律和財務(wù)數(shù)據(jù)庫訪問方案(他們稱之為“智能信息”)的公司工作。大多數(shù)法庭記錄都是通過PACER以PDF形式提供的,一個站點(diǎn)被特地開發(fā)出來用于發(fā)布法庭記錄;谶@個數(shù)據(jù)集的一個意義重大的數(shù)據(jù)庫產(chǎn)品需要建立一條處理管道,它能夠從超過兩億分份PDF文檔中提取文本并對其進(jìn)行索引,展示美國超過20年的訴訟記錄。這些處理過程將花費(fèi)數(shù)月的機(jī)器時間,使得軟件工作組在構(gòu)建它們時的面臨很大的壓力。在這一處理過程中的早期有個一步驟是從電子文檔化的PDF中提取出內(nèi)容,其在稍后的將會被送入一個NLP處理階段——顯示關(guān)鍵字,標(biāo)注部分詞類,識別實(shí)體,而然后發(fā)出報告。

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

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

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

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

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

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

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看起還不怎么成熟——理想情況下你應(yīng)該能夠?qū)懗?promise.then(f(x)).then(g(x)).then(h(x)) 等等代碼,但現(xiàn)在那還是不可用的。

約定模式在渲染PDF方面起了很大的作用,因?yàn)樗鼮椴⑿械匿秩咎幚砹粝铝丝臻g。對于只是從一份PDF中提取出文本感覺上好像有大量的工作要做——你必須相信你的回調(diào)會按照秩序運(yùn)行并且跟蹤到哪個是在最后。

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

‘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);
 }
});

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

Lunr

創(chuàng)建一個Lunr函數(shù)直接添加字段-所有的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)

搜索也很方便,一個簡單的方法可以查詢索引,因?yàn)樗皇且粋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

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

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

var serializedIndex = JSON.stringify(index1.toJSON())
var deserializedIndex = JSON.parse(serializedIndex)
var index2 = lunr.Index.load(deserializedIndex)

Index.toJSON也會返回一個“bean”風(fēng)格的對象(而不是一個string)。我從來沒有見過像這樣的API,但是我喜歡這個創(chuàng)意——它給了你一個干凈的Javascript對象,只帶有需要被序列化的數(shù)據(jù)。

下面是索引的屬性:

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

這種索引最棒的一個特性是作業(yè)可以并行完成,然后作為一個map-reduce作業(yè)被整合。上述對象只有三個條目需要被整合,因?yàn)?ldquo;域”和“管道”是靜態(tài)的。下面就展示了再現(xiàn)步驟的實(shí)現(xiàn)(注意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)

通過創(chuàng)建三個索引我測試了這段代碼:index1,index2和index3。index1是{doc1},index2是{doc2,doc3},而index3則是{doc1,doc2,doc3}。為了測試這段代碼,你需要簡單的改變:

JSON.stringify(index3.toJSON())
 
JSON.stringify(combine(index1, index2))

可能性

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

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

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

關(guān)鍵詞:JavaScriptpdf