Фон

Два общих вопроса, которые я получил от студентов, изучающих Python, были примерно следующего содержания: «Я понимаю синтаксис, но что я могу на самом деле делать с Python?» или «Где я могу найти больше практики Python?».

Чтобы ответить на оба вопроса, я решил начать писать статьи с базовых примеров Python. Таким образом, статьи, добавленные к «Основам Python», нацелены на то, чтобы помочь визуализировать универсальность языка, в то же время предоставляя объяснение кода для учебных целей.

Обзор

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

Используемая версия Python:
3.8.3

Прокомментированный код, доступный на GitHub:
https://github.com/bmaya1/python-basic-examples/blob/master/web-crawler/simple-web-crawler.py

Ссылка на документацию:

Обзор функций:

  1. Для этого простого поискового робота мы будем определять URL-адреса, выбирая теги привязки в HTML-коде веб-страницы. Это будет достигнуто путем создания подкласса HTMLParser и переопределения метода handle_starttag.
  2. После того, как синтаксический анализатор HTML установлен, нам необходимо:
  • Сделайте запрос к URL-адресу для его HTML-содержимого
  • Отправьте содержимое HTML на наш анализатор HTML и определите все новые URL-адреса.
  • Отслеживайте все посещенные URL
  • Повторите процесс для всех найденных новых URL-адресов, пока мы не проанализируем все URL-адреса или не будет достигнут предел сканирования.

Шаг 1. Создайте подкласс HTMLParser

Конструктор и базовые методы получения

Мы начнем этот класс с импорта необходимых модулей, присвоения имени подклассу AnchorParser и добавления конструктора.

from html.parser import HTMLParser
from urllib.request import urlopen
from urllib.parse import urljoin, urlparse
from urllib.error import HTTPError
from http.client import InvalidURL
from ssl import _create_unverified_context
class AnchorParser(HTMLParser):
    def __init__(self, baseURL = ""):
        # Parent class constructor
        HTMLParser.__init__(self)
        # Set that will contain all hyperlinks found in a webpage
        self.pageLinks = set()
        # The base url of the webpage to parse
        self.baseURL = baseURL

Классу AnchorParser теперь нужен метод получения, чтобы возвращать набор pageLinks.

def getLinks(self):
    return self.pageLinks

Затем нам нужно переопределить метод handle_starttag, чтобы он нацелен только на теги привязки.

Переопределение handle_starttag

Ниже приводится информация о методе handle_starttag из Документов Python:

HTMLParser.handle_starttag (тег, attrs)

Этот метод вызывается для обработки начала тега (например, <div id="main">).

Аргумент тег - это имя тега, преобразованное в нижний регистр. Аргумент attrs представляет собой список (name, value) пар, содержащих атрибуты, заключенные в <> скобки тега. имя будет переведено в нижний регистр, кавычки в значении удалены, а ссылки на символы и сущности заменены.

Например, для тега <A HREF="https://www.cwi.nl/"> этот метод будет называться handle_starttag('a', [('href', 'https://www.cwi.nl/')]).

Как видно выше, метод handle_starttag по существу анализирует начало тегов HTML. Для нашего кода мы хотим идентифицировать тег привязки ‹a›.

def handle_starttag(self, tag, attrs):
    if tag == "a":

После определения тега привязки аргумент attrs необходимо повторять до тех пор, пока не будет найден атрибут «href».

Что такое аргумент attrs?

Как видно из документации Python, attrs является аргументом для метода handle_starttag: «список пар (name, value)».

Предположим, следующий тег привязки находится в HTML веб-страницы:

<a href=”https://www.w3schools.com" target=”_blank”>Visit W3Schools.com!</a>

Code Taken From: https://www.w3schools.com/tags/tag_a.asp

После вызова метода handle_starttag список attrs будет иметь следующую структуру (имя, значение):

[('href', 'https://www.w3schools.com'), ('target', '_blank')]

Итерация по аргументу attrs

Цикл for ниже будет перебирать пары (name, value) до тех пор, пока не будет найден атрибут href:

for (attribute, value) in attrs:
    if attribute == "href":

urljoin - Обработка относительных и абсолютных путей

Значение href может быть абсолютным или относительным путем:

Absolute Path Example:
<a href="https://www.w3schools.com/test.html" target="_blank">Visit W3Schools.com!</a>
Relative Path Example:
<a href="/test.html" target="_blank">Visit W3Schools.com!</a>

Чтобы учесть обе возможности, в любом случае будет использоваться urllib.parse.urljoin для формирования абсолютного пути. Тогда наш код для handle_starttag будет выглядеть так:

def handle_starttag(self, tag, attrs):
    if tag == "a":
        for(attribute, value) in attrs:
            if attribute == "href":
                absoluteUrl = urljoin(self.baseURL, value)

Ниже приведен пример, показывающий, как urljoin будет действовать как с абсолютными, так и с относительными значениями href:

Absolute Path Example:
urljoin("https://www.w3schools.com","https://www.w3schools.com/test.html")
'https://www.w3schools.com/test.html'

Relative Path Example:
urljoin("https://www.w3schools.com", "/test.html")
'https://www.w3schools.com/test.html'

При использовании urljoin не имеет значения, имеет ли значение href абсолютный или относительный путь, результат для обоих случаев:

'https://www.w3schools.com/test.html'

Игнорирование значений href, которые не являются действительными URL-адресами

Любые значения href, которые не являются допустимыми URL-адресами (значения, не начинающиеся с http / https), следует игнорировать. Ниже приведены примеры значений, которые мы хотим игнорировать:

<a href="[email protected]"> Send Email Now! </a>
<a href="tel:+1111111111"> +111 111 1111 </a>

Можно добавить условие ниже, чтобы убедиться, что absoluteUrl содержит действительный URL:

if urlparse(absoluteUrl).scheme in ["http", "https"]:
    self.pageLinks.add(absoluteUrl)

Последняя строка кода (показанная выше) добавляет любой допустимый URL в набор pageLinks.

На этом завершается код класса AnchorParser.

Шаг 2. Создайте класс MyWebCrawler

Теперь, когда у нас есть анализатор HTML, нам нужно сосредоточиться на следующем:

  • Выполнение запроса к URL-адресу для его HTML-содержимого
  • Отправьте содержимое HTML в объект AnchorParser, чтобы идентифицировать любые новые URL-адреса.
  • Отслеживайте все посещенные URL
  • Повторите процесс для всех найденных новых URL-адресов, пока мы не проанализируем все URL-адреса или не будет достигнут предел сканирования.

Конструктор и базовые методы получения

Мы начнем с наименования нашего класса MyWebCrawler и создания конструктора.

class MyWebCrawler(object):
    def __init__(self, url, maxCrawl=10):
        self.visited = set() # To track all visited urls
        self.starterUrl = url # The starting url to parse
        self.max = maxCrawl # Max amount of URLs to visit

Затем нам понадобится метод получения, чтобы вернуть все посещенные URL-адреса.

def getVisited(self):
    return self.visited

К классу MyWebCrawler нужно добавить еще два метода:

parse(): Responsible for making a web request and using the AnchorParser class
crawl(): Responsible for tracking URLs visited, and repeating/stopping the crawling functionality

Запуск метода parse ()

Цель состоит в том, чтобы сделать веб-запрос к URL-адресу, получить его HTML-содержимое и передать это содержимое объекту AnchorParser.

Давайте начнем с подписи метода и использования urlopen для создания веб-запроса и получения HTML-содержимого веб-страницы.

def parse(self, url):
    htmlContent = urlopen(url, context=_create_unverified_context()).read().decode()

Note: context=_create_unverified_context() is used to ignore certificate validation when making the web request 

Создание экземпляра AnchorParser и подачи в HTML

После того, как веб-запрос сделан для содержимого HTML, мы можем использовать класс AnchorParser, чтобы помочь нам проанализировать HTML.

Сначала мы создаем объект AnchorParser:

parser = AnchorParser(url)

Затем мы загружаем HTML-контент в объект AnchorParser:

parser.feed(htmlContent)

Метод parse () будет действовать как вспомогательный метод, и поэтому мы должны возвращать все URL-адреса, собранные нашим объектом AnchorParser:

return parser.getLinks()

Базовая обработка исключений для parse ()

Следует иметь в виду, что веб-запрос может привести к ошибкам. Чтобы учесть это, мы добавим очень простую обработку исключений. Текущий написанный код будет помещен в блок try. В этой статье мы сосредоточимся только на обнаружении следующих исключений: HTTPError, InvalidURL, UnicodeDecodeError

После добавления блоков try / except метод parse () должен выглядеть следующим образом:

def parse(self, url):
    try:
        htmlContent = urlopen(url, context=_create_unverified_context()).read().decode()
        parser = AnchorParser(url)
        parser.feed(htmlContent)
        return parser.getLinks()
    except (HTTPError, InvalidURL, UnicodeDecodeError):
        print("FAILED: {}".format(url))
        return set() # Returns an empty set

На этом код метода parse () завершен.

Запуск метода crawl ()

Цель состоит в том, чтобы отслеживать все посещенные URL-адреса и либо повторить, либо остановить сканирование.

Давайте начнем с сигнатуры метода и нового набора для отслеживания URL-адресов, которые еще нужно посетить.

def crawl(self):
    urlsToParse = {self.starterUrl}

Затем нам понадобится цикл, чтобы функция сканирования работала до тех пор, пока не будут посещены все URL-адреса или пока не будет достигнут максимальный предел сканирования.

while(len(urlsToParse) > 0 and len(self.visited) < self.max):

В цикле мы хотим получить следующий URL и удалить его из urlsToParse. Мы также хотим избегать URL-адресов, которые уже были посещены.

nextUrl = urlsToParse.pop()
if nextUrl not in self.visited:

Последние три строки кода добавлены по следующим причинам:

  • После подтверждения того, что URL новый и не был посещен, мы отследим его, добавив его в набор посещенных.
  • Оператор печати может быть добавлен для просмотра того, какой URL будет анализироваться.
  • Мы должны вызвать вспомогательный метод parse (). Метод parse () выполняет веб-запрос, получает содержимое HTML и передает содержимое HTML в объект AnchorParser.
self.visited.add(nextUrl)
print("Parsing: {}".format(nextUrl))
urlsToParse |= self.parse(nextUrl)

На этом завершается код метода crawl ().

Это все!

Лучший способ понять код - это просмотреть его, написать и поэкспериментировать. Чтобы помочь с этим, код доступен на GitHub и включает несколько комментариев, в которых обобщаются функциональные возможности. Надеюсь, эта статья поможет всем, кому нужно было разобраться в коде.