Парсинг PDF-документов, лежащих на сайте: скачивание и извлечение текста

📝 Кратко: Сбор данных из документов требует иного подхода, чем обычный веб-скрейпинг. Мы разберем, как реализовать парсинг PDF Python: от автоматического поиска и скачивания файлов до извлечения текста и сложных таблиц с помощью современных библиотек.
Парсинг PDF-документов

Привет! А вы знаете, что самое обидное в парсинге? Это когда ты научился идеально собирать данные с сайтов, вытаскивать любые заголовки и цены, а потом заказчик присылает ссылку и говорит: «Тут всё лежит в PDF, вытащи оттуда таблицы». И в этот момент понимаешь, что привычный софт тут не поможет.

PDF — это вообще не про структуру, это скорее «цифровой распечаток», где данные разбросаны по координатам. Я сам через это прошел, перепробовал кучу библиотек и набил немало шишек. В этой статье я поделюсь своим опытом и покажу, как сделать парсинг PDF Python стабильным и понятным. Мы научимся автоматически находить файлы на сайте, скачивать их и вытаскивать данные из pdf документов в аккуратные таблицы. Разберем всё на реальном примере, чтобы вы могли просто скопировать код и запустить его у себя.

Почему обычный подход к парсингу не работает с PDF?

Когда мы работаем с HTML, у нас есть теги: <table>, <tr>, <td>. Это логично и понятно. PDF же создавался для того, чтобы документ выглядел одинаково и на экране, и на бумаге. Внутри него нет структуры «строка-столбец», там просто записано: «нарисуй символ ‘А’ в координатах X=50, Y=100». Именно поэтому, когда вы пытаетесь просто скопировать таблицу из PDF в Excel, данные часто слипаются в одну строку.

Чтобы сделать качественный парсинг PDF с помощью Python, нам нужно решить две проблемы. Сначала нужно найти файлы на сайте и скачать их. Часто ссылки на них генерируются скриптами, поэтому обычный requests может их не увидеть. Тут нам на помощь придет Playwright — он откроет сайт как настоящий браузер. Вторая задача — вытащить данные из самого файла. Мы будем использовать pdfplumber (лучший для таблиц) и PyMuPDF (для быстрого чтения текста). Я разбил этот процесс на шаги, чтобы вы понимали, что происходит в каждой строчке кода.

Этап 1: Автоматический поиск PDF-файлов на сайте

Для примера мы возьмем реальную площадку для тестов: https://file-examples.com/index.php/sample-documents-download/sample-pdf-download/. На этом сайте специально выложены примеры PDF разного размера, и это идеальное место, чтобы отладить наш скрипт. Наша задача — зайти туда, найти все ссылки на PDF и собрать их в список.

Используем Playwright для поиска скрытых файлов

Playwright — это современный инструмент, который запускает полноценный браузер. Это важно, потому что он дожидается, пока все скрипты на сайте отработают, и мы увидим финальную версию страницы со всеми ссылками. Мы напишем функцию, которая возвращает нам готовый список (list) строк-адресов.

import asyncio
from playwright.async_api import async_playwright

async def find_all_pdf_links():
    # Наш целевой полигон с примерами документов
    url = "https://file-examples.com/index.php/sample-documents-download/sample-pdf-download/"
    
    async with async_playwright() as p:
        # Запускаем браузер в фоновом режиме (скрыто от глаз)
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()
        
        # Переходим на сайт и ждем, пока сетевая активность утихнет
        await page.goto(url, wait_until="networkidle")
        
        # Собираем все ссылки (теги <a>) на странице
        # Метод вытаскивает значения атрибута href сразу у всех элементов
        links = await page.locator("a").all_get_attributes("href")
        
        # Очищаем список: оставляем только ссылки на .pdf и убираем дубликаты
        valid_links = [l for l in links if l and l.endswith(".pdf")]
        
        await browser.close()
        # Возвращаем список уникальных ссылок для дальнейшей работы
        return list(set(valid_links))

Объяснение кода:

  • wait_until="networkidle": Это «предохранитель». Мы не начинаем собирать ссылки, пока браузер не скажет, что загрузка всех скриптов и стилей завершена. Это гарантирует полноту данных.
  • all_get_attributes("href"): Очень быстрая команда Playwright, которая за один проход вытаскивает все адреса. Новичку это экономит кучу времени на написании циклов.
  • list(set(valid_links)): На страницах часто бывает по две ссылки на один файл (например, иконка и текст). set превращает список во множество, автоматически удаляя все повторы.

📚 Документация: Playwright Auto-waiting «Playwright автоматически дожидается, пока элементы станут доступными, а страница — стабильной, прежде чем выполнять действия, что радикально снижает количество ошибок в парсерах».

Этап 2: Скачивание файлов на компьютер

Теперь у нас есть список ссылок. Но они пока просто «текст в памяти». Чтобы их прочитать, их нужно физически сохранить на ваш жесткий диск. Мы создадим специальную папку, куда будем складывать все найденные файлы. Для скачивания мы используем библиотеку requests. Она проще и стабильнее для простых запросов на загрузку файлов, чем Playwright.

Надежное сохранение через Requests и Pathlib

Мы будем использовать режим стриминга. Это как если бы вы пили воду через соломинку: вы не пытаетесь проглотить весь стакан за раз, а пьете маленькими глотками. Так и Python будет записывать файл по 8 килобайт, что спасет ваш компьютер от зависания при скачивании тяжелых документов.

import requests
from pathlib import Path

def download_single_pdf(url, output_folder):
    # Создаем объект пути и саму папку, если её нет
    folder = Path(output_folder)
    folder.mkdir(parents=True, exist_ok=True)
    
    # Вытаскиваем имя файла из ссылки (всё, что после последнего слеша)
    file_name = url.split("/")[-1]
    file_path = folder / file_name
    
    # Скачиваем файл "потоком", чтобы не забивать память
    with requests.get(url, stream=True) as response:
        # Если ссылка рабочая (статус 200), начинаем запись
        if response.status_code == 200:
            with open(file_path, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    f.write(chunk)
            return file_path # Возвращаем путь к скачанному файлу
    return None

Объяснение кода:

  • Path(output_folder).mkdir(...): Это профессиональный подход. Мы не просто пишем путь, а используем мощь pathlib. Подробнее о том, как это упрощает жизнь, читайте в статье про python pathlib примеры.
  • wb: Важнейший флаг (Write Binary). PDF — это бинарный код. Если вы забудете букву b, Python попытается записать его как текст, и файл навсегда превратится в бесполезный мусор.
  • iter_content: Этот метод позволяет скачивать даже огромные файлы размером в несколько гигабайт на самом слабом ноутбуке, так как в памяти одновременно находится всего 8 КБ данных.

📚 Документация: Requests Stream Workflow
«Использование stream=True позволяет эффективно управлять ресурсами при загрузке больших объектов, так как данные передаются только тогда, когда вы их запрашиваете».

Этап 3: Полный цикл — Ищем, качаем, парсим данные

Теперь объединим всё в одну мощную систему. Мы добавим библиотеку pdfplumber, которая будет заглядывать внутрь скачанного файла и вытаскивать оттуда текст. Это и есть настоящий парсинг PDF Python. В финальном коде ниже вы увидите, как данные плавно перетекают из одной функции в другую: список ссылок из Playwright переходит в цикл скачивания, а пути к файлам — в модуль извлечения текста.

Финальный интегрированный скрипт (Всё в одном)

Этот код является готовым решением. Он заходит на сайт, находит все PDF, сохраняет их в папку downloaded_reports и выводит содержимое первой страницы каждого документа прямо в консоль. Это идеальная база для вашего будущего проекта.

import asyncio
import requests
import pdfplumber
from pathlib import Path
from playwright.async_api import async_playwright

# 1. Функция поиска ссылок
async def fetch_pdf_urls():
    url = "https://file-examples.com/index.php/sample-documents-download/sample-pdf-download/"
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()
        await page.goto(url, wait_until="networkidle")
        links = await page.locator("a").all_get_attributes("href")
        pdf_urls = [l for l in links if l and l.endswith(".pdf")]
        await browser.close()
        return list(set(pdf_urls))

# 2. Функция скачивания
def save_pdf(url):
    folder = Path("downloaded_reports")
    folder.mkdir(exist_ok=True)
    name = url.split("/")[-1]
    path = folder / name
    with requests.get(url, stream=True) as r:
        if r.status_code == 200:
            with open(path, "wb") as f:
                for chunk in r.iter_content(chunk_size=8192):
                    f.write(chunk)
            return path
    return None

# 3. Функция парсинга контента
def parse_pdf_content(file_path):
    with pdfplumber.open(file_path) as pdf:
        # Берем только первую страницу для демонстрации
        first_page = pdf.pages[0]
        text = first_page.extract_text()
        return text[:150] # Возвращаем начало текста

# ГЛАВНЫЙ ПРОЦЕСС
async def main():
    print("--- ШАГ 1: Поиск PDF на сайте ---")
    urls = await fetch_pdf_urls()
    print(f"Найдено ссылок: {len(urls)}")

    for url in urls:
        print(f"\nРаботаю с: {url.split('/')[-1]}")
        
        # ШАГ 2: Скачиваем
        saved_path = save_pdf(url)
        if saved_path:
            print(f"Файл сохранен в: {saved_path}")
            
            # ШАГ 3: Извлекаем текст
            content = parse_pdf_content(saved_path)
            print(f"Текст из файла: {content}...")

if __name__ == "__main__":
    asyncio.run(main())

Объяснение работы системы:

  • Асинхронность: Мы используем asyncio.run(main()), чтобы Playwright работал правильно. Это современный стандарт для Python-скриптов, работающих с сетью.
  • Связка функций: Обратите внимание, как saved_path = save_pdf(url) возвращает путь к файлу, который мы тут же передаем в parse_pdf_content(saved_path). Это делает код модульным: вы легко можете заменить блок скачивания или блок парсинга.
  • Безопасность данных: Каждый этап проверяет результат. Если файл не скачался (None), скрипт не упадет, а просто перейдет к следующей ссылке.

Советы по масштабированию и чистоте кода

Когда вы начнете парсить не 3 файла, а 3000, вы столкнетесь с новыми вызовами. Первое — это структура проекта. Не валите всё в один файл. Хорошим тоном считается разделение логики на модули. Также крайне важно использовать обработку исключений try...except, чтобы одна «битая» ссылка не останавливала работу парсера, который трудился три часа.

Для того чтобы ваш код был не просто рабочим, но и профессиональным, обязательно изучите принципы в нашей статье про чистый код на Python. Также, если вы планируете хранить результаты парсинга не в консоли, а в файлах, вам пригодится опыт работы с путями — загляните в python pathlib примеры. И помните: автоматизация — это не только про результат, но и про удовольствие от того, что робот делает за вас всю скучную работу. Не бойтесь совершать ошибки, ведь даже опытные разработчики часто натыкаются на типичные проблемы, которые мы описали в 10 ошибок новичков в Python.

Заключение

Парсинг PDF на Python — это мощный инструмент, который делает вас «супергероем» в глазах любого бизнеса, работающего с документами. Мы сегодня построили настоящий конвейер: научили скрипт самостоятельно находить цели в интернете, скачивать их, бережно сохраняя ресурсы компьютера, и заглядывать внутрь «закрытых» PDF-файлов для извлечения данных.

Самое главное — не останавливайтесь на достигнутом. PDF бывает разным: от простых текстовых страниц до сложных отсканированных картинок, требующих OCR-технологий. Но теперь у вас есть база, с которой не страшно начинать любые проекты по автоматизации сбора документов.

🔁 Если вам полезны советы по автоматизации на Python, посмотрите также:
Python pathlib примеры — мастер-класс по работе с файловой системой вашего проекта
Как писать чистый и читаемый код на Python — превратите ваш парсер в образец инженерного искусства
10 ошибок новичков в Python — проверьте свой код на типичные «детские болезни»
💬 Остались вопросы? Пишите в комментариях — с радостью уточню, дополню или помогу с вашим кодом.
📢 Подписывайтесь на Telegram-канал PythonAuto, чтобы не пропустить новые гайды по автоматизации, парсингу и разные трюки на Python.
👉 Ваш интерес — лучшая мотивация для новых статей!

Оставьте комментарий