PyPDF如何将多个PDF文件合并为一个PDF?

35
如果我有1000多个PDF文件需要合并成一个PDF,
from PyPDF2 import PdfReader, PdfWriter

writer = PdfWriter()

for i in range(1000):
    filepath = f"my/pdfs/{i}.pdf"
    reader = PdfReader(open(filepath, "rb"))
    for page in reader.pages:
        writer.add_page(page)

with open("document-output.pdf", "wb") as fh:
    writer.write(fh)

执行上述代码时,当 reader = PdfReader(open(filepath, "rb")) 时,会出现以下错误信息:

IOError: [Errno 24] Too many open files:

我认为这可能是一个 bug,如果不是,我该怎么办?

5个回答

75

最近我遇到了完全相同的问题,所以我深入研究了PyPDF2,看看出了什么问题,以及如何解决它。

注意:我假设filename是一个格式良好的文件路径字符串。我的所有代码都假设相同。

简短回答

使用PdfFileMerger()类,而不是PdfFileWriter()类。 我已经尽可能提供了以下内容,以尽量与您的内容相似:

from PyPDF2 import PdfFileMerger, PdfFileReader

[...]

merger = PdfFileMerger()
for filename in filenames:
    merger.append(PdfFileReader(file(filename, 'rb')))

merger.write("document-output.pdf")

长篇回答

你使用的 PdfFileReaderPdfFileWriter 的方式会使每个文件保持打开状态,最终导致 Python 生成 IOError 24。更具体地说,当你向 PdfFileWriter 添加页面时,你正在向打开的 PdfFileReader 中添加页面的引用(因此如果关闭文件,则会出现 IO 错误)。Python 检测到文件仍然被引用,并且不进行任何垃圾回收/自动文件关闭,尽管重新使用文件句柄。它们一直保持打开状态,直到 PdfFileWriter 不再需要访问它们,即在你的代码中的 output.write(outputStream) 处。

为了解决这个问题,需要在内存中创建内容的副本,并允许文件关闭。我在 PyPDF2 代码中的探险中注意到 PdfFileMerger() 类已经具有此功能,因此我选择使用它而不是重新发明轮子。但是,我发现我的初步查看 PdfFileMerger 不够仔细,它只在特定条件下创建副本。

我的初始尝试如下,结果导致相同的 IO 问题:

merger = PdfFileMerger()
for filename in filenames:
    merger.append(filename)

merger.write(output_file_path)

查看 PyPDF2 源代码,我们发现 append() 要求传递 fileobj,然后使用 merge() 函数,并将其最后一页作为新文件的位置传入。在使用 PdfFileReader(fileobj) 打开之前,merge()fileobj 执行以下操作:

    if type(fileobj) in (str, unicode):
        fileobj = file(fileobj, 'rb')
        my_file = True
    elif type(fileobj) == file:
        fileobj.seek(0)
        filecontent = fileobj.read()
        fileobj = StringIO(filecontent)
        my_file = True
    elif type(fileobj) == PdfFileReader:
        orig_tell = fileobj.stream.tell()   
        fileobj.stream.seek(0)
        filecontent = StringIO(fileobj.stream.read())
        fileobj.stream.seek(orig_tell)
        fileobj = filecontent
        my_file = True
我们可以看到append()选项确实接受字符串,当这样做时,它会假定它是一个文件路径,并在该位置创建一个文件对象。最终结果与我们试图避免的完全相同。然而,如果我们在将其传递到append()之前,将该路径字符串转换为文件对象或PdfFileReader(见编辑2)对象中的一个,它将自动为我们创建一个副本作为StringIO对象,从而允许Python关闭该文件。

我建议使用更简单的merger.append(file(filename,'rb')),因为其他人报告说PdfFileReader对象可能会保留在内存中,即使调用了writer.close()

希望这有所帮助! 编辑: 我假设您正在使用PyPDF2,而不是PyPDF。 如果您不是,则强烈建议您进行切换,因为PyPDF已不再得到维护,作者正式批准Phaseit开发PyPDF2。
如果由于某些原因您无法切换到PyPDF2(许可证,系统限制等),则PdfFileMerger将无法使用。 在这种情况下,您可以重用PyPDF2的merge函数中的代码(如上所述)将文件的副本创建为StringIO对象,并在您的代码中使用它来代替文件对象。 编辑 2: 基于评论(感谢@Agostino),之前推荐使用merger.append(PdfFileReader(file(filename, 'rb')))的建议已更改。


1
我说实话,我没有看长答案。不过短答案很好。 - brad-tot
2
我注意到在使用writer.append(PdfFileReader(file(filename, 'rb'))) 创建中间PdfFileReader对象时,无法删除某些文件。即使调用writer.close()后,它们仍然被锁定。而使用更简单的merger.append(file(filename, 'rb'))则似乎没有同样的问题。 - Agostino
1
如果文件太大,这样做不会遇到内存问题吗? - Nishant
1
@Rejected 好的,谢谢。我曾经看到过一个选择命名临时文件和内存的小型实用函数,这是一个不错的解决方案。 - Nishant
2
@Rejected 我相信对于Python 3,你需要使用open而不是filemerger.append(PdfFileReader(file(filename, 'rb'))). 就像这样 merger.append(PdfFileReader(open(filename, 'rb'))). - Hiebs915
显示剩余6条评论

3

pdfrw软件包一次性读取每个文件,因此不会遇到太多打开文件的问题。这里是一个拼接脚本的示例

相关部分--假设inputs是输入文件名的列表,outfn是输出文件名:

from pdfrw import PdfReader, PdfWriter

writer = PdfWriter()
for inpfn in inputs:
    writer.addpages(PdfReader(inpfn).pages)
writer.write(outfn)

免责声明:我是pdfrw的主要作者。

3
我已经编写了以下代码来帮助回答:-
import sys
import os
import PyPDF2

merger = PyPDF2.PdfFileMerger()

#get PDFs files and path

path = sys.argv[1]
pdfs = sys.argv[2:]
os.chdir(path)


#iterate among the documents
for pdf in pdfs:
    try:
        #if doc exist then merge
        if os.path.exists(pdf):
            input = PyPDF2.PdfFileReader(open(pdf,'rb'))
            merger.append((input))
        else:
            print(f"problem with file {pdf}")

    except:
            print("cant merge !! sorry")
    else:
            print(f" {pdf} Merged !!! ")

merger.write("Merged_doc.pdf")

我在这里使用了PyPDF2.PdfFileMerger和PyPDF2.PdfFileReader,而不是将文件名显式转换为文件对象。


2
问题在于您同一时间只能打开一定数量的文件。虽然有方法可以更改此限制(http://docs.python.org/3/library/resource.html#resource.getrlimit),但我认为您不需要这样做。
您可以尝试在for循环中关闭文件:
input = PdfFileReader()
output = PdfFileWriter()
for file in filenames:
   f = open(file, 'rb')
   input = PdfFileReader(f)
   # Some code
   f.close()

2
如果使用f.close(),exec output.write(outputStream),会提示IO错误。 - daydaysay

0

可能你打开了太多的文件。你可以在循环中明确地使用f=file(filename) ... f.close(),或者使用with语句,以便每个打开的文件都能被正确关闭。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接