如何在提取文本之前识别可能损坏的 pdf 页面? 基于pdftotext的快速bash函数

问题描述

TL;DR

我的工作流程:

  1. 下载PDF
  2. 使用 pdftk 将其拆分为页面
  3. 使用 pdftotext 提取每个页面的文本
  4. 对文本进行分类添加元数据
  5. 结构化格式发送给客户

我需要提取一致的文本才能从3跳转到4。如果文本出现乱码,我必须对其页面进行OCR。但是,OCR 所有页面都不成问题。如何事先确定哪些页面应该进行 OCR?我尝试在每个页面上运行 pdffontspdftohtml一个页面运行两次 subprocess.run 是不是很贵?

页面损坏是什么意思?

无法从其来源提取文本的 PDF 页面,可能是由于 to_unicode 转换。

说明

我正在构建一个依赖于每天从一千个 PDF 文件提取文本的应用程序。每个 PDF 中的文本布局都有些结构化,因此在大多数情况下从 python 调用 pdftotext 效果很好。但是,来自一两个资源的某些 PDF 文件会带来带有问题字体的页面,从而导致文本出现乱码。我认为只在有问题的页面上使用 OCR 可以解决这个问题。所以,我的问题是如何在提取文本之前识别哪些页面可能会导致乱码

首先,我尝试在提取后使用正则表达式(\p{Cc} 或拉丁字母表之外的不太可能的字符)识别乱码文本,但它不起作用,因为我发现带有有效字符和数字的损坏文本,即 { {1}},以及。

其次,我尝试识别每个页面调用 pdffonts 的乱码文本 - 识别名称、编码、嵌入性和 to_unicode 映射的存在 - 并解析其输出。在我的测试中,它运行良好。但我发现还需要计算有多少字符使用了可能有问题的字体,pdftohtml - 在 AAAAABS12 54c] $( JJJJ Pk 标签显示每个文本块及其字体名称 - 在这里节省了一天。 @LMC 帮助我弄清楚如何去做,看看 answer。不好的部分是我最终为每个 pdf 页面调用了两次 p,这是非常昂贵的。 It would be cheaper if I could just bind those tools

我想知道查看 PDF 源代码并验证某些 CMAP(subprocess.run 是,不是自定义字体)(如果存在)或其他启发式方法提取文本之前查找有问题的字体是否可行且可行或OCR。

我的一个 PDF 文件中出现乱码的示例:

uni

以上文本是使用 pdftotext 从本 document 的第 25 页中提取的。

对于该页面,pdffonts 输出

0\n1\n2\n3\n4\n2\n0\n3\n0\n5 6\n6\nÿ\n89 ÿ\n4\n\x0e\n3\nÿ\n\x0f\x10\n\x11\n\x12\nÿ\n5\nÿ\n6\n6\n\x13\n\x11\n\x11\n\x146\n2\n2\n\x15\n\x11\n\x16\n\x12\n\x15\n\x10\n\x11\n\x0e\n\x11\n\x17\n\x12\n\x18\n\x0e\n\x17\n\x19\x0e\n\x1a\n\x16\n2 \x11\n\x10\n\x1b\x12\n\x1c\n\x10\n\x10\n\x15\n\x1d29 2\n\x18\n\x10\n\x16\n89 \x0e\n\x14\n\x13\n\x14\n\x1e\n\x14\n\x1f\n5 \x11\x1f\n\x15\n\x10\n! \x1c\n89 \x1f\n5\n3\n4\n"\n1\n1\n5 \x1c\n89\n#\x15\n\x1d\x1f\n5\n5\n1\n3\n5\n$\n5\n1 5\n2\n5\n%8&&#\'#(8&)\n*+\n\'#&*,\nÿ\n(*ÿ\n-\n./0)\n1\n*\n*//#//8&)\n*ÿ\n#/2#%)\n*,\nÿ\n(*/ÿ\n/#&3#40)\n*/ÿ\n#50&*-\n.()\n%)\n*)\n/ÿ\n+\nÿ\n*#/#\n&\x19\n\x12\nÿ\n\x1cÿ\n,\x1d\n\x12\n\x1b\x10\n\x15\n\x116\nÿ\n\x15\n7\nÿ\n8\n9\n4\n6\nÿ\n%\x10\n\x15\n\x11\n\x166\nÿ\n:\x12\x10;\n2\n*,\n%#26\nÿ\n<\n$\n3\n0\n3\n+\n3\n8\n3\nÿ\n+\nÿ\n=\x15\n\x10\n6\nÿ\n>\n9\n0\n?\nÿ\n4\n3\n3\n1\n+\n8\n9\n3\n<\n@A\nB\nC\nD\nEÿ\nGH\nI\nÿ\nJ\nJ\nK\nL\nJ\nM\nJ\nN\nO\nP\nO\nQ\nI\n#\x1bÿ\n0\n1\nÿ\n\x1c\n\x10\nÿ\n*\x1a\n\x16\n\x18\nÿ\n\x1c\n\x10\nÿ\n0\n3\n0\n5\n\x0e\n/\x10\n\x15\n\x13\x16\n\x12\nÿ\n/\x10\n\x16\n\x1d\x1c\x16\n\x12\n6\nÿ\n* \x19\n\x15\n\x116\nÿ\n\x12\n\x19\n\x11\n\x19\n\x12\n\x16\nÿ\n\x15ÿ\n/*-\n\x0e\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\n(\x10\nÿ\x16\n\x1c\n\x10\n\x1bÿ\n\x1c\n\x12\nÿ\n%\x13\n\x10\n9\n\x10\nÿ\n\x1c\n\x10\nÿ\n\'\x12\n\x1a\x15\n\x10\n\x11\n\x10\nÿ\n\x1c\n\x12\nÿ\n%\x16\n\x16\n\x10\nR\n\x10\n\x1c\x16\n\x12\nÿ\n\'\x10\n\x16\n\x12\n\x18\nÿ\n\x1c\n\x12\nÿ\n-\n\x19\x11\n1\n\x12\nÿ\n\x1cÿ\n#\x11\n\x12\n\x1cÿ\n\x1c\n\x10\nÿ\n*\x18\n\x12\nR\x126\nÿ\n/\x16\n\x12\n\x0e\n& \x10\n\x12\n\x15\n\x12\nÿ\n%\x10\n\x18\x11\n\x16\n\x10\nÿ\n:\x12\x13\n\x12\n\x1c\x0e\nÿ\n*\x19\n\x11\n\x19\n\x10\n+\x10\nÿ\n\x10\nÿ\n&\x10\nR\x11\n\x16\n\x10\n+\x10\nÿ\n\x15ÿ\n/*-\n2\n2\'<\nÿ\n+\nÿ\n#S\n\x11\n\x16\n\x12\n\x17\n\x19\n\x1c \x12\n\x18\nÿ\n*\x1c\n\x1b\x15\x11\n\x16\n\x12\n\x11\n\x1d\x0e\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\nÿ\n*\x11\n\x10\n\x15 \x12\n\x1b\x10\n\x15\n\x11\n\x10\n6\nTU\nV\nWU\nXÿ\nYXÿ\nTU\nV\nW\nX\nXYZU\n[U\nT\\]X\\U\nW\nX\nVD\n^\n_\n`\nÿ\nab\nÿ\nXGb\nc\nE^\nd\nO\nP\nO\nQ\nP\ne\nO\nf\nP\nf\nJ\nf\nP\ne\ng\nGb\nh_\nEGI\niaA\nYjTk\nXlm@ YjTk\nXlmX] ]jTk@[Yj] U\nZk]U\nZU\n] X]noU\nW\nX] W@V\n\\\nX]\nÿ\n89\nÿ\n89\np ÿ\nq\n(\x10\x14\n\x12\x13\n8r\nIOV\x11\x03\x14\n(VWH\x03GRFXPHQWR\x03p\x03FySLD\x03GR\x03RULJLQDO\x03DVVLQDGR\x03GLJLWDOPHQWH\x03SRU\x03(00$18(/$\x030$5,$\x03&$/$\'2\x03\'(\x03)$5,$6\x036,/9$\x11\x033DUD\x03FRQIHULU\x03R\x03RULJLQDO\x0f\x03DFHVVH\x03R\x03VLWH\x03\x0f\x03LQIRUPH\x03R\x03SURFHVVR\x03\x13\x13\x13\x13\x16\x17\x18\x10\x1a\x18\x11\x15\x13\x15\x14\x11\x1b\x11\x13\x15\x11\x13\x13\x1a\x16\x03H\x03R\x03\nFyGLJR\x03\x17(\x14\x14\x16\x14\x13\x11\x03

很容易将 name type encoding emb sub uni object ID ------------------------------------ ----------------- ---------------- --- --- --- --------- [none] Type 3 Custom yes no no 13 0 DIIDPF+ArialMT CID TrueType Identity-H yes yes yes 131 0 DIIEDH+Arial CID TrueType Identity-H yes yes no 137 0 DIIEBG+TimesNewRomanPSMT CID TrueType Identity-H yes yes yes 142 0 DIIEDG+Arial CID TrueType Identity-H yes yes no 148 0 Arial TrueType WinAnsi yes no no 159 0 命名的字体识别为有问题。到目前为止,鉴于我分析的数据,我的看法是使用自定义或身份 h 编码标记字体,没有 to_unicode 映射或没有命名为可能有问题的字体。但是,正如我所说,我也发现使用 ToUnicode 表而不是自定义编码字体的情况也有问题。据我所知,也可以找到,例如,为损坏的字体定义的单个字符,但不影响页面的整体可读性,因此可能不需要对该页面进行 OCR。换句话说,如果给定页面中的字体没有ToUnicode转换,并不意味着该页面的文本完全受到影响。

我正在寻找比正则表达式乱码更好的解决方案。

我必须进行 OCR 的 PDF 页面示例

以下所有页面都包含葡萄牙语文本,但如果您尝试复制文本并粘贴到某处,您会看到普遍的胡言乱语。

到目前为止我做了什么

因为我创建了一个 bash 脚本来迭代页面并将每个页面的 pdftohtml 和 pdffonts 输出合并到一个 HTML 中,所以我避免在一个页面调用两次子进程:

[none]

上面的代码使我能够调用一次子进程并使用 #!/bin/sh # Usage: ./font_report.sh -a 1 -b 100 -c foo.pdf while getopts "a:b:c:" arg; do case $arg in a) FirsT_PAGE=$OPTARG;; b) LAST_PAGE=$OPTARG;; c) FILENAME=$OPTARG;; *) echo 'Error: invalid options' >&2 exit 1 esac done : ${FILENAME:?Missing -c} if ! [ -f "$FILENAME" ]; then echo "Error: $FILENAME does not exist" >&2 exit 1 fi echo "<html xmlns='http://www.w3.org/1999/xhtml' lang='' xml:lang=''>" ; for page in $(seq $FirsT_PAGE $LAST_PAGE) do { echo "<page number=$page>" ; echo "<pdffonts>" ; pdffonts -f $page -l $page $FILENAME ; echo "</pdffonts>" ; ( pdftohtml -f $page -l $page -s -i -fontfullname -hidden $FILENAME -stdout | tail -n +35 | # skips head tag and its content head -n -1 # skips html ending tag ) ; echo "</page>" } done echo "</html>" 为每个页面解析 html(考虑 lxml 标记)。但是还是需要看文字内容才知道是不是文字坏了。

解决方法

基于pdftotext的快速函数

这是一个完整的(重写)功能扫描坏页:

#!/bin/bash

findBadPages() {
    local line opts progress=true usage="Usage: ${FUNCNAME[0]} [-f first page]"
    usage+=' [-l last page] [-m min bad/page] [-q (quiet)]'
    local -a pdftxt=(pdftotext -layout - -)
    local -ia badpages=()
    local -i page=1 limit=10 OPTIND
    while getopts "ql:f:m:" opt;do
        case $opt in
            f ) pdftxt+=(-f $OPTARG); page=$OPTARG ;;
            l ) pdftxt+=(-l $OPTARG) ;;
            m ) limit=$OPTARG ;;
            q ) progress=false ;;
            * ) printf >&2 '%s ERROR: Unknown option!\n%s\n' \
                           "${FUNCNAME[0]}" "$usage";return -1 ;;
        esac
    done
    shift $((OPTIND-1))
    while IFS= read -r line; do
        [ "$line" = $'\f' ] && page+=1 && $progress && printf %d\\r $page
        ((${#line} > 1 )) && badpages[page]+=${#line}
    done < <(
        tr -d '0-9a-zA-Z\047"()[]{}<>,-./+?!$&@#:;%$=_ºÁÃÇÔàáâãçéêíóôõú– ' < <(
            "${pdftxt[@]}" <"$1"
    ))
    for page in ${!badpages[@]} ;do
        (( ${badpages[page]} > limit )) && {
            $progress && printf "There are %d strange characters in page %d\n" \
               ${badpages[page]} $page || echo $page ;}
    done
}

那么现在:

findBadPages DJE_3254_I_18062021\(1\).pdf
There are 2237 strange characters in page 23
There are 258 strange characters in page 24
There are 20 strange characters in page 32

findBadPages -m 100 -f 40 -l 100 DJE_3254_I_18062021.pdf 
There are 623 strange characters in page 80
There are 1068 strange characters in page 81
There are 1258 strange characters in page 82
There are 1269 strange characters in page 83
There are 1245 strange characters in page 84
There are 256 strange characters in page 85

findBadPages DJE_3254_III_18062021.pdf
There are 11 strange characters in page 125
There are 635 strange characters in page 145

findBadPages -qm100 DJE_3254_III_18062021.pdf 
145

findBadPages -h
/bin/bash: illegal option -- h
findBadPages ERROR: Unknown option!
Usage: findBadPages [-f first page] [-l last page] [-m min bad/page] [-q (quiet)]

用法:

findBadPages [-f INTEGER] [-l INTEGER] [-m INTEGER] [-q] <pdf file>

哪里

  • -f 让您指定第一页
  • -l 用于最后一页
  • -m 表示每页找到的最少 奇怪 个字符来打印状态。
  • -q 标志在进程中禁止显示页码,然后只显示坏页 编号。

注意

tr -d 使用的字符串:0-9a-zA-Z\047"()[]{}<>,-./:;%$=_ºÁÃÇÔàáâãçéêíóôõú– 是通过对 PDF 文件中使用的字符进行排序而构建的!他们无法匹配另一种语言!也许在未来的某些用途中,可能需要添加一些重音字符或其他遗漏的可打印字符。

,

@mkl 可能会说使用单词字典搜索的方法

我尝试了不同的方法来查看在您最小的第三个示例中检测两个坏页的一些简单方法,并怀疑它很容易被具有好的和坏文本的页面击败,因此这几乎可以肯定不是一个完整的答案需要更多的传球来完善。

我们必须接受您提出的问题,即每个 PDF 的质量未知,但需要进行处理。所以接受我们希望大多数页面都很好的事实,我们盲目地度过了爆发阶段。

一个非常常见的单词结构可以包含 3 个字母的音节“est” 因此,如果我们搜索突发文件,我们会发现 Page 23 和 Page 24 中缺少这些文件,因此它们很可能是损坏的候选对象。

enter image description here

同样,对于 855 页的文件,您说 Page 146 是一个问题(通过我之前的仅损坏页面的搜索方法确认,包含���,只是那个已损坏)但现在我们可以很容易地看到前 40 页

页面当然也需要OCR(包括那些只有图像的

第 4、5、8、9、10、35 页(35 是一个很奇怪的页面?BG 图像?)

但是我们得到了 2 个第 19、33 页的误报(确实有文本但没有 est 或 EST)

和 20,32,38 有 EST 所以搜索需要不区分大小写

SO 使用不区分大小写的搜索无需修改我们应该得到 95% 的置信度(40 次中有 2 次错误)需要 OCR 但我没有深入测试为什么小写 est 只返回 275从总数 855 中删除,除非稍后有非常高百分比的图像需要 OCR。

我之前曾建议通过查找 ??????? 更快地搜索第三个 6054 页面文件。这给了我们一个更不稳定的使用结果,但确实显示损坏是从 25 到 85 的所有文本页面

那会导致什么?

实际上很少有人使用???

损坏的页面通常包含 ???或 ���

在葡萄牙语中,损坏的页面不太可能包含 /I est

部分损坏的页面可能包含用于 OCR 的大图像,并且est 与否

一些损坏的页面将不是上述情况

,

由于这也是(或主要是)性能问题,我建议将您的代码修改为更多的多线程解决方案或简单地使用 GNU Parallel

关于它的好文章 -> link

,

尝试使用其他模块以正确提取文本;我建议使用 PyPDF2。

这是一个可以解决问题的函数:

import PyPDF2

def extract_text(filename,page_number):
    # Returns the content of a given page
    pdf_file_object = open(filename,'rb')
    pdf_reader = PyPDF2.PdfFileReader(pdf_file_object)
    # page_number - 1 below because in python,page 1 is considered as page 0
    page_object = pdf_reader.getPage(page_number - 1)
    text = page_object.extractText()
    pdf_file_object.close()
    return text

顺便说一下,PyPDF2 不是 Python 中的预装模块。要安装它,请安装 pip(尽管这点很可能已经完成)并通过命令行运行“pip install PyPDF2”。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...