Статья Скачивание журналов с сайта для чтения онлайн в виде картинок и сборка их в PDF

Иногда я люблю почитать журналы. Но я уже давно их не покупал. Наверное последний купленный мой журнал был «Хакер» 2009 года. Уж не помню даже, какой выпуск. Помню, что тогда к журналам прилагался диск с программами и в этом номере был диск с дистрибутивом Linux Mandriva. Красивая была ось. Ну да суть не в этом. В последующее время я все больше предпочитал читать журналы, да и книги в электронном виде, потому, что бумажные издания несколько кусались по цене, да и кусаются сейчас. Так что, в этом плане особо ничего не изменилось. И если раньше было очень много сайтов и групп в том же ВК, где можно было скачать любой журнал почти за любой год, то с приходом авторского права все резко изменилось. Стало труднее найти свежие журналы, сайты если и находились, то через какое-то время переставали выкладывать новые из-за обращений правообладателя. А впоследствии и вовсе стали блокироваться. И я стал предпочитать скачивать журналы себе на жесткий диск. Благо места хватало. Тем более, что после прочтения его можно было сжать в архив и отправить в облако на долгосрочное хранение. Но, это, так сказать, преамбула.

Захотел я почитать свежий выпуск журнала. Не суть какого. Тут важно то, что я даже нашел сайт, где его можно было почитать.

Наш подопытный — это сайт zhurnala.ru. От остальных сайтов того же содержания отличается мало, ну, разве что, интерфейс немного получше.

И да, все, что вы прочитаете в данной статье предоставлено исключительно в общеобразовательных целях и для прокачки навыков и умений. Это, если, конечно, вы этого не умеете )

Так вот, можно почитать. Онлайн. И даже скачать. Вот только ссылки на скачивание вели все на разные файлообменники. А я их, почему-то, не люблю, наверное, с того самого времени, как только в первый раз столкнулся с ними. Ладно, подумал я. Качать пока не буду. Почитаю прямо на сайте. Благо, там встроенный плеер для просмотра картинок из журнала. Но, тут тоже меня ждало разочарование. В самом плеере журнала не было. А была просто ссылка на сайт issuu.com, который у меня заблокирован провайдером. Ну не ставить же VPN из-за такой малости.

0000.png

Можно было бы махнуть рукой, но я решил, а почему бы не скачать журнал себе на диск использовав для этого Python? Вдруг получиться. И я решил исследовать страницу. Для начала я пробежался по коду страницы, но, как и ожидалось, ничего особо интересного там не было. Кроме нескольких нужных ссылок. Тогда я полез в запросы.

0002.png

Чуток покопался и нашел GET-запрос, в ответ на который прилетает JSON со всеми ссылками на картинки страниц журнала. Это запрос reader3_4.json. Вот ссылка: https://reader3.isu.pub/borov665/itnews_032022/reader3_4.json

Конечно же, для того, чтобы скачать только лишь один журнал этих данных вполне достаточно, но, я решил немного поработать над тем, чтобы можно было скачивать любой журнал с данного сайта. Для того, чтобы получить JSON со ссылками нужно передать в ссылке название журнала. А где его можно взять? То есть, вот эта вот часть — itnews_032022 — название журнала и есть. В ссылке на страницу с журналом ничего похоже не наблюдалось. Тогда я решил все же покопаться в коде страницы. А именно в том фрейме, где загружается плеер. Именно там я заметил ссылку, в которой содержалось нужное мне название. Ну и заодно прихватить из кода название бумажного журнала. Что же, давайте приступим к реализации кода.


Что потребуется?

Нужно установить библиотеку requests, bs4, lxml, PIL и img2pdf. Сделать это можно с помощью команды:

pip install requests bs4 lxml Pillow img2pdf

Для чего последние две библиотеки я поясню уже по ходу статьи. И импортировать в модуль библиотеки os.path, time.

Вот так вот выглядит полный блок импорта:

Python:
import os.path
import time

import requests
from bs4 import BeautifulSoup
from PIL import Image
import img2pdf

Двигаемся дальше. Теперь, чтобы корректно делать запросы, желательно установить заголовки запроса. Вот, что у меня получилось:

Python:
headers = {
    'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.174 '
                  'YaBrowser/22.1.3.942 Yowser/2.5 Safari/537.36',
    'accept': '*/*'
}


Получение сегмента ссылки для загрузки JSON

Давайте создадим функцию, в которую будем передавать ссылку на страницу с журналом, а на выходе получать нужный нам сегмент ссылки для будущей загрузки JSON, а так же название журнала. Создадим функцию get_linksegment_on_json(url).

Python:
def get_linksegment_on_json(url):
    req = requests.get(url=url, headers=headers)
    soup = BeautifulSoup(req.text, 'lxml')
    return soup.find('div', class_='entry-content').find_all('p')[1].\
        find('iframe', class_='lazy lazy-hidden')['data-src'].split("=")[-1], soup.find('h1', class_='entry-title').text

Делаем запрос на страницу журнала, после чего создаем объект BeautifulSoup, в который передаем содержимое запроса и указываем парсер, с помощью которого будем парсить страницу. Ну, а далее, возвращаем найденное значение сегмента и найденное название журнала. Таким образом у нас будет возвращаться список из двух элементов. Саму функцию поиска сегмента вызовем в функции main().

Python:
def main():
    segment = get_linksegment_on_json(input('[+] Введите ссылку на страницу с журналом: '))


Получаем JSON со ссылками и загружаем картинки

Необходимый сегмент, который по сути и является идентификатором журнала получен. Теперь можно приступать к получению JSON и загрузке картинок. Создадим для начала папку, в которую будем складывать загруженные картинки. Далее создаем список, в который будем записывать папку и название картинки. Это будем нужно нам для дальнейшего использования. И делаем запрос на получение JSON со вставкой идентификатора журнала. Затем пробегаемся по разделу со ссылками. Для этого используем цикл for в котором перебираем элементы JSON, при этом узнаем количество этих элементов, которое равно длине раздела pages.

Выводим в терминал принт, чтобы не было скучно и понятно, на каком этапе мы находимся:

print(f"[+] Качаю картинку {num + 1}/{len(resp['document']['pages'])}…")

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

Python:
def get_download_image(segment):
    if not os.path.isdir('pages'):
        os.mkdir('pages')
    image_list = []
    resp = requests.get(url=f'https://reader3.isu.pub/borov665/{segment}/reader3_4.json', headers=headers).json()
    for num in range(0, len(resp['document']['pages'])):
        print(f"[+] Качаю картинку {num + 1}/{len(resp['document']['pages'])}...")
        req = requests.get(url=f"https://{resp['document']['pages'][num]['imageUri']}", headers=headers)
        with open(f'{os.path.join("pages", f"page_{num + 1}.jpg")}', 'wb') as file:
            file.write(req.content)
            image_list.append(f'{os.path.join("pages", f"page_{num + 1}.jpg")}')
    print('\n[+] Загрузка картинок завершена!')
    return image_list


Объединяем картинки в PDF

После того, как картинки загружены, их надо собрать в файл PDF. Для этого будем использовать библиотеку img2pdf, в которую передается для конвертации список картинок. Таким образом, открываем файл на запись в побайтовом режиме. Указываем название файла, которое мы получили ранее со страницы. И в функции записи файла передаем функцию img2pdf с параметром convert, куда передаем полученный нами список картинок.

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

Python:
# создание файла для записи в побайтовом режиме
# конвертация списка картинок и запись в файл
# удаление картинок из папки загрузок, а также удаление самой папки
def merge_journal_page(title, image_list):
    with open(f'{title}.pdf', 'wb') as file:
        file.write(img2pdf.convert(image_list))
    print(f'[INFO] PDF файл "{title}" создан!')
    for img in image_list:
        os.remove(img)
    if os.path.isdir(os.path.join('pages', 'img_compress')):
        os.removedirs(os.path.join('pages', 'img_compress'))
    else:
        os.removedirs('pages')

Сжимаем изображения перед конвертацией в PDF

Я немного подумал и решил, что неплохо было бы еще и немного сжать полученные картинки перед тем, как собирать их в PDF, для того, чтобы уменьшить размер получаемого файла. Качество страдает не особо. Можно сказать, что невооруженным глазом долго придется присматриваться к тому, что же изменилось на картинке. Но, справедливости ради надо отметить, что конвертация не всегда одинаково полезна для всех картинок. Бывает, что исходные картинки загружаются уже не особо высокого качества, а их сжатие чуть-чуть его еще ухудшает. Но, читаемо, в принципе. Так что, на вкус и цвет все фломастеры разные. Поэтому, я добавил еще и выбор для пользователя, в котором он будет решать, конвертировать ли ему картинки или нет.

Для конвертации картинок будем использовать библиотеку Pillow из которой загрузим модуль Image. Создаем в функции список со ссылками на конвертированные файлы, чтобы их впоследствии собрать в PDF, после создаем папку для сжатых картинок. Перебираем список со ссылками и каждую картинку передаем для открытия в Image. После чего, стразу же сохраняем с опцией optimize=True и качеством 40 от исходной картинки — quality=40. Затем добавляем ссылку на картинку в словарь. После того, как конвертация завершиться, удаляем исходные картинки и возвращаем словарь, в котором сохраняли ссылки на конвертированные картинки.

Python:
def compress_img(image_list):
    compress_img_list = []
    if not os.path.isdir(os.path.join('page', 'img_compress')):
        os.mkdir(os.path.join('pages', 'img_compress'))
    for num, img in enumerate(image_list):
        Image.open(img).save(os.path.join('pages', 'img_compress', f'page_{num+1}_com.jpg'), optimize=True, quality=40)
        compress_img_list.append(os.path.join('pages', 'img_compress', f'page_{num+1}_com.jpg'))
    for img in image_list:
        os.remove(img)
    return compress_img_list

Вот в принципе и все. Теперь, с помощью данного скрипта можно скачать любой журнал с сайта zhurnala.ru. Копируем ссылку на страницу с журналом, передаем ее в скрипт и получаем на выходе собранные в файла PDF картинки страниц журнала.

Ну, а вот пример собранных журналов, один, тот, что больше размером, без сжатия изображений, а второй со сжатием.

0003.png


Как видите, разница в размере присутствует. А теперь полный код скрипта:

Python:
"""Скрипт для загрузки журналов с сайта https://zhurnala.ru/.
Для загрузки копируем ссылку на журнал, вставляем в скрипт и на выходе
получаем файл PDF с нужным журналом.

Нужно понимать, что в данном случае получается PDF без текстового слоя,
так что копировать из него текст не получиться, если только распознать.
Но для чтения данный PDF вполне пригоден, так как скачиваются картинки
в высоком качестве.

Скрипт создан в качестве практики, в процессе обучения, для получения
опыта в парсинге данных сайтов."""

import os.path
import time

import requests
from bs4 import BeautifulSoup
from PIL import Image
import img2pdf

# заголовки для запросов
headers = {
    'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.174 '
                  'YaBrowser/22.1.3.942 Yowser/2.5 Safari/537.36',
    'accept': '*/*'
}


# получение сегмента ссылки для последующей загрузки JSON
# и также получение названия журнала
def get_linksegment_on_json(url):
    req = requests.get(url=url, headers=headers)
    soup = BeautifulSoup(req.text, 'lxml')
    return soup.find('div', class_='entry-content').find_all('p')[1].\
        find('iframe', class_='lazy lazy-hidden')['data-src'].split("=")[-1], soup.find('h1', class_='entry-title').text


# создание папки для загрузки изображений
# загрузка JSON и получение из него в цикле ссылок на загрузку изображений
# загрузка изображений и сохранение в ранее созданную папку
# возвращение списка загруженных картинок
def get_download_image(segment):
    if not os.path.isdir('pages'):
        os.mkdir('pages')
    image_list = []
    resp = requests.get(url=f'https://reader3.isu.pub/borov665/{segment}/reader3_4.json', headers=headers).json()
    for num in range(0, len(resp['document']['pages'])):
        print(f"[+] Качаю картинку {num + 1}/{len(resp['document']['pages'])}...")
        req = requests.get(url=f"https://{resp['document']['pages'][num]['imageUri']}", headers=headers)
        with open(f'{os.path.join("pages", f"page_{num + 1}.jpg")}', 'wb') as file:
            file.write(req.content)
            image_list.append(f'{os.path.join("pages", f"page_{num + 1}.jpg")}')
    print('\n[+] Загрузка картинок завершена!')
    return image_list


# создание папки для сохранения сжатых картинок
# перебор в цикле картинок и сжатие с сохранением в созданной папке
# добавление путей к картинкам в список
# удаление загруженных фото из папки с возвращением списка сжатых картинок
def compress_img(image_list):
    compress_img_list = []
    if not os.path.isdir(os.path.join('page', 'img_compress')):
        os.mkdir(os.path.join('pages', 'img_compress'))
    for num, img in enumerate(image_list):
        Image.open(img).save(os.path.join('pages', 'img_compress', f'page_{num+1}_com.jpg'), optimize=True, quality=40)
        compress_img_list.append(os.path.join('pages', 'img_compress', f'page_{num+1}_com.jpg'))
    for img in image_list:
        os.remove(img)
    return compress_img_list


# создание файла для записи в побайтовом режиме
# конвертация списка картинок и запись в файл
# удаление картинок из папки загрузок, а также удаление самой папки
def merge_journal_page(title, image_list):
    with open(f'{title}.pdf', 'wb') as file:
        file.write(img2pdf.convert(image_list))
    print(f'[INFO] PDF файл "{title}" создан!')
    for img in image_list:
        os.remove(img)
    if os.path.isdir(os.path.join('pages', 'img_compress')):
        os.removedirs(os.path.join('pages', 'img_compress'))
    else:
        os.removedirs('pages')


# вызов функции для получения сегмента, а также получение пользовательского ввода ссылки на страницу с журналом
# получение списка изображений и вызов функции для загрузки картинок
# запрос пользователя сжимать или не сжимать картинки
# в зависимости от выбора сжатие картинок и объединение в PDF
# или просто объединение загруженных картинок в PDF
def main():
    start = time.monotonic()
    segment = get_linksegment_on_json(input('[+] Введите ссылку на страницу с журналом: '))
    image_list = get_download_image(segment[0])
    if input('\n[+] Сжать загруженные картинки y/n?').lower() == 'y':
        compress_img_list = compress_img(image_list)
        merge_journal_page(segment[1], compress_img_list)
    else:
        merge_journal_page(segment[1], image_list)
    print(f'\nВремя загрузки картинок и создания журнала с учетом пользовательского выбора: '
          f'{round(time.monotonic() - start)} секунд')


if __name__ == "__main__":
    main()

Спасибо за внимание. Надеюсь, что данная статья кому-то будет полезной
 
Последнее редактирование:
Мы в соцсетях:

1 августа стартует курс «Основы программирования на Python» от команды The Codeby

Курс будет начинаться с полного нуля, то есть начальные знания по Python не нужны. Длительность обучения 2 месяца. Учащиеся получат методички, видео лекции и домашние задания. Много практики. Постоянная обратная связь с кураторами, которые помогут с решением возникших проблем.

Запись на курс до 10 августа. Подробнее ...