В этом уроке мы создадим скрапер – программу, собирающую данные со страниц сайта. А конкретнее, напишем код, который будет анализировать наш сайт и подсчитывать сколько раз пользователи просмотрели уроки.
Архитектура
Первое, с чего я рекомендую начать, это наметить самые крупные части будущей программы. Наивысшие абстракции, так сказать.
Нам потребуется:
- Сделать запрос и получить страницу из сети Интернет
- Проанализировать страницу и получить из неё нужные данные (количество просмотров каждого урока)
- Привести данные к нужному формату
- Просуммировать данные
Теперь наметим наши намерения в коде:
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 для веб-версии. Так сложилось исторически. Всем очевидны перспективы обоих языков и обоих частей проекта. Конечно, десктоп будет со временем переписан и избавлен от Паскаля – эту часть не развивают, а лишь поддерживают в работоспособном состоянии.
Подскажите как новичку,как распарсить уже готовый скрипт?
На простом примере я ввожу эти данные:
import paramiko
import time
import json
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(«***********», port=***, username=»name», password=»password»)
f = open(«c:\ps\Data.txt», mode=»w»)
stdin, stdout, stderr = ssh.exec_command(«show interface ISP»)
opt = stdout.readlines()
opt = «».join(opt)
print(opt)
f.write(opt)
stdin, stdout, stderr = ssh.exec_command(«show policy»)
opt = stdout.readlines()
opt = «».join(opt)
#print(opt)
#f.write(opt)
stdin, stdout, stderr = ssh.exec_command(«show ip arp»)
opt1 = stdout.readlines()
opt1 = «».join(opt1)
print(opt1)
#f.write(opt1)
stdin, stdout, stderr = ssh.exec_command(«show ip route»)
opt2 = stdout.readlines()
opt2 = «».join(opt2)
print(opt2)
#f.write(opt2)
stdin, stdout, stderr = ssh.exec_command(«show ip name-server»)
opt3 = stdout.readlines()
opt3 = «».join(opt3)
print(opt3)
f.write(opt1 + opt2 + opt3)
f.close()
#while True:
#print(«This prints once a minute.»)
#time.sleep(1)
Здравствуйте. Не совсем понятно, что именно Вам требуется.
Давайте разберёмся с понятиями.
Скрапер — код, который получат код. К примеру, загружает страницу сайта.
Парсер — код, который подготавливает данные. Сюда входит, синтаксический разбор, очистка данных, их преобразование в необходимый формат, приведение типов и тому подобное. К примеру, получение значений определённых тегов на сайте.
Теперь, о Вашей задаче.
Если Вам нужно получить очищенные данные из того, что возвращает скрипт, то нужно анализировать структуру этих данных. К примеру, если по SSH вернётся JSON, то Вам надо посмотреть, какие поля он содержит и что во что вложено.
Если Вы воспринимаете сам код скрипта как данные (хотите получить что-то из самого текста скрипта), то стоит его открыть как текстовый файл и искать необходимое при помощи регулярных выражений и различных строковых методов.
Если Вам нужно просто получать какие-то данные, генерируемые скриптом (значение переменных, результаты работы функций и вызовов методов), то Вам надо импортировать этот скрипт в свою программу или наладить межпроцессное взаимодействие при помощи очередей/сообщений/пайпов или как-то ещё.
Конкретизируйте задачу — постараюсь помочь.
С этого скрипта мне нужно получить данные с работы роутера.То есть принцип такой,работа самого скрипта считать информацию с устройства и вывести мне все данные.Ниже опять же командами описано то что я хочу получить на выходе работы этого скрипта.С парсингом я не то что на Вы,а просто не понимаю смысла его работы,так что если можете помочь,то попробуйте объяснить как для «Хлебушка»,и если можете сделать как пример парсинга на этом скрипте чтобы у меня хотя бы был образец на который стоит ориентироваться.
Но если уж прям совсем уточнить задачу,то нужно получить данные с работы скрипта.
Этот скрипт открывает SSH туннель и присоединяется к роутеру:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(«***********», port=***, username=»name», password=»password»)
Затем открывает текстовый файл:
f = open(«c:\ps\Data.txt», mode=»w»)
После этого отправляет команды роутеру:
stdin, stdout, stderr = ssh.exec_command(«show interface ISP»)
stdin, stdout, stderr = ssh.exec_command(«show policy»)
stdin, stdout, stderr = ssh.exec_command(«show ip arp»)
stdin, stdout, stderr = ssh.exec_command(«show ip route»)
stdin, stdout, stderr = ssh.exec_command(«show ip name-server»)
Преобразует ответ на команду в строку:
opt = stdout.readlines()
opt = ‘ ‘.join(opt)
И записывает эти ответы в открытый ранее текстовый файл:
f.write(opt)
В конце закрывает файл:
f.close()
Таким образом, «данные с работы скрипта» находятся в файле по адресу «c:\ps\Data.txt».
Узнать больше о работе с файлами можно в уроке «Работа с файлами«.
Если надо эти данные преобразовать, то работайте с ними как с текстом. Подробнее в уроке о регулярных выражениях. Это и будет Вашим парсером.
Если нужна помощь в этом, предоставьте содержимое файла «c:\ps\Data.txt» и то, к какому виду надо привести данные.
Официальная документация библиотеки paramiko (отвечает за работу с SSH) здесь.
Если нужна помощь в написании команд роутеру, то это совсем не по теме нашего сайта, но в сети есть много информации. Конкретно Вам нужен язык Bash. Начать можно с официального учебника.
Я наверное не так выразился,прошу прощения.Мне нужно распарсить уже готовые данные,тоесть .txt файл
В таком случае предоставьте содержимое .txt файла и то, к какому виду надо привести данные; как их планируете использовать. Подозреваю, что Вы хотите, чтобы Ваша программа получила эти данные из файла и произвела с ними какие-то манипуляции. Какие?
Мне нужно чтобы информация была без лишних строк и единым списком.Например: [K
id: GigabitEthernet1
index: 1
interface-name: ISP
type: GigabitEthernet
description: Broadband connection
traits: Peer
traits: Mac
traits: Ip
traits: Ip6
traits: Supplicant
traits: Ethernet
traits: GigabitEthernet
link: up
connected: yes
state: up
mtu: 1500
tx-queue-length: 2000
address: ***************
mask: ****************
uptime: 764695
global: yes
defaultgw: yes
priority: 700
security-level: public
mac: *****************
auth-type: none
port, name = 0:
id: GigabitEthernet1/0
index: 0
interface-name: 0
label: 0
type: Port
traits: EthernetPort
traits: GigabitEthernetPort
link: up
speed: 1000
duplex: full
auto-negotiation: on
flow-control: on
eee: off
cable-diagnostics: no
[K[K================================================================================
Name IP MAC Interface
================================================================================
DESKTOP-O7H02RC ************* ******************** Home
****************** ***************** ISP
[K[K================================================================================
Destination Gateway Interface F Metric
================================================================================
0.0.0.0/0 ************ ISP U 0
*************** 0.0.0.0 Guest U 0
*************** 0.0.0.0 ISP U 0
*************** 0.0.0.0 Home U 0
[K[K
И вот эту вакханалью,надо приести в нужный вид,тоесть чтобы была просто информационная таблица.
Это часть данных которые в .txt полученных со скрипта
Приведу не самый быстрый вариант, но такое решение проще для понимания.
(Сайт автоматически удаляет пробелы в начале строки, поэтому в коде все отступы слева я заменю на знак ‘~’).
Сначала создаю папку, в которую помещаю файл.txt. Потом создаю в этой же папке скрипт Python.
В скрипте создаю переменную patterns типа список, в котором перечисляю кортежи, содержащие каждый по две строки: первая — что надо найти, вторая — на что заменить.
patterns = [
~~~~('[K\n', ''),
~~~~('[K', ''),
~~~~('\n\n\n', '\n'),
~~~~('\n\n', '\n'),
~~~~('=', ''),
~~~~('*', ''),
~~~~('\n ', '\n'),
]
Так, к примеру, каждый знак «равно» будет заменён на пустую строку или, проще говоря, удалён. С помощью этих кортежей можно настроить все преобразования.
Затем открываю файл.txt и читаю его содержимое. После этого применяю к тексту замену перечисленных ранее паттернов:
with open('example.txt', 'r') as f:
~~~~text = f.read()
~~~~while True:
~~~~~~~~do = False
~~~~~~~~for pattern in patterns:
~~~~~~~~~~~~if pattern[0] in text:
~~~~~~~~~~~~~~~~text = text.replace(*pattern)
~~~~~~~~~~~~~~~~do = True
~~~~~~~~if not do:
~~~~~~~~~~~~break
~~~~print(text)
# Вывод:
id: GigabitEthernet1
index: 1
interface-name: ISP
type: GigabitEthernet
description: Broadband connection
traits: Peer
traits: Mac
traits: Ip
traits: Ip6
traits: Supplicant
traits: Ethernet
traits: GigabitEthernet
link: up
connected: yes
state: up
mtu: 1500
tx-queue-length: 2000
address:
mask:
uptime: 764695
global: yes
defaultgw: yes
priority: 700
security-level: public
mac:
auth-type: none
port, name 0:
id: GigabitEthernet1/0
index: 0
interface-name: 0
label: 0
type: Port
traits: EthernetPort
traits: GigabitEthernetPort
link: up
speed: 1000
duplex: full
auto-negotiation: on
flow-control: on
eee: off
cable-diagnostics: no
Name IP MAC Interface
DESKTOP-O7H02RC Home
ISP
Destination Gateway Interface F Metric
0.0.0.0/0 ISP U 0
0.0.0.0 Guest U 0
0.0.0.0 ISP U 0
0.0.0.0 Home U 0
Вы можете использовать мой код, просто меняя паттерны в списке patterns.
Так же настоятельно рекомендую изучить уроки:
https://pythoninfo.ru/osnovy/re
https://pythoninfo.ru/osnovy/str-python
https://pythoninfo.ru/osnovy/rabota-s-faylami
Простите,но я не совсем понимаю как это в полном виде должно выглядеть.Не сам итог а написание.
Я просто правда не понимаю как 1е со 2ым связывается чтобы была полноценная картина для удачного итога.
И уж прошу прощения за такую цж назойливость и наглость,но вопрос,как из этих данных сделать табличную форму?
Просто пишите первую часть, потом вторую (каждый знак «~» замените на пробел), вот так:
patterns = [
~~~~(‘[K\n’, »),
~~~~(‘[K’, »),
~~~~(‘\n\n\n’, ‘\n’),
~~~~(‘\n\n’, ‘\n’),
~~~~(‘=’, »),
~~~~(‘*’, »),
~~~~(‘\n ‘, ‘\n’),
]
with open(‘example.txt’, ‘r’) as f:
~~~~text = f.read()
~~~~while True:
~~~~~~~~do = False
~~~~~~~~for pattern in patterns:
~~~~~~~~~~~~if pattern[0] in text:
~~~~~~~~~~~~~~~~text = text.replace(*pattern)
~~~~~~~~~~~~~~~~do = True
~~~~~~~~if not do:
~~~~~~~~~~~~break
~~~~print(text)
input()
Создаёте файл example.txt, в него помещаете результаты работы скрипта, опрашивающего роутер. Помещаете этот файл и файл с кодом, приведённым выше в одну папку. Запускаете код и видите вывод в консоли как в предыдущем комментарии. Нажимаете «Enter» и консоль закрывается.
Для записи данных в таблицу Эксель проще всего воспользоваться библиотекой openpyxl. Это большая отдельная тема. Думаю, если Вы ознакомитесь с примерами из документации, сделать это будет не сложно.
https://openpyxl.readthedocs.io/en/stable/
Может не правильно выразился,просто данные нужны не списком а табличной формы,и желательно в Json.Короче чем дальше тем хуже
import json
patterns = [
~~~~(‘[K\n’, »),
~~~~(‘[K’, »),
~~~~(‘\n\n\n’, ‘\n’),
~~~~(‘\n\n’, ‘\n’),
~~~~(‘=’, »),
~~~~(‘*’, »),
~~~~(‘:’, »),
~~~~(‘\n ‘, ‘\n’),
]
with open(‘example.txt’, ‘r’) as f:
~~~~text = f.read()
~~~~while True:
~~~~~~~~do = False
~~~~~~~~for pattern in patterns:
~~~~~~~~~~~~if pattern[0] in text:
~~~~~~~~~~~~~~~~text = text.replace(*pattern)
~~~~~~~~~~~~~~~~do = True
~~~~~~~~if not do:
~~~~~~~~~~~~break
~~~~text_to_list = [i.split(‘ ‘) for i in text.split(‘\n’)]
with open(‘data.json’, ‘w’) as f:
~~~~json.dump(text_to_list, f)
Итоговый json будет расположен в файле data.json в той же папке.
Если так тоже не подходит, приведите более подробное описание необходимого результата и предоставьте пример.