听说你还在苦恼PDF的填写,那你一定没有试过这篇文章--by.广州业务组
回答
收藏

听说你还在苦恼PDF的填写,那你一定没有试过这篇文章--by.广州业务组

莫奈
2023-12-01 11:29·浏览量:1396
莫奈
发布于 2023-12-01 10:48更新于 2023-12-01 11:291396浏览

分享人:莫奈

在日常工作中,我们会或多或少的和PDF打交道,又或者他已经是你工作中的一部分,但是我们如何让影刀更好地服务PDF文件嘞,比如我们如何实现自动化填写PDF表单,又如何利用PDF填写去衍生场景,最近我就面临了这样的问题,所以研究了一下这方面的解决方案,所以今天给大家带来的是应对PDF文件的专题,欢迎大家来分享自己的想法


1、对于PDF文件的内容填写

我们如何实现这类或者这类PDF的填写呢,A图所示的是已经携带了表单域的PDF文件,而B图所示的是没有携带表单域的PDF文件,接下来我将对这两个文件的处理方法分别进行讲解

A图

B图

首先对于A图这类下发下来就已经帮我们设置好表单域的PDF文件

方法一:我们可以使用PYPDF2对表单域进行读取,代码如下,使用方法get_form_text_fields()即可把PDF文件中的含有的文本表单域打印出来,返回的结果是一个字典类型,格式类型为{表单域名称: 当前值},目前PDF文件中都是空白,所以读取出来的内容为空,当然空白读取出来的值还可能是None,这里需要注意下

from PyPDF2 import PdfReader

pdf = PdfReader("测试.pdf")

fields = pdf.get_form_text_fields()
print(fields)

此时我们可以建立一个PDF输出对象PdfWriter,克隆PDF读取对象内容,使用方法update_page_form_field_values()对输出对象的文本表单域进行更新,然后我们查看一下我们输出的PDF对象,发现已经成功将”holle world”写入我们指定的文本表单域中

from PyPDF2 import PdfReader, PdfWriter, generic

pdf = PdfReader("测试.pdf") #读取PDF文件

fields = pdf.get_form_text_fields() #获取PDF文件上的文本表单域
print(fields)

fields.update({"undefined": 'holle world'}) #更新读取出来的表单域内容中,指定文本表单域的值

output = PdfWriter() #创建写入对象
print(fields)

output.clone_document_from_reader(pdf) #克隆源文件
output.update_page_form_field_values(output.pages[0], fields) #更新文本表单域的值

with open('output.pdf', 'wb') as f:
    output.write(f)

但是这里我们需要注意一下,上面也说过读取PDF文本表单域空白的部分,有一部分是空白的表单域但是读取出来的结果是None,如果我们直接去更新文本表单域会导致None被填写到新生成的文件中

所以如果其他地方需要保持空白的情况,我们需要加一步骤更新一下读出来的数据,将None替换成‘’

for field in fields:
    print(fields[field])
    if fields[field] is None:
        fields.update({field: ''})

方法二:我们利用pdfrw进行表单域的读取和填写,代码如下,读取PDF后通过访问Root.AcroForm.Fields可以获取PDF中的所有表单域字段,返回一个列表,其中每个元素都是一个字典,表示一个表单字段。然后循环该列表并通过.T拿到当前文本表单域的名称属性值,将其打印如图,我们即可拿到当前PDF文件中的表单域名称信息

import pdfrw
pdf = pdfrw.PdfReader("测试.pdf")

fields = pdf.Root.AcroForm.Fields

for field in fields:
    field_name = field.T
    print(field_name)

刚才也说了我们拿到的表单域字段列表,里面单个表单域信息时以字典形式存储的,我们单独打印undefined这一个表单域的结果看一下

我们将打印结果先格式化一下,结果如下:

{
	'/AP': {
		'/N': (685, 0)
	},
	'/DA': '(/Cour  0 Tf 0 g)',
	'/DR': {
		'/Encoding': {
			'/PDFDocEncoding': (871, 0)
		},
		'/Font': {
			'/Cour': (870, 0)
		}
	},
	'/F': '4',
	'/FT': '/Tx',
	'/Ff': '25165824',
	'/MaxLen': '25',
	'/P': {
		'/Annots': (800, 0),
		'/Contents': [(787, 0), (788, 0), (789, 0), (790, 0), (791, 0), (792, 0), (793, 0), (794, 0)],
		'/CropBox': ['0', '0', '595.32', '841.92'],
		'/Group': {
			'/CS': '/DeviceRGB',
			'/S': '/Transparency',
			'/Type': '/Group'
		},
		'/MediaBox': ['0', '0', '595.32', '841.92'],
		'/Parent': {
			'/Count': '4',
			'/Kids': [{ ...
			}, {
				'/Annots': [(213, 0), (212, 0), (211, 0), (210, 0), (209, 0), (208, 0), (207, 0), (206, 0), (205, 0), (204, 0), (203, 0), (202, 0), (201, 0), (200, 0), (199, 0), (198, 0), (197, 0), (196, 0), (195, 0), (194, 0), (193, 0), (192, 0), (191, 0), (224, 0), (223, 0), (222, 0), (221, 0), (220, 0), (219, 0), (218, 0), (217, 0)],
				'/Contents': (2, 0),
				'/CropBox': ['0', '0', '595.32', '841.92'],
				'/Group': {
					'/CS': '/DeviceRGB',
					'/S': '/Transparency',
					'/Type': '/Group'
				},
				'/MediaBox': ['0', '0', '595.32', '841.92'],
				'/Parent': { ...
				},
				'/Resources': {
					'/ExtGState': {
						'/GS7': (844, 0),
						'/GS8': (845, 0)
					},
					'/Font': {
						'/F1': (848, 0),
						'/F2': (851, 0),
						'/F3': (854, 0),
						'/F4': (860, 0),
						'/F5': (863, 0)
					},
					'/ProcSet': ['/PDF', '/Text', '/ImageB', '/ImageC', '/ImageI']
				},
				'/Rotate': '0',
				'/StructParents': '1',
				'/Tabs': '/S',
				'/Type': '/Page'
			}, {
				'/Annots': (184, 0),
				'/Contents': (4, 0),
				'/CropBox': ['0', '0', '595.32', '841.92'],
				'/Group': {
					'/CS': '/DeviceRGB',
					'/S': '/Transparency',
					'/Type': '/Group'
				},
				'/MediaBox': ['0', '0', '595.32', '841.92'],
				'/Parent': { ...
				},
				'/Resources': {
					'/ExtGState': {
						'/GS7': (844, 0),
						'/GS8': (845, 0)
					},
					'/Font': {
						'/F1': (848, 0),
						'/F2': (851, 0),
						'/F3': (854, 0),
						'/F4': (860, 0),
						'/F5': (863, 0)
					},
					'/ProcSet': ['/PDF', '/Text', '/ImageB', '/ImageC', '/ImageI']
				},
				'/Rotate': '0',
				'/StructParents': '2',
				'/Tabs': '/S',
				'/Type': '/Page'
			}, {
				'/Annots': (185, 0),
				'/Contents': (7, 0),
				'/CropBox': ['0', '0', '595.32', '841.92'],
				'/Group': {
					'/CS': '/DeviceRGB',
					'/S': '/Transparency',
					'/Type': '/Group'
				},
				'/MediaBox': ['0', '0', '595.32', '841.92'],
				'/Parent': { ...
				},
				'/Resources': {
					'/ExtGState': {
						'/GS7': (844, 0),
						'/GS8': (845, 0)
					},
					'/Font': {
						'/F1': (848, 0),
						'/F2': (851, 0),
						'/F3': (854, 0),
						'/F4': (860, 0),
						'/F5': (863, 0),
						'/F8': (261, 0)
					},
					'/ProcSet': ['/PDF', '/Text', '/ImageB', '/ImageC', '/ImageI']
				},
				'/Rotate': '0',
				'/StructParents': '3',
				'/Tabs': '/S',
				'/Type': '/Page'
			}],
			'/Type': '/Pages'
		},
		'/Resources': {
			'/ExtGState': {
				'/GS7': (844, 0),
				'/GS8': (845, 0)
			},
			'/Font': {
				'/F1': (848, 0),
				'/F2': (851, 0),
				'/F3': (854, 0),
				'/F4': (860, 0),
				'/F5': (863, 0),
				'/F6': (866, 0),
				'/F7': (869, 0)
			},
			'/ProcSet': ['/PDF', '/Text', '/ImageB', '/ImageC', '/ImageI']
		},
		'/Rotate': '0',
		'/StructParents': '0',
		'/Tabs': '/S',
		'/Type': '/Page'
	},
	'/Rect': ['139.92', '645.12', '565.2', '664.32'],
	'/Subtype': '/Widget',
	'/T': '(undefined)',
	'/TU': '(undefined)',
	'/Type': '/Annot',
	'/V': '()'
}

对于这一大长串,大概列了一下其中几项的大致含义供大家参考

/Type:注释类型(Annotation type)
/Subtype:注释子类型(Annotation subtype)
/P:所属页面(Page),它是一个字典对象,包含了页面的各种属性,如父级页面、资源等
/F:字段标志(Field flags),这是一个整数值,用于表示字段的各种属性
/T:文本表单域的名称(Text field name)
/FT:字段类型(Field type),对于文本表单域是/Tx
/DA:默认外观字符串(Default appearance string)
/Rect:文本表单域的位置和大小(Rectangle)
/AP:外观字典(Appearance dictionary)
/V:文本表单域的值(Value)
/MaxLen:允许输入的最大字符数(Max length)

其中我们需要用到的是/T来指定操作文本表单域的名称,通过更改/V来更新文本表单域携带的默认值,好了现在基本知识我们了解到这里,接下来先上代码,我们如何将”holle world”写入我们指定的文本表单域中

import pdfrw
pdf = pdfrw.PdfReader("测试.pdf")

fields = pdf.Root.AcroForm.Fields

for field in fields:
    field_name = field.T
    if field_name == '(undefined)': #判断是否是我们需要更新的文本表单域
        field.update(pdfrw.PdfDict(V=pdfrw.objects.pdfstring.PdfString('(holle world)')))
				#更新指定文本表单域的"/V"为我们需要的内容

writer = pdfrw.PdfWriter()
writer.write('output.pdf', pdf)

不过这里有几个点需要注意一下:

1、我们需要写入的内容需要放入’(holle world)’括号里面,如果没有括号,内容则无法写入到PDF的文本表单域中,这里我们需要留心一下

2、pdfrw更新文本表单域值对中文字段的更新,需要注意一下,这里是自己踩的坑记录一下:

这里我们如果直接将中文字段更新到文本表单域中写入,会产生如图报错

str1 = '您好'
field.update(pdfrw.PdfDict(V=pdfrw.objects.pdfstring.PdfString('('+str1+')')))

但是此时我们对需要写入的中文字段进行编码,然后在进行写入,代码如下

str1 = '您好'
str1 = str1.encode('utf-8').decode('Latin-1')
field.update(pdfrw.PdfDict(V=pdfrw.objects.pdfstring.PdfString('('+str1+')')))

此时我们编码一下后可以正常运行下去,但是写入的内容会乱码

于是,我们反向去操作一下,我先将我需要的中文字段写入到PDF文件中,再使用pdfrw进行读取看一下结果

然后我们将这串代码再回写到空白PDF文件中,我们就可以将中文字段“您好”写入PDF指定文本表单域中

str1 = 'þÿ`¨Y}'
field.update(pdfrw.PdfDict(V=pdfrw.objects.pdfstring.PdfString('('+str1+')')))

于是我们进行测试一下,我们对您好进行编码一下,得到结果`¨Y},我们不难发现对于上面我们正确写入您好的字段有一些偏差,þÿ`¨Y} —><þÿ>< `¨Y}>可以分为这两个字段,如果我们再测试一个”男“,先将它写入PDF文件中读取出来þÿu7,然后单独编码一下得到u7,我们可以发现写入中文的正确路子

str1 = "您好"
data = str1.encode('utf-16be').decode('Latin-1')

首先我们反编译一下þÿ,得到b'\xfe\xff

latin1_str = "þÿ"
str = latin1_str.encode('latin-1')

于是我们改写一下中文的写入代码如下:

str1 = '您好'
str1 = str1.encode('utf-16be')
str1 = b'\\xfe\\xff' + str1
str1 = str1.decode('Latin-1')
field.update(pdfrw.PdfDict(V=pdfrw.objects.pdfstring.PdfString('('+str1+')')))

拓展:

接下来我们可以衍生我们的方法到没有表单域的PDF文件,相信聪明的你看到这里已经明白我接下来要讲的内容了,其实表单域都是我们人为进行设置的,所谓设置好表单域的PDF文件其实就是某些特殊场景下,提前设置好了文本表单域来规定填写位置、大小和字号等,这类场景常见于签证登记表等等,也欢迎大家在评论区进行更多使用场景的分享;

所以对于这类没有表单域的PDF文件,我们可以通过第三方软件来设置表单域,来符合我们的填写规则,这样我们就可以不断去复用我们这个制作好的模板,去批量完成我们的PDF填写~

我们可以接着思考,如果我们有word或者其他的文件也有填写的需求,那是不是可以先把它转换成PDF的格式,然后去设置指定位置的表单域,来规定字体、大小或者颜色,这样就可以将这类需求实现自动化了~

比如这里我想批量去填写这样一个表单,那么我需要做的是先使用第三方软件去给我的PDF添加表单域,这里我使用的是WPS进行表单域的添加

举例我们先添加这几个表单域到我们的PDF文件中,我们对姓名、性别、身份证、联系电话以及一个勾选框,把文本表单域都设置好之后

我们使用影刀对代码进行一下封装之后,我们使用封装好的指令就可以愉快地进行PDF的填写

结果:

2.对于PDF文件中的各类勾选框操作

我们不难发现我们要操作的PDF文件中,其实不止填写框这个元素存在的,还有如下图所示的勾选框,那接下来就让我继续教学一下,面对这类勾选表单域,我们该如何去做吧

其实同样的,我们还是需要使用这两个库去操作,接下来我们开始对我们上面已经添加好表单域的PDF进行操作,来进行勾选勾选框的操作

方法一:PYPDF2操作PDF,无法实现勾选操作(有大佬了解的可以秀一波了),我们先使用PYPDF2对PDF进行读取,方法get_fields()即可把PDF文件中的含有的表单域打印出来,代码如下:

from PyPDF2 import PdfReader, PdfWriter, generic

pdf = PdfReader("测试2.pdf")

fields = pdf.get_fields()
print(fields)

{'name': {'/T': 'name', '/FT': '/Tx', '/TU': 'name', '/V': ''}, 'gender': {'/T': 'gender', '/FT': '/Tx', '/TU': 'gender'}, 'number': {'/T': 'number', '/FT': '/Tx', '/TU': 'number', '/V': ''}, 'phone': {'/T': 'phone', '/FT': '/Tx'}, 'radiobutton1': {'/T': 'radiobutton1', '/FT': '/Btn', '/TU': 'radiobutton1', '/V': '/Off'}}

我们会得到字典类型的返回参数,里面是我们目前PDF文件上含有的表单域字段,简单的解释如下

/T:文本表单域的名称(Text field name)
/FT:字段类型(Field type),对于文本表单域是/Tx,对于勾选表单域是/Btn
/TU: 表示添加表单域时加的提示词
/V:文本表单域的值(Value)

所以我们如果对勾选框操作,理论上我们需要将'radiobutton1': {'/T': 'radiobutton1', '/FT': '/Btn', '/TU': 'radiobutton1', '/V': '/Off'}中的’/V‘对应的值更改为’/On‘或者’/Yes‘,我们输出的代码如下:

from PyPDF2 import PdfReader, PdfWriter, generic, constants

pdf = PdfReader("测试2.pdf")

fields = pdf.get_fields()

for field_name in fields.keys():
    field = fields[field_name] #获取对应字段名称,判断是否为需要更改目标
    if field_name == 'name':
        field[generic.NameObject("/V")] = generic.TextStringObject("/Yes")

output = PdfWriter()

output.clone_document_from_reader(pdf)
output.update_page_form_field_values(output.pages[0], fields)

with open('output.pdf', 'wb') as f:
    output.write(f)

但实际运行下来会报错:Incorrect first char in NameObject:({'/T': 'radiobutton1', '/FT': '/Btn', '/TU': 'radiobutton1', '/V': '/Off'});这里我去查了一些资料,尝试更改了代码,但是并没有解决,看看论坛里有没有大佬来解密

方法二:pdfrw操作PDF进行勾选操作,这里推荐大家使用这个库进行操作,首先我们使用pdfrw库对PDF进行读取,同样使用Root.AcroForm.Fields可以获取PDF中的所有表单域字段,首先我们将我们需要操作的勾选框表单域进行读取查看,代码如下:

import pdfrw
pdf = pdfrw.PdfReader("测试2.pdf")

fields = pdf.Root.AcroForm.Fields

for field in fields:
    field_name = field.T
    if field_name == '(radiobutton1)':
        print(field)

结果:{'/AP': {'/D': {'/Off': (51, 0), '/Yes': (52, 0)}, '/N': {'/Off': (53, 0), '/Yes': (54, 0)}}, '/AS': '/Off', '/C': ['0.976471', '0.258824', '0.266667'], '/CA': '1', '/CreationDate': "(D:20231122204118+08'00')", '/DA': '(/ZaDb 0 Tf 0 g)', '/F': '4', '/FT': '/Btn', '/HighDpi': 'true', '/M': "(D:20231122204220+08'00')", '/MK': {}, '/NM': '({41bdf7b2-9639-4f95-ba56-31a24580a52b})', '/P': {'/Annots': [(11, 0), (12, 0), (13, 0), (14, 0), (15, 0)], '/Contents': (16, 0), '/MediaBox': ['0', '0', '595.3', '841.9'], '/Parent': {'/Count': '1', '/Kids': [{...}], '/Type': '/Pages'}, '/Resources': {'/ExtGState': {'/GS13': (17, 0), '/KSPE2': (18, 0)}, '/Font': {'/FT14': (19, 0), '/FT19': (20, 0), '/FT24': (21, 0), '/FT29': (22, 0), '/FT8': (23, 0)}, '/XObject': {'/KSPX1': (24, 0)}}, '/Type': '/Page'}, '/Rect': ['159.907', '636.473', '167.345', '644.067'], '/Subtype': '/Widget', '/T': '(radiobutton1)', '/TU': '(radiobutton1)', '/Type': '/Annot', '/V': '/Off'}

这里我们得到了字段信息,我们需要进行勾选的操作,其实也是对属性’/V‘进行操作,这里我们需要注意一点,刚才我也说了我们需要将’/V‘对应的值更改为’/On‘或者’/Yes‘,那究竟需要改成那个值呢,这里我们就需要看一下{'/AP': {'/D': {'/Off': (51, 0), '/Yes': (52, 0)}这里了,比如这里的AP外观字典里面会告诉我们不勾选时是’/Off‘、勾选则是’/Yes‘,所以我们这里需要勾选勾选框,那么就需要把’Off‘改为’/Yes‘,代码如下:

import pdfrw
pdf = pdfrw.PdfReader("测试2.pdf")

fields = pdf.Root.AcroForm.Fields
write_value = 'Set' #Set:勾选 NoSet:不勾选
keySet = ''

for field in fields:
    field_name = field.T
    if field_name == '(radiobutton1)':
        for k in field['/AP']['/D']:
            if k != '/Off':
                keySet = k
        if write_value == 'Set':
            field.update(pdfrw.PdfDict(V=pdfrw.objects.pdfstring.PdfString(keySet),
                                       AS=pdfrw.objects.pdfstring.PdfString(keySet)))
        if write_value == 'NoSet':
            field.update(pdfrw.PdfDict(V=pdfrw.objects.pdfstring.PdfString('/Off'),
                                       AS=pdfrw.objects.pdfstring.PdfString('/Off')))

writer = pdfrw.PdfWriter()
writer.write('output.pdf', pdf)

然后我们查看一下结果,打开我们输出的PDF文件,我们可以看到我们已经完成了勾选表单域的勾选操作

那么到这里就结束了,不知道今天的专题是否能够帮助大家去解决问题,又或者给大家带来一定的启发,感谢大家阅读至此,让我们下次再见吧~


收藏2
全部回答1
最新
发布回答
回答