用Adobe PDF Extract API挖出数据
在过去几个世纪里,在数以百万计的报告和科学研究中存在着难以计数的科学数据。虽然很多都被数字化成了易于使用的PDF格式,但里面的信息可能仍然被锁在那里,以一种聚合的方式来处理它并不容易。我们最近发布的PDF Extract API提供了一种强大的方式来获取文档的原始文本并智能地理解其中的内容。在这篇文章中,我将演示我们如何使用这个API来收集和汇总大量的数据,使之成为一个统一的整体。
我们的数据
在这个假设的例子中,我想象一个天文学组织(恰如其分地被命名为 "星光部")来研究恒星的发光程度。在我说什么之前,请注意我不是一个天文学家,也不是一个科学家。请记住这都是一个假设性的例子。总之,DSL(它是一个政府机构,所以当然要用首字母缩写)研究一组恒星的亮度。每年,它都会就这些恒星创建一份多页的报告。报告包含一个封面,然后是12页代表一年中每个月的表格。
DSL运作了20年,研究和报告同样的星星。这是一个丰富的信息,虽然它们很容易阅读并以PDF格式存储,但你如何去收集数据并对整个时间段进行分析?那是20个文件,每个文件有12页表格,总共有240个表格。有20个明星被分析,所以这就是4800个单独的统计数字。
正如我所说,所有这些数据都是电子格式的,所以理论上你可以复制和粘贴数据。但这很容易出错,而且无法扩展(想象一下,如果数据扩展到50年,每周报告和100颗星)。在这里,PDF Extract可以拯救一切。请注意,我在演示中使用的是我们的NodeSDK,但你也可以使用我们的Java或PythonSDKs。如果你使用其他东西,我们也有一个REST API,你可以使用。
PDF Extract支持从一个PDF中返回多种东西。
- 文本(以及字体、位置、组织信息等,所以比文本本身要多得多)
- 图片
- 表格
在我们的案例中,获取表格是我们需要的。在高层次上,我们需要遍历每一个可用的PDF,从中提取表格,然后我们的代码会将这些表格解析成一个统一的数据集。
代码的相关部分来到了这块。
JavaScript
const options = new PDFServicesSdk.ExtractPDF.options.ExtractPdfOptions.Builder()
.addElementsToExtract(PDFServicesSdk.ExtractPDF.options.ExtractElementType.TABLES)
.addTableStructureFormat(PDFServicesSdk.ExtractPDF.options.TableStructureType.CSV)
.build();
完整的操作还有几行代码,但基本上就是这样了。然而,你会看到的是,处理我们这组数据的代码本身其实比提取的过程更复杂。我们可以很容易地得到这些数据。我们只是需要更努力地工作来聚合它。
解决方案
我的解决方案可以归结为以下几个步骤。
- 扫描我们的文档文件夹中的PDF文件。这是通过文件夹上的glob模式完成的。我为此使用了优秀的globbyNPM包。
const pdfs = await globby('./gen/*.pdf');
- 看看我们是否已经提取了数据。我写的代码是这样的:一旦它提取并解析了相关的信息,它就会把结果储存在PDF的 "旁边"。我想,我们将来可能会得到更多的数据,再次运行这个脚本,而不会让它不必要地重复工作。
// pdf is the filename we are checking
// datafile would translate foo.pdf to foo.json
let datafile = pdf.replace('.pdf', '.json');
if(!fs.existsSync(datafile)) {
console.log(`Need to fetch the data for ${pdf}`);
data = await getData(pdf);
fs.writeFileSync(datafile, JSON.stringify(data), 'utf8');
- 如果PDF还没有被分析过,我们就需要做这个工作。这发生在名字很好的
getData函数中。这个函数有以下几个子步骤。首先,它使用PDF提取API抓取表格。
JavaScript
const credentials = PDFServicesSdk.Credentials
.serviceAccountCredentialsBuilder()
.fromFile('pdftools-api-credentials.json')
.build();
const executionContext = PDFServicesSdk.ExecutionContext.create(credentials);
const options = new PDFServicesSdk.ExtractPDF.options.ExtractPdfOptions.Builder()
.addElementsToExtract(PDFServicesSdk.ExtractPDF.options.ExtractElementType.TABLES)
.addTableStructureFormat(PDFServicesSdk.ExtractPDF.options.TableStructureType.CSV)
.build();
const extractPDFOperation = PDFServicesSdk.ExtractPDF.Operation.createNew();
const input = PDFServicesSdk.FileRef.createFromLocalFile(pdf,PDFServicesSdk.ExtractPDF.SupportedSourceFormat.pdf);
extractPDFOperation.setInput(input);
extractPDFOperation.setOptions(options);
let output = './' + nanoid() + '.zip';
try {
let result = await extractPDFOperation.execute(executionContext);
await result.saveAsFile(output);
} catch(e) {
console.log('Exception encountered while executing operation', err);
reject(err)
}
这个调用的结果是一个zip文件,所以我使用了一个库(nanoid)来生成一个随机文件名。接下来,我们需要把表格从压缩文件中取出来。
// ok, now we need to get tables/*.csv from the zip
const zip = new StreamZip.async({ file: output });
const entries = await zip.entries();
let csvs = [];
for (const entry of Object.values(entries)) {
if(entry.name.endsWith('.csv')) csvs.push(entry.name);
}
所以在这一点上,我们已经在zip中得到了12个CSV文件的名字。我们需要将它们全部解析出来,并将它们添加到一个数组中(每个月一个项目)。
let result = [];
这些数据被返回,然后以JSON格式存储在PDF旁边。这些数据(我在最后会有链接,你可以自己看一下)是一个数组的数组。顶层数组中的每个元素代表一个月。然后第二层数组是表格数据的数组。这里是一个实例。
[
{
"NAME": "Albadore",
"LUMINOSITY": "44.377727966701904"
},
{
"NAME": "Barnie",
"LUMINOSITY": "22.82641486947966"
},
{
"NAME": "Camden",
"LUMINOSITY": "30.147538516875166"
},
{
"NAME": "Delphinus",
"LUMINOSITY": "24.18838138551451"
},
{
"NAME": "Ernie",
"LUMINOSITY": "39.326469491032285"
},
{
"NAME": "Foofihagen",
"LUMINOSITY": "34.09839876252938"
},
{
"NAME": "Glados",
"LUMINOSITY": "45.42275137909815"
},
{
"NAME": "Helix",
"LUMINOSITY": "28.434317384475182"
},
{
"NAME": "Icarus",
"LUMINOSITY": "45.26036772094769"
},
{
"NAME": "Juniper",
"LUMINOSITY": "37.61983037130726"
},
{
"NAME": "Kelix",
"LUMINOSITY": "34.33577955268733"
},
{
"NAME": "Lindy",
"LUMINOSITY": "44.132514920041444"
},
{
"NAME": "Madzuga",
"LUMINOSITY": "23.429262283398895"
},
{
"NAME": "Nicronat",
"LUMINOSITY": "32.57940088137253"
},
{
"NAME": "Olicity",
"LUMINOSITY": "41.76234700460494"
},
{
"NAME": "Patronus",
"LUMINOSITY": "38.57095289213085"
},
{
"NAME": "Queen",
"LUMINOSITY": "20.83277986794452"
},
{
"NAME": "Romana",
"LUMINOSITY": "39.28237819440117"
},
{
"NAME": "Silver",
"LUMINOSITY": "39.03680756359309"
},
{
"NAME": "Tritonus",
"LUMINOSITY": "36.96249959958089"
}
],
你会注意到,这些数值是引号。我可以在保存之前将它们转换为适当的数字。
- 好的,所以在这一点上,我们已经创建了,或者说读入了很多年的明星数据。这些信息在一个叫做
data的变量中。我有一个名为result的变量,它将存储所有信息。
let year = pdf.split('/').pop().split('.').shift();
result.push({
year,
data
});
我把年份加到这个值上,以帮助我保持事情的条理性。
- 然后,最后将结果写入文件系统。
fs.writeFileSync(outputFile, JSON.stringify(result), 'utf8');
总而言之,这一切看起来有点复杂,而且确实如此。我们有跨越不同页面和不同PDF的数据,但Extract API能够暴露我需要的数据。最棒的是,在这一点上,我已经在一个文件中得到了全部的信息(当然,一个数据库也可以很好地工作),我可以按照我认为合适的方式对其进行切分。我使用Chart.js制作了一个快速的演示,并能够绘制所有的星星和它们随时间的变化。