В этом уроке мы создадим скрапер – программу, собирающую данные со страниц сайта. А конкретнее, напишем код, который будет анализировать наш сайт и подсчитывать сколько раз пользователи просмотрели уроки.
Архитектура
Первое, с чего я рекомендую начать, это наметить самые крупные части будущей программы. Наивысшие абстракции, так сказать.
Нам потребуется:
- Сделать запрос и получить страницу из сети Интернет
- Проанализировать страницу и получить из неё нужные данные (количество просмотров каждого урока)
- Привести данные к нужному формату
- Просуммировать данные
Теперь наметим наши намерения в коде:
def clean():
"""
Эта функция приводит данные к нужному формату
"""
pass
def read():
"""
Эта функция получает данные со страницы
"""
pass
def get_page():
"""
Эта функция получает данные из Интернет
"""
pass
def main():
"""
Это управляющая функция, которая вызывает
остальные, суммирует результат их работы
и выводит в консоль
"""
pass
if __name__ == '__main__':
main()
Несколько замечаний по приведённому коду:
- Запомните правило: не важно, в каком порядке указывать абстракции, но этот порядок должен быть, и он должен быть одинаковым во всё проекте. Здесь функции расположены снизу вверх в порядке их вызова.
- Существует подход, при котором каждый блок кода должен выполнять только одно действие. Такие функции называются чистыми и прекрасно, когда получается написать программу в этом стиле. Но этот подход, как и любой подход, не является безоговорочным законом и, если ему следовать неукоснительно, легко получить код, в котором будет больше инфраструктуры, чем логики. В нашем коде данный принцип нарушается в функции main() – она делает много всего сразу.
- Обратите внимание на конструкцию if __name__ == ‘__main__’. Это стандартный подход в Python для исполнения кода внутри скрипта. Дело в том, что, если импортировать любой модуль, то его код сразу же выполнится (что, чаще всего, нежелательно), а так мы защищаемся от подобного сценария. Подробнее об этом расскажу в другой раз.
Следующим шагом в написании программы может быть добавление параметров функций, возвращаемых значений, объявление констант. Следует хорошо продумать, как части кода будут обмениваться данными (что каждая из них принимает и возвращает, в какой последовательности).
Вообще, программы можно писать по-разному, кому как удобнее. Мне удобнее писать от абстрактных частей к конкретным, вдоль потока выполнения. Так и сделаем.
Управляющий цикл
Начнём с функции main():
def main():
i = 0
result = 0
url = 'https://pythoninfo.ru/page/{}'
while True:
page = get_page(url, i)
if page.status_code != 200:
break
i += 1
page_sum = sum(clean(read(page)))
print(f'Страница №{i}', ':', page_sum)
result += page_sum
print(result)
Здесь мы:
- Объявляем и инициализируем переменные и константы. i – просто счётчик для цикла; result – переменная, в которой мы будем накапливать итоговое значение; url – собственно строка url, в которую будем подставлять номер запрашиваемой страницы.
- Создаём главный цикл программы, в котором и произойдёт вся магия))
- В первой строке тела цикла получаем страницу сайта в переменную page
- Проверяем, удалось ли получить страницу. Если нет, завершаем перебор страниц
- Передаём в переменную page_sum сумму преобразованных данных о просмотрах каждого урока на странице
- Накапливаем суммы просмотров всех уроков страниц в переменную result
- После выхода из цикла выводим результат в терминал
Получаем страницы
Следующая часть кода – получение страницы из сети Интернет. Для этого используем библиотеку requests, о которой мы уже рассказывали в одном из прошлых уроков.
Не забудьте её установить:
pip install requests
И импортировать:
import requests
А дальше всё лаконично:
def get_page(url, i):
page = requests.get(url.format(i + 1))
return page
Думаю, здесь объяснять нечего.
Парсинг
Теперь, пожалуй, самое сложное – получить данные со страницы. К слову, эта задача становится всё сложнее с развитием front-end технологий. Стандартом в мире Питона является применение библиотеки BeautifulSoup4. Эта библиотека предназначена для парсинга (получения данных) из файлов в формате HTML и XML. Её так же надо установить:
pip install beautifulsoup4
И импортировать:
from bs4 import BeautifulSoup
Библиотека является очень популярной, во многом, из-за удобства использования и хорошей документации, в том числе, на русском языке. Но что именно парсить? Давайте рассмотрим страницу со списком уроков. Под каждым из них указано количество просмотров. Щёлкаем по этим цифрам правой клавишей мыши и выбираем «Просмотреть код» (у меня Гугл Хром, в других браузерах название пункта выпадающего меню может отличаться). После этого в браузере откроется окно инструментов разработчика, а в нём код страницы, в том месте, где расположен код выбранного элемента. Тут, конечно, неплохо бы хоть немного уметь читать HTML. Мы видим, что нужные нам цифры – это значения тегов span с классом ‘post-card__views’. Забираем!
def read(page):
page = page.text
soup = BeautifulSoup(page, "html.parser")
all_data = soup.findAll('span', class_='post-card__views')
watch_list = [data.text for data in all_data]
return watch_list
Подготовка данных
Теперь давайте посмотрим на списки, которые возвращает эта функция:
['69', '160', '200', '134', '657', '226', '973', '252', '156', '226', '69', '160', '200', '134']
['405', '11к.', '491', '1.5к.', '528', '1.4к.', '4.5к.', '392', '24.6к.', '2.1к.', '35', '88', '161', '106']
['3.3к.', '2.9к.', '1.3к.', '2.2к.', '4.8к.', '2к.', '1.2к.', '1.3к.', '3.9к.', '1.2к.', '30', '71', '149', '97']
['1.9к.', '620', '2.5к.', '414', '5.6к.', '2.3к.', '2.8к.', '490', '104', '1.3к.', '30', '71', '149', '97']
Обратите внимание, что есть два типа значений: те, которые можно просто преобразовать в число и те, которые надо очистить от буквы «к» и привести к тысячам. Это просто:
def clean(crud_list):
cleaned_list = []
for item in crud_list:
if 'к' in item:
item = item.replace('к.', '')
if '.' in item:
item = item.replace('.', '') + '00'
else:
item += '000'
cleaned_list.append(int(item))
return cleaned_list
Проверим содержимое cleaned_list:
[69, 160, 200, 134, 657, 226, 973, 252, 156, 226, 69, 160, 200, 134]
[405, 11000, 491, 1500, 528, 1400, 4500, 392, 24600, 2100, 35, 88, 161, 106]
[3300, 2900, 1300, 2200, 4800, 2000, 1200, 1300, 3900, 1200, 30, 71, 149, 97]
[1900, 620, 2500, 414, 5600, 2300, 2800, 490, 104, 1300, 30, 71, 149, 97]
Да, так гораздо лучше.
Переходим к следующему этапу… Стоп. Мы уже всё сделали?
Полный код:
import requests
from bs4 import BeautifulSoup
def clean(crud_list):
cleaned_list = []
for item in crud_list:
if 'к' in item:
item = item.replace('к.', '')
if '.' in item:
item = item.replace('.', '') + '00'
else:
item += '000'
cleaned_list.append(int(item))
return cleaned_list
def read(page):
page = page.text
soup = BeautifulSoup(page, "html.parser")
all_data = soup.findAll('span', class_='post-card__views')
watch_list = [data.text for data in all_data]
return watch_list
def get_page(url, i):
page = requests.get(url.format(i + 1))
return page
def main():
i = 0
result = 0
url = 'https://pythoninfo.ru/page/{}'
while True:
page = get_page(url, i)
if page.status_code != 200:
break
i += 1
page_sum = sum(clean(read(page)))
print(f'Страница №{i}', ':', page_sum)
result += page_sum
print(result)
if __name__ == '__main__':
main()
# Вывод:
Страница №1 : 3616
Страница №2 : 47306
Страница №3 : 24447
Страница №4 : 18375
93744
Да, всё верно, 93744 просмотров в общем на все материалы сайта на 11.01.2022
Код ревью
— Мы уже всё сделали?
— Нет!
Каждый раз (когда позволяет время) анализируйте свой код. Где могут быть проблемы? Что можно улучшить? Если Вы этого не сделаете, то Вы прочитаете его ещё не раз. Но уже не по своей воле ))
Как сделать код лучше?
- Вынес бы все константы в начало скрипта
- Изменил бы проверку статуса ответа. Дело в том, что запрос может быть удачным, но иметь статус не равный 200
- Изменил бы основной цикл таким образом, чтобы не делать url.format(i + 1) внутри функции get_page()
- Убрал бы page = page.text из функции read(), а передавал бы в эту функцию сразу текст
- Логику в функции clean() можно сделать более продвинутой. Задумайтесь, что будет, если урок наберёт миллион просмотров? Правильно, всё сломается.
- Количество страниц со временем будет расти и это будет негативно сказываться на быстродействии. Стоит сделать программу асинхронной.
Что действительно стоит улучшить?
Мы живём в реальном мире, где нет ничего идеального. И ВАШ КОД НИКОГДА НЕ БУДЕТ ИДЕАЛЬНЫМ. С этим стоит смириться. Всегда есть что улучшить и надо правильно расставлять приоритеты – исправить/ускорить/доделать всё не получится. Ещё одна правда состоит в том, что причина Вашего выбора часто будет лежать за границами конкретного кода. К примеру, из шести перечисленных пунктов я выберу последний, так как асинхронный скрапер полезно будет использовать в других задачах, не связанных с этим конкретным кодом.
from bs4 import BeautifulSoup
import asyncio
import aiohttp
def clean(crud_list):
cleaned_list = []
for item in crud_list:
if 'к' in item:
item = item.replace('к.', '')
if '.' in item:
item = item.replace('.', '') + '00'
else:
item += '000'
cleaned_list.append(int(item))
return cleaned_list
async def read(page):
page = await page.text()
soup = BeautifulSoup(page, "html.parser")
all_data = soup.findAll('span', class_='post-card__views')
watch_list = [data.text for data in all_data]
return clean(watch_list)
async def get_page(session, url, i):
page = await session.get(url.format(i + 1))
return page
async def main():
i = 0
result = 0
url = 'https://pythoninfo.ru/page/{}'
async with aiohttp.ClientSession() as session:
while True:
page = await get_page(session, url, i)
if page.status != 200:
break
i += 1
page_sum = sum(await read(page))
print(f'Страница №{i}', ':', page_sum)
result += page_sum
print(result)
asyncio.get_event_loop().run_until_complete(main())
При выборе приоритетов развития кода важно здраво оценивать:
- Его задачи. К примеру, если вашим порталом пользуются через АПИ и почти не используют веб интерфейс, то и тратить время на развитие фронт-энда нет смысла
- Перспективы развития. К примеру, если ваш проект является в стадии раннего развития на молодом рынке – пусть он будет изредка выдавать ошибки, но привлечёт много внимания своим функционалом. Пилим фичи!
- Стоимость ошибок. Если Ваше приложение работает с деньгами или персональными данными, Вы изо дня в день будете оттачивать мастерство в ловле мельчайших багов
- Стадии жизни применяемых и окружающих технологий. Пример из моей практики. В одном проекте используется Object Pascal для десктопа и Python 3 для веб-версии. Так сложилось исторически. Всем очевидны перспективы обоих языков и обоих частей проекта. Конечно, десктоп будет со временем переписан и избавлен от Паскаля – эту часть не развивают, а лишь поддерживают в работоспособном состоянии.




