

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

结果:

我们不难发现我们要操作的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文件,我们可以看到我们已经完成了勾选表单域的勾选操作

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