Создание и изменение PDF-файлов в Python

Оглавление

Узнать, как в Python можно создавать и модифицировать PDF-файлы, очень и очень полезно. PDF (Portable Document Format) — это один из самых распространенных форматов для документов в интернете. Файлы в формате PDF могут содержать текст, изображения, таблицы, разнообразные формы, а также различные мультимедийные форматы видео и анимации. Причем все это может быть вместе, в одном файле.

Подобное изобилие создает определенные трудности в работе с PDF-файлами. Ведь для открытия файла нужно декодировать множество различных типов данных! К счастью, в экосистеме Python есть несколько отличных пакетов для чтения, изменения и создания PDF- файлов.

Из этого руководства вы узнаете как:

  • Считывать текст из PDF-файла
  • Делить PDF-файл на несколько других
  • Объединять PDF-файлы
  • Вращать и обрезать страницы в PDF-файлах
  • Шифровать и дешифровать PDF-файлы паролем
  • Создавать PDF-файл с нуля.

Примеры кода и PDF-файлы, которые мы используем в данном руководстве, можно загрузить отсюда.

Извлечение текста из PDF-файла

В этом разделе вы узнаете, как читать PDF-файл и извлекать из него текст при помощи пакета PyPDF2. Однако, для начала его надо установить при помощи менеджера pip.

$ python3 -m pip install PyPDF2

Проверить результат установки можно при помощи следующей команды:

$ python3 -m pip show PyPDF2
Name: PyPDF2
Version: 1.26.0
Summary: PDF toolkit
Home-page: http://mstamy2.github.com/PyPDF2
Author: Mathieu Fenniak
Author-email: biziqe@mathieu.fenniak.net
License: UNKNOWN
Location: c:\\users\\david\\python38-32\\lib\\site-packages
Requires:
Required-by:

Обратите особое внимание на версию пакета. На момент написания статьи последней версией пакета PyPDF2 была 1.26.0. Если вы используете IDE IDLE, для работы с только что установленным пакетом ее надо перезапустить.

Открываем PDF-файл

Начнем с того, что откроем PDF-файл и считаем из него некоторую информацию. Мы будем использовать файл Pride_and_Prejudice.pdf, расположенный в директории practice_files/ репозитория, ссылку на который мы дали выше.

Импортируем класс PdfFileReader из пакета PyPDF2:

from PyPDF2 import PdfFileReader

Для создания нового экземпляра класса PdfFileReader нам потребуется информация о пути к файлу, который мы хотим открыть. Будем использовать для этого модуль pathlib.

from pathlib import Path
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "Pride_and_Prejudice.pdf"
)

Сейчас переменная pdf_path содержит путь к файлу Pride_and_Prejudice.pdf, в котором находится книга Джейн Остин «Гордость и предубеждение».

Примечание: возможно вам придется изменить этот путь в соответствии с расположением директории creating-and-modifying-pdfs/ на вашем компьютере.

Теперь мы создаем экземпляр класса PdfFileReader:

pdf = PdfFileReader(str(pdf_path))

Нам пришлось конвертировать содержимое переменной pdf_path в строку, так как класс PdfFileReader не знает, как работать с объектами pathlib.Path.

Вспомним, что в Python все открытые файлы должны быть закрыты до завершения работы программы. Так вот, объект PdfFileReader все это делает за вас, и вам не нужно беспокоиться об открытии и закрытии файлов!

Заметим, что создав экземпляр класса PdfFileReader, мы можем его использовать для получения информации о PDF-файле. Например, метод .getNumPages () возвращает количество страниц, содержащихся в файле PDF:

pdf.getNumPages()
# результат: 234

Заметим, что метод .getNumPages() написан в так называемом «верблюжем регистре» (lowerCamelCase или, иначе говоря, MixedCase), в то время как PEP8 рекомендует «змеиный регистр» (примерно так — lower_case_with_underscores). Верблюжий регистр PEP8 советует использовать в именах классов. Но имейте в виду, что PEP8 это свод рекомендаций, а не правил. Как бы то ни было, верблюжий регистр здесь вполне приемлем.

Примечание: Пакет PyPDF2 вышел из пакета pyPdf, который был написан в 2005 году, всего через 4 года после публикации PEP8. К этому времени появилось много программистов, которые мигрировали из других языков, где верблюжий регистр был обычным делом.

Также мы можем получить некоторую информацию о документе с помощью атрибута .documentInfo:

pdf.documentInfo
{'/Title': 'Pride and Prejudice, by Jane Austen', '/Author': 'Chuck',
'/Creator': 'Microsoft® Office Word 2007',
'/CreationDate': 'D:20110812174208', '/ModDate': 'D:20110812174208',
'/Producer': 'Microsoft® Office Word 2007'}

Объект, возвращенный атрибутом .documentInfo, выглядит как словарь, но это не совсем так. Все его значения можно также получить атрибутивным образом, обратившись к ним через точку (‘.‘).

Например, чтобы получить заголовок, воспользуемся атрибутом .title:

pdf.documentInfo.title
'Pride and Prejudice, by Jane Austen'

Объект, полученный при помощи атрибута .documentInfo, содержит метаданные PDF-файла, которые задаются при его создании.

Класс PdfFileReader содержит все методы и атрибуты, необходимые вам для доступа к данным PDF-файла. Давайте рассмотрим, что и как можно сделать с этим файлом!

Извлекаем текст из страницы

Страницы PDF-файла представлены в пакете PyPDF2 классом PageObject. Мы используем экземпляры класса PageObject для взаимодействия со страницами в PDF-файле. Но нам не нужно непосредственно создавать собственные экземпляры класса PageObject. Вместо этого к ним можно получить доступ через метод .getPage() объекта класса PdfFileReader.

Извлечь текст из единичной страницы PDF-файла можно в два шага:

  1. Получаем экземпляр класса PageObject при помощи метода PdfFileReader.getPage().
  2. Извлекаем текст в виде строки с помощью метода .extractText(), который есть у каждого экземпляра класса PageObject.

В файле Pride_and_Prejudice.pdf 234 страницы. Каждая страница проиндексирована от 0 до 233. Мы можем получить экземпляр класса PageObject, представляющий конкретную страницу, передав индекс страницы в PdfFileReader.getPage():

first_page = pdf.getPage(0)

Метод .getPage() возвратит экземпляр класса PageObject:

type(first_page)
# результат <class 'PyPDF2.pdf.PageObject'>

Теперь мы можем извлечь текст при помощи метода PageObject.extractText():

first_page.extractText()

'\\n \\nThe Project Gutenberg EBook of Pride and Prejudice, by Jane
Austen\\n \\n\\nThis eBook is for the use of anyone anywhere at no cost
and with\\n \\nalmost no restrictions whatsoever.  You may copy it,
give it away or\\n \\nre\\n-\\nuse it under the terms of the Project
Gutenberg License included\\n \\nwith this eBook or online at
www.gutenberg.org\\n \\n \\n \\nTitle: Pride and Prejudice\\n \\n
\\nAuthor: Jane Austen\\n \\n \\nRelease Date: August 26, 2008
[EBook #1342]\\n\\n[Last updated: August 11, 2011]\\n \\n \\nLanguage:
Eng\\nlish\\n \\n \\nCharacter set encoding: ASCII\\n \\n \\n***
START OF THIS PROJECT GUTENBERG EBOOK PRIDE AND PREJUDICE ***\\n \\n
\\n \\n \\n \\nProduced by Anonymous Volunteers, and David Widger\\n
\\n \\n \\n \\n \\n \\n \\nPRIDE AND PREJUDICE \\n \\n \\nBy Jane
Austen \\n \\n\\n \\n \\nContents\\n \\n'

Заметим, что вывод на экран был отформатирован при размещении на этой странице. То, что вы увидите на своем экране, может несколько отличаться.

Каждый объект класса PdfFileReader имеет атрибут .pages, при помощи которого мы можем производить итерацию по всем страницам нашего PDF-файла.

Например, следующий цикл for выведет на экран текст со всех страниц файла Pride_and_Prejudice.pdf:

for page in pdf.pages:
    print(page.extractText())

Теперь давайте соберем наши знания воедино и напишем программу, которая извлекает весь текст из файла Pride_and_Prejudice.pdf и записывает его в текстовый файл (с расширением .txt).

Собираем все вместе

Откройте новое окно в вашем редакторе или IDE и напишете там следующий код:

from pathlib import Path
from PyPDF2 import PdfFileReader

# Не забудьте поменять путь на тот, что есть в вашем компьютере.
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice-files"
    / "Pride_and_Prejudice.pdf"
)

# 1
pdf_reader = PdfFileReader(str(pdf_path))
output_file_path = Path.home() / "Pride_and_Prejudice.txt"

# 2
with output_file_path.open(mode="w") as output_file:
    # 3
    title = pdf_reader.documentInfo.title
    num_pages = pdf_reader.getNumPages()
    output_file.write(f"{title}\\nNumber of pages: {num_pages}\\n\\n")

    # 4
    for page in pdf_reader.pages:
        text = page.extractText()
        output_file.write(text)

Давайте разберем этот код:

  1. Сначала мы сохраняем новый экземпляр класса PdfFileReader в переменную pdf_reader. Мы также создаем объект Path, который указывает на файл Pride_and_Prejudice.txt в нашей домашней директории и сохраняем его в переменную output_file_path.
  2. Далее мы открываем файл, указатель на который сохранен в переменной output_file_path, в режиме записи, и присваиваем этому объекту имя output_file. Инструкция with гарантирует нам, что файл будет закрыт сразу же после выполнения этого блока кода.
  3. Затем, внутри блока with, мы записываем в текстовый файл заголовок (title) нашего PDF-файла и количество страниц — при помощи метода output_file.write().
  4. И наконец, с помощью цикла for мы производим итерацию по всем страницам PDF-файла. На каждом шаге цикла в переменную page записывается элемент из объекта PageObject. Текст из каждой страницы извлекается при помощи метода page.extractText() и затем записывается в output_file.

Когда вы сохраните и запустите эту программу, она создаст файл Pride_and_Prejudice.txt в вашей домашней директории, в котором будет весь текст из файла Pride_and_Prejudice.pdf. Откройте его и убедитесь сами!

Проверка ваших знаний

Упражнение: Выведите текст из PDF файла.

В папке practice_files данного репозитория находится файл под названием zen.pdf. Создайте экземпляр класса PdfFileReader и используйте его для вывода на экран текста из первой страницы.

Решение:

Сначала определяем путь до PDF-файла:

# Вначале импортируем необходимые библиотеки и классы
from pathlib import Path
from PyPDF2 import PdfFileReader

# Затем создаем объект Path для нашего PDF файла
# Возможно, вам нужно изменить этот путь на вашем компьютере
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "zen.pdf"
)

Теперь создаем экземпляр класса PdfFileReader:

pdf_reader = PdfFileReader(str(pdf_path))

Не забывайте, что экземпляры класса PdfFileReader могут создаваться только со строковой переменной, которая содержит в себе путь к файлу. Объекты типа Path не подходят!

Теперь используем метод .getPage(), чтобы получить первую страницу:

first_page = pdf_reader.getPage(0)

Помним, что страницы индексируются начиная с 0.

Затем используем метод .extractText(), чтобы извлечь текст:

text = first_page.extractText()

И наконец, выводим текст на экран:

print(text)

Если готовы, переходим ко второй секции.

Извлечение страниц из PDF-файла

В предыдущей секции мы научились извлекать весь текст из PDF-файла и сохранять его в текстовый файл. Сейчас мы научимся извлекать страницу или диапазон страниц из PDF-файла и сохранять ее или их в новый PDF-файл.

Для создания нового PDF-файла используется класс PdfFileWriter. Давайте рассмотрим этот класс и изучим шаги, необходимые для создания нового PDF-файла при помощи пакета PyPDF2.

Используем класс PdfFileWriter

Класс PdfFileWriter создает новые PDF-файлы. Импортируем класс PdfFileWriter и создадим новый экземпляр с именем pdf_writer:

from PyPDF2 import PdfFileWriter
pdf_writer = PdfFileWriter()

Объекты PdfFileWriter похожи на пустые PDF-файлы. Перед сохранением в файл надо добавить в них несколько страниц.

Давайте добавим пустую страницу:

page = pdf_writer.addBlankPage(width=72, height=72)

Параметры width и height являются обязательными и определяют размеры страницы в единицах под названием points (точки). Одна точка равна 1/72 дюйма. Таким образом мы создали пустую страницу размеров в один квадратный дюйм и добавили ее в объект pdf_writer (который является экземпляром класса PdfFileWriter).

Метод .addBlankPage() возвращает новый экземпляр класса PageObject, представляющий собой страницу, которую мы добавили в объект класса PdfFileWriter:

type(page)
# результат: <class 'PyPDF2.pdf.PageObject'>

В этом примере мы присвоили значение экземпляра класса PageObject, возвращаемое методом .addBlankPage(), переменной Page, но на практике это обычно не требуется. То есть, обычно вызывается метод .addBlankPage() без присваивания возвращаемого значения чему-либо:

 pdf_writer.addBlankPage(width=72, height=72)

Чтобы записать содержимое объекта pdf_writer в файл PDF, передаем объект файла в двоичном режиме записи в метод pdf_writer.write():

from pathlib import Path
with Path("blank.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

Данный код создает файл под названием blank.pdf в нашей текущей рабочей директории. Если мы откроем этот файл в каком-нибудь редакторе, например в Adobe Acrobat, то увидим документ с единственной пустой страницей площадью в один дюйм.

Техническое замечание: Обратите внимание, что мы сохраняем PDF-файл, передавая объект файла методу .write(), принадлежащему объекту класса PdfFileWriter, а не методу .write() из стандартной библиотеки Python.

То есть, следующий код работать не будет:

with Path("blank.pdf").open(mode="wb") as output_file:
    output_file.write(pdf_writer)

Подобные ошибки часто ставят в тупик начинающих программистов, так что старайтесь их избегать.

Объекты класса PdfFileWriter могут записывать в новые PDF-файлы, но с их помощью нельзя создать ничего, кроме пустых страниц.

Это может казаться серьезным недостатком, но во многих случаях нам не нужно создавать новый контент. Зачастую мы просто работаем со страницами, извлеченными из других PDF файлов, открыв их при помощи класса PdfFileReader.

Примечание: мы научимся создавать PDF-файл с нуля дальше, в секции «Создание PDF-файла с нуля».

В вышеприведенном примере можно выделить три шага по созданию нового PDF-файла при помощи пакета PyPDF2:

  1. Создаем экземпляр класса PdfFileWriter.
  2. Добавляем к нему одну или более страниц.
  3. Записываем его в файл, используя метод PdfFileWriter.write().

Вы увидите этот шаблон много раз, когда мы будем изучать различные методы добавления страниц в экземпляр класса PdfFileWriter.

Извлекаем одну страницу из PDF-файла

Давайте снова вернемся к файлу Pride_and_Prejudice.pdf, с которым уже работали в прошлой секции. Мы откроем этот PDF-файл, извлечем первую страницу и создадим новый PDF-файл только с этой единственной страницей.

Для начала, импортируем классы PdfFileReader и PdfFileWriter из пакета PyPDF2, а класс Path — из модуля pathlib.

from pathlib import Path
from PyPDF2 import PdfFileReader, PdfFileWriter

Теперь откроем файл Pride_and_Prejudice.pdf при помощи экземпляра класса PdfFileReader:

# при необходимости поменяйте путь, в зависимости от расположения 
# файлов на вашем компьютере.
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "Pride_and_Prejudice.pdf"
)
input_pdf = PdfFileReader(str(pdf_path))

Передаем индекс 0 в метод .getPage(), чтобы получить объект класса PageObject, который будет представлять первую страницу:

first_page = input_pdf.getPage(0)

Теперь создаем новый экземпляр класса PdfFileWriter и добавляем к нему first_page при помощи метода .addPage():

pdf_writer = PdfFileWriter()
pdf_writer.addPage(first_page)

Метод .addPage() добавляет страницу ко множеству страниц объекта pdf_writer, точно так же, как это делает метод .addBlankPage(). Разница только в том, что методу .addPage() требуется существующий объект класса PageObject.

Теперь запишем содержимое pdf_writer в новый файл:

with Path("first_page.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

Теперь мы имеем новый файл, сохраненный в нашей рабочей директории под названием first_page.pdf. Он содержит страницу обложки из файла Pride_and_Prejudice.pdf. Отлично!

Извлекаем несколько страниц из PDF-файла

Давайте извлечем первую главу из Pride_and_Prejudice.pdf и сохраним ее в новый PDF-файл.

Если вы откроете Pride_and_Prejudice.pdf при помощи любого редактора PDF, вы увидите, что первая глава находится на второй, третьей и четвертой страницах PDF-файла. Поскольку страницы индексируются, начиная с 0, нам нужно извлечь страницы с индексами 1, 2 и 3.

Для начала настроим все, импортировав нужные нам классы и открыв файл PDF:

from PyPDF2 import PdfFileReader, PdfFileWriter
from pathlib import Path
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "Pride_and_Prejudice.pdf"
)
input_pdf = PdfFileReader(str(pdf_path))

Наша цель — извлечь страницы с индексами 1, 2 и 3, добавить их в новый экземпляр класса PdfFileWriter, а затем записать их в новый PDF-файл.

Один из способов это сделать — при помощи цикла перебрать нужные нам индексы, начиная с 1 и заканчивая 3, извлекая страницу на каждом шаге цикла и добавляя ее в экземпляр класса PdfFileWriter:

pdf_writer = PdfFileWriter()
for n in range(1, 4):
    page = input_pdf.getPage(n)
    pdf_writer.addPage(page)

Цикл перебирает числа 1, 2 и 3, поскольку функция range (1, 4) не включает правую границу (4). На каждом этапе цикла страница с текущим индексом извлекается с помощью метода .getPage () и добавляется в объект pdf_writer с помощью метода .addPage ().

Теперь объект pdf_writer имеет три страницы, что можно проверить при помощи метода .getNumPages ():

pdf_writer.getNumPages()
# результат: 3

И наконец, можно записать извлеченные страницы в новый PDF-файл:

with Path("chapter1.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

Теперь можно открыть файл chapter1.pdf, который находится в текущей рабочей директории, и прочитать первую главу книги «Гордость и предубеждение».

Другой способ извлечь несколько страниц из PDF-файла — воспользоваться тем фактом, что атрибут PdfFileReader.pages поддерживает срезы. Давайте повторим предыдущий пример, используя атрибут .pages вместо циклического перемещения по нужному диапазону.

Начнем с инициализации нового объекта класса PdfFileWriter:

pdf_writer = PdfFileWriter()

Теперь переберем страницы из нужного диапазона, от 1 до 4,воспользовавшись срезом атрибута .pages:

for page in input_pdf.pages[1:4]:
   pdf_writer.addPage(page)

Не забывайте, что срез берется от первого элемента включительно до последнего, но не включая его. Таким образом, .pages[1:4] вернет итерируемую последовательность страниц с индексами 1, 2 и 3.

И наконец, запишем извлеченные страницы в новый PDF-файл:

with Path("chapter1_slice.pdf").open(mode="wb") as output_file:
    pdf_writer.write(output_file)

Теперь откроем файл chapter1_slice.pdf в нашем текущем рабочем каталоге и сравним его с файлом chapter1.pdf, который мы создали ранее. Они абсолютно одинаковы!

Иногда нам нужно извлечь все страницы из PDF-файла. Можно использовать способы, показанные выше, но PyPDF2 содержит другой удобный метод. Экземпляры класса PdfFileWriter имеют метод .appendPagesFromReader(), который можно использовать для добавления страниц из экземпляра класса PdfFileReader.

Чтобы использовать .appendPagesFromReader (), надо передать в него экземпляр класса PdfFileReader в качестве параметра. Например, следующий код копирует все страницы из PDF-файла «Гордость и предубеждение» в экземпляр PdfFileWriter:

pdf_writer = PdfFileWriter()
pdf_writer.appendPagesFromReader(pdf_reader)

Объект pdf_writer теперь содержит все страницы из объекта pdf_reader!

Проверка ваших знаний

Упражнение: Извлеките последнюю страницу PDF-файла.

Извлеките последнюю страницу из файла Pride_and_Prejudice.pdf и сохраните ее в новый файл с именем last_page.pdf.

Решение:

Устанавливаем путь к файлу Pride_and_Prejudice.pdf:

# Вначале импортируем необходимые классы и библиотеки
from pathlib import Path
from PyPDF2 import PdfFileReader, PdfFileWriter

# Создаем объект Path, который содержит путь до нужного файла.
# На вашем компьютере он может быть другим
pdf_path = (
    Path.home()
    / "creating-and-modifying-pdfs"
    / "practice_files"
    / "Pride_and_Prejudice.pdf"
)

Теперь создаем экземпляр класса PdfFileReader:

pdf_reader = PdfFileReader(str(pdf_path))

Не забывайте, что экземпляры класса PdfFileReader могут создаваться только со строковой переменной, которая содержит в себе путь к файлу. Объекты типа Path не подходят!

Используем атрибут .pages, чтобы получить доступ ко всем страницам в PDF-файле. К последней странице можно обратиться по индексу -1:

last_page = pdf_reader.pages[-1]

Теперь можно создать экземпляр класса PdfFileWriter и добавить к нему последнюю страницу:

pdf_writer = PdfFileWriter()
pdf_writer.addPage(last_page)

Наконец, запишем содержимое pdf_writer в файл last_page.pdf:

output_path = Path.home() / "last_page.pdf"
with output_path.open(mode="wb") as output_file:
    pdf_writer.write(output_file)

Если готовы, продолжение следует!