一个用于根据文件内容移动和重命名扫描文档的 Windows 服务。





5.00/5 (6投票s)
本文介绍了我如何编写一个小型 Windows 服务来处理根据内容对扫描(并经过 OCR)的文档进行排序和重命名的任务。
引言
在本文中,我将介绍我如何制作一个简单的 Windows 服务,该服务监视一个文件夹以接收传入的 PDF 文档(例如,来自扫描仪),然后根据文件内容重命名文件并将其移动到指定文件夹。该解决方案使用正则表达式来决定将文档移动到哪里(识别),然后使用它来提取对文件命名有用的信息,例如发票日期、客户名称等。
背景
在购买了一台新的扫描仪(绝佳的 ScanSnap IX500)以数字化超过 2000 页的旧发票和其他文件后,我面临着对所有扫描文档进行排序的问题,我意识到手动完成对我来说会非常枯燥,所以我决定用编程的方式来解决这个问题。
在对扫描仪及其附带的软件进行了一些研究后,我发现使用高分辨率扫描进行 OCR,然后缩小图像以生成实际的 PDF 是获得良好 OCR 质量的最佳方法。OCR 由 ABBYY 引擎完成,该引擎会在图像的相应位置之上放置一个透明文本层,从而创建一个可以标记、复制等操作的 PDF。
因此,当 ABBYY 完成后,我得到一个“可搜索 PDF”,而这个 PDF 又需要为我的项目进行解析。在研究了 PDF 文档软件的开源解决方案后,我发现 Apache PDFBox 最符合我的需求,而且碰巧在这里 codeproject.com 上有一篇文章(Converting-PDF-to-Text-in-C),其中包含了一些预编译的二进制文件,您在 .NET 项目中使用它所需的一切,所以我继续使用那里的示例。
使用代码
编译
要能够编译我的项目,您需要从此处下载二进制文件,并将以下文件包含在您项目的资源文件夹中
- IKVM.OpenJDK.Core.dll
- IKVM.OpenJDK.SwingAWT.dll
- pdfbox-1.7.0.dll
另外,请务必将以下文件复制到您项目的 bin 文件夹中(否则它将无法运行)
- commons-logging.dll
- fontbox-1.7.0.dll
- IKVM.OpenJDK.Util.dll
- IKVM.Runtime.dll
架构
该解决方案创建了一个 Windows 服务,需要使用 .NET 相应框架文件夹中的 installutil.exe 命令进行安装。在调试模式下,代码像往常一样使用 F5 运行,但当编译为发布版本时,它会变成一个服务。
总体流程
该项目的整个想法是:
- 监视一个文件夹以查找新的 PDF 文件。
- 当出现新文件时,搜索文件中的特定标识符以决定如何处理它。
- 当标识符匹配时,选择文件中的重要信息并使用它来适当地命名文件。
- 将文件移动到取决于标识符的目标文件夹。
设置文件监视器
由于我使用的 ABBYY(OCR 软件)的设置方式,文件在经过 OCR 处理后会被命名为 <prefix>_OCR.pdf,因此 FileSystemWatcher
对象如下设置:
FileSystemWatcher watcher = new FileSystemWatcher(watchFolder, "*_OCR.pdf");
watcher.NotifyFilter = NotifyFilters.LastWrite| NotifyFilters.FileName | NotifyFilters.DirectoryName;
watcher.Created += new FileSystemEventHandler(OnCreated);
watcher.EnableRaisingEvents = true;
在测试软件时,我经常发现由于标识符编写不当,文件会进入错误的目录并带有错误的文件名,为了能够重新处理文件而不考虑文件名,我还为 rematchFolder
设置了一个监视器,其过滤器仅设置为“*.pdf”。这样您就可以更改配置,然后将任何文件放入 rematch 文件夹,并使用新规则再次进行处理。
配置
有两个配置部分来运行服务。一个是 app.config,它指向输入文件夹、不匹配文件夹、规则配置文件所在的位置以及日志文件的存放位置。
规则配置存储在 XML 文件中,然后加载到一个 PDFTemplate
对象的列表(PDFTemplates
)中。 PDFTemplate
类仅包含:
identifiers
中的字符串列表,其中每个字符串都是一个正则表达式,并且必须匹配文件中的所有标识符才能触发规则。contentSelectors
中的字符串列表,其中每个字符串都是一个正则表达式,包含匹配组(在正则表达式中用“(...)”表示),第一个匹配到内容的选定器将用于重命名文件。fileNamePrefix
中的字符串,用于设置规则应将文件重命名到的文件名的前缀。destionationFolder
中的字符串,包含一个目录的完整路径,规则应将文件移动到该目录。
XML 文件因此如下所示:
<ArrayOfPDFTemplate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<PDFTemplate>
<identifiers>
<string>[Ss]ome company</string>
</identifiers>
<contentSelectors>
<string>\bInvoice date\W+(?:\w+\W+){0,20}?([0-9] *[0-9] *[0-9] *[0-9] *- *[0-9] *[0-9] *- *[0-9] *[0-9])\b</string>
<string>([0-9] *[0-9] *[0-9] *[0-9] *- *[0-9] *[0-9] *- *[0-9] *[0-9])</string>
</contentSelectors>
<fileNamePrefix>Some Company</fileNamePrefix>
<destinationFolder>C:\Sorted PDF Files\Some Company</destinationFolder>
</PDFTemplate>
...
</ArrayOfPDFTemplate>
运行匹配和重命名文件
现在,当一个文件被处理时,它会遍历 PDFTemplates
列表中所有对象的标识符,并针对第一个匹配项应用规则。如果根本没有匹配到任何规则,该文件将被移动(但不重命名)到一个指定的“noMatch”文件夹进行手动处理。
用于搜索文件中的标识符并进行重命名的代码如下:
...
//Extract all text from the PDF document
org.apache.pdfbox.pdmodel.PDDocument doc = org.apache.pdfbox.pdmodel.PDDocument.load(fullPath);
org.apache.pdfbox.util.PDFTextStripper stripper = new org.apache.pdfbox.util.PDFTextStripper();
text = stripper.getText(doc);
doc.close();
...
//Go through all identifiers, looking for a match
foreach (string identifier in thisTemplate.identifiers)
{
if (!Regex.IsMatch(text, identifier, RegexOptions.IgnoreCase))
{
identifiersFound = false;
break;
}
}
...
//Look for a matching contentselector
foreach (string contentSelector in thisTemplate.contentSelectors)
{
Match thisMatch = Regex.Match(text, contentSelector,
RegexOptions.IgnoreCase | RegexOptions.Multiline);
if (thisMatch.Captures.Count != 0)
{
string selection = thisMatch.Groups[1].Value;
newFileName = newFileName + "_" + selection;
break;
}
}
然后就剩下重命名和移动正在处理的文件的问题了。
关注点
这篇文章实际上并非旨在优雅地解决问题(代码需要一些重构才能做到这一点 - 它只是一个临时解决方案),而是如果您面临类似情况,它为您提供了一个起点。我曾真的试图寻找可以为我完成这项工作的软件,但在使用正则表达式和设置我自己的规则集合来应用于文件方面,我一无所获。