Написание Веб-пауков

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

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

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

Перемещение по одному домену

Даже если вы не слышали о «Шести степенях Википедии», вы почти наверняка слышали о ее названии — «Шесть степеней Кевина Бэкона». В обоих играх целью является связывание двух непохожих объектов (в первом случае — статей Википедии, которые связаны между собой, а во втором случае — актеров, снимавшихся в одном фильме) цепочкой, содержащей не более шести объектов (включая два исходных).

Например, Эрик Айдл снялся в «Дадли-Ду-Райт» вместе с Бренданом Фрейзером, который снялся в «Воздух, который я дышу» вместе с Кевином Бэконом. В этом случае цепочка от Эрика Айдла до Кевина Бэкона состоит всего из трех объектов.

В этом разделе вы начнете проект, который станет решателем задачи «Шести степеней Википедии»: вы сможете взять страницу Эрика Айдла и найти минимальное количество кликов по ссылкам, которые приведут вас на страницу Кевина Бэкона.

Но что насчет нагрузки на серверы Википедии?

Согласно Фонду Викимедиа (родительской организации, стоящей за Википедией), веб-сайты Википедии получают примерно 2500 обращений в секунду, более 99% из которых приходится на домен Википедии (см. раздел «Объем трафика» на странице «Викимедиа в цифрах«). Из-за огромного объема трафика ваши веб-скрейперы вряд ли окажут заметное влияние на нагрузку серверов Википедии.

Также имейте в виду, что если вы планируете большой проект, связанный с данными из Википедии, вам следует проверить, доступны ли эти данные уже через API Википедии. Википедия часто используется в качестве веб-сайта для демонстрации скрейперов и веб-пауков, потому что у нее простая структура HTML и она относительно стабильна. Однако ее API часто делает доступ к этим же данным более эффективным.

Вы должны уже уметь писать скрипт на Python, который извлекает произвольную страницу из Википедии и создает список ссылок на этой странице:

from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen('http://en.wikipedia.org/wiki/Kevin_Bacon')
bs = BeautifulSoup(html, 'html.parser')

for link in bs.find_all('a'):
    if 'href' in link.attrs:
        print(link.attrs['href'])

Если вы посмотрите на полученный список ссылок, то заметите, что там есть все статьи, которые вы ожидаете: «Apollo 13», «Philadelphia», «Primetime Emmy Award» и так далее. Однако там также есть и ненужные вещи:

//wikimediafoundation.org/wiki/Privacy_policy
//en.wikipedia.org/wiki/Wikipedia:Contact_us

Фактически, Википедия полна ссылок из боковой панели, нижнего колонтитула и верхнего колонтитула, которые появляются на каждой странице, а также ссылок на страницы категорий, страницы обсуждений и другие страницы, которые не содержат разных статей:

/wiki/Category:Articles_with_unsourced_statements_from_April_2014
/wiki/Talk:Kevin_Bacon

Недавно мой друг, работая над похожим проектом скрапинга Википедии, упомянул, что он написал большую функцию фильтрации, длиной более 100 строк кода, чтобы определить, является ли внутренняя ссылка Википедии страницей статьи. К сожалению, он не потратил много времени в начале на поиск общих черт между «ссылками на статьи» и «другими ссылками», иначе он мог бы обнаружить хитрость. Если вы рассмотрите ссылки, которые указывают на страницы статей (в отличие от других внутренних страниц), то увидите, что у них есть три общих черты:

  • Они находятся внутри div с идентификатором bodyContent.
  • URL не содержат двоеточий.
  • URL начинаются с /wiki/.

Вы можете использовать эти правила, чтобы немного изменить код и получить только желаемые ссылки на статьи с помощью регулярного выражения:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

html = urlopen('http://en.wikipedia.org/wiki/Kevin_Bacon')
bs = BeautifulSoup(html, 'html.parser')

for link in bs.find('div', {'id':'bodyContent'}).find_all('a', href=re.compile('^(/wiki/)((?!:).)*$')):
    if 'href' in link.attrs:
        print(link.attrs['href'])

Если вы запустите это, вы увидите список всех URL статей, на которые ссылается статья в Википедии о Кевине Бейконе.

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

  • Одна функция getLinks, которая принимает URL статьи Википедии в формате /wiki/<Название_статьи> и возвращает список всех связанных URL статей в том же формате.
  • Основная функция, которая вызывает getLinks с начальной статьей, выбирает случайную ссылку на статью из возвращенного списка и снова вызывает getLinks, пока вы не остановите программу или пока на новой странице не будет найдено ни одной ссылки на статью.

Вот полный код, который выполняет это:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import datetime
import random
import re

random.seed(datetime.datetime.now())

def getLinks(articleUrl):
    html = urlopen('http://en.wikipedia.org{}'.format(articleUrl))
    bs = BeautifulSoup(html, 'html.parser')
    return bs.find('div', {'id':'bodyContent'}).find_all('a', href=re.compile('^(/wiki/)((?!:).)*$'))

links = getLinks('/wiki/Kevin_Bacon')

while len(links) > 0:
    newArticle = links[random.randint(0, len(links)-1)].attrs['href']
    print(newArticle)
    links = getLinks(newArticle)

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

Псевдослучайные числа и случайные зерна (сид)

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

Поэтому случайные числа могут быть вызовом. Большинство алгоритмов генерации случайных чисел стремятся создавать равномерно распределенные и труднопредсказуемые последовательности чисел, но для этого нужно «зерно»(сид) числа, чтобы дать этим алгоритмам что-то, с чем работать изначально. Точно такое же зерно произведет точно такую же последовательность «случайных» чисел каждый раз, поэтому я использовал системные часы как отправную точку для создания новых последовательностей случайных чисел и, следовательно, новых последовательностей случайных статей.

Это делает программу немного более интересной для запуска.

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

Затем программа определяет функцию getLinks, которая принимает URL статьи в формате /wiki/…, добавляет к нему доменное имя Википедии, http://en.wikipedia.org, и извлекает объект BeautifulSoup для HTML на этом домене. Затем она извлекает список тегов ссылок на статьи на основе ранее обсужденных параметров и возвращает их.

Основная часть программы начинается с установки списка тегов ссылок на статьи (переменная links) в список ссылок на исходной странице: https://en.wikipedia.org/wiki/Kevin_Bacon. Затем она переходит в цикл, находит случайный тег ссылки на статью на странице, извлекает из него атрибут href, печатает страницу и получает новый список ссылок из извлеченного URL.

Конечно, решение задачи «Шесть степеней Википедии» требует не только создания скрепера, который переходит с страницы на страницу. Вы также должны уметь хранить и анализировать полученные данные. Для продолжения решения этой проблемы см. раздел 6 — Хранение данных.

Обработайте ваши исключения! Хотя эти примеры кода опускают большую часть обработки исключений во имя краткости, имейте в виду, что могут возникнуть множество потенциальных проблем. Что если Википедия изменит имя тега bodyContent, например? Когда программа попытается извлечь текст из этого тега, возникнет AttributeError.

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

Поиск по всему сайту

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

Однако вы можете изучить поведение таких приложений без их полномасштабного запуска. Чтобы узнать больше о запуске этих приложений с использованием базы данных, см. раздел 6.

Тёмная и глубокая Сети (даркнет и дипнет)

Вы, вероятно, слышали много раз о терминах глубокая сеть, тёмная сеть или скрытая сеть, особенно в последнее время в СМИ. Что они означают?

Глубокая сеть — это любая часть сети, не входящая в поверхностную сеть. Поверхностная сеть — это часть интернета, индексируемая поисковыми системами. Оценки варьируются, но глубокая сеть, скорее всего, составляет около 90% интернета. Поскольку Google не может выполнять такие вещи, как отправка форм, нахождение страниц, на которые не ссылались на верхнем уровне домена, или исследование сайтов, на которых robots.txt запрещает это, поверхностная сеть остаётся относительно небольшой.

Тёмная сеть, также известная как даркнет, — это совершенно другое явление. Она работает на существующей аппаратной инфраструктуре сети, но использует Tor или другой клиент с протоколом приложения, работающим поверх HTTP и обеспечивающим защищённый канал для обмена информацией.

Хотя возможно скрапить тёмную сеть, как любой другой веб-сайт, это выходит за рамки этой статьи.

В отличие от тёмной сети, дипнет относительно легко скрапить. Многие инструменты в этой статье научат вас краулингу и скрапингу информации с многих мест, куда боты Google не могут попасть.

Когда краулинг всего веб-сайта может быть полезен, а когда вреден?

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

Генерация карты сайта

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

Сбор данных

Другой мой клиент хотел собрать статьи (истории, блоги, новостные статьи и т. д.), чтобы создать рабочий прототип специализированной поисковой платформы. Хотя для этих веб-скрэпингов не требовалось исчерпывающего обхода, они должны были быть достаточно обширными (нас интересовали данные только с нескольких сайтов). Я смог создать краулеры, которые рекурсивно обходили каждый сайт и собирали только данные, найденные на страницах статей. Общий подход к исчерпывающему обходу сайта состоит в том, чтобы начать с верхнего уровня (например, домашней страницы) и искать список всех внутренних ссылок на этой странице. Затем каждая из этих ссылок обходится, и на каждой из них находятся дополнительные списки ссылок, вызывая еще один раунд краулинга.

Очевидно, что эта ситуация может быстро выйти из-под контроля. Если на каждой странице есть 10 внутренних ссылок, а веб-сайт имеет глубину в 5 страниц (довольно типичная глубина для среднего сайта), то количество страниц, которые вам нужно просканировать, составит 10^5, или 100 000 страниц, прежде чем вы сможете быть уверены, что исчерпывающе охватили веб-сайт. Интересно, что хотя «5 страниц глубиной и 10 внутренних ссылок на страницу» являются достаточно типичными размерами для веб-сайта, очень мало веб-сайтов имеют 100 000 или более страниц.

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

Набор похож на список, но элементы не имеют определенного порядка, и будут храниться только уникальные элементы, что идеально подходит для наших нужд. Подлежат сканированию и поиску только ссылки, которые «новые»:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
pages = set()
def getLinks(pageUrl):
    global pages
    html = urlopen('http://en.wikipedia.org{}'.format(pageUrl))
    bs = BeautifulSoup(html, 'html.parser')
    for link in bs.find_all('a', href=re.compile('^(/wiki/)')):
        if 'href' in link.attrs:
            if link.attrs['href'] not in pages:
                #Мы столкнулись с новой страницей
                newPage = link.attrs['href']
                print(newPage)
                pages.add(newPage)
                getLinks(newPage)
getLinks('')

Чтобы показать вам полный эффект того, как работает этот бизнес по скрапингу веба, я расслабил стандарты того, что является внутренней ссылкой (по сравнению с предыдущими примерами). Вместо того, чтобы ограничить скрейпер статьями, он ищет все ссылки, начинающиеся с /wiki/, независимо от того, где они находятся на странице, и независимо от того, содержат ли они двоеточия.

Помните: статьи не содержат двоеточий в URL, но страницы загрузки файлов, страницы обсуждения и подобные содержат двоеточия в URL.

Изначально функция getLinks вызывается с пустым URL. Это интерпретируется как «главная страница Википедии», так как пустой URL дополняется http://en.wikipedia.org внутри функции.

Затем каждая ссылка на первой странице проходится в цикле, и проверяется, есть ли она в глобальном наборе страниц (наборе страниц, с которыми скрипт уже сталкивался). Если ее там нет, она добавляется в список, выводится на экран, и функция getLinks вызывается рекурсивно для нее.

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

У Python есть предельный предел рекурсии (количество раз, которое программа может рекурсивно вызывать саму себя) — 1000. Поскольку сеть ссылок Википедии чрезвычайно обширна, эта программа в конечном итоге достигнет этого предельного предела рекурсии и остановится, если вы не добавите счетчик рекурсии или что-то подобное, чтобы предотвратить это.

Для «плоских» сайтов, глубина которых меньше 1000 ссылок, этот метод обычно хорошо работает, за некоторыми исключениями. Например, я однажды столкнулся с ошибкой в динамически генерируемом URL, который зависел от адреса текущей страницы для записи ссылки на эту страницу. Это приводило к бесконечно повторяющимся путям вроде /blogs/blogs…/blogs/blog-post.php.

В большинстве случаев, однако, этот рекурсивный метод должен быть приемлемым для любого типичного веб-сайта, с которым вы, вероятно, столкнетесь.

Сбор данных по всему сайту

Веб-скрейперы были бы довольно скучными, если бы они просто переходили с одной страницы на другую. Чтобы они стали полезными, нужно иметь возможность что-то делать на странице, находясь на ней. Давайте посмотрим, как создать скрейпер, который собирает заголовок, первый абзац контента и ссылку на редактирование страницы (если доступно).

Как всегда, первый шаг — определить, как лучше всего это сделать, — это изучить несколько страниц сайта и выявить закономерность. Изучив несколько страниц Википедии (как статьи, так и страницы, не являющиеся статьями, например, страница с политикой конфиденциальности), следующее становится ясным: • Все заголовки (на всех страницах, независимо от их статуса как страницы статьи, страницы истории редактирования или любой другой страницы) имеются под тегами h1 → span, и это единственные теги h1 на странице. • Как упоминалось ранее, весь текст контента находится под тегом div#bodyContent. Однако, если вы хотите быть более конкретным и получить только первый абзац текста, вам может быть лучше использовать div#mw-content-text → p (выбор только первого тега абзаца). Это верно для всех страниц с контентом, за исключением страниц файлов (например, https://en.wikipedia.org/wiki/File:Orbit_of_274301_Wikipedia.svg), которые не имеют разделов текста контента. • Ссылки для редактирования появляются только на страницах статей. Если они есть, они будут найдены в теге li#ca-edit, под li#ca-edit → span → a.

Изменив нашу основную программу для сканирования, вы можете создать комбинированную программу для сканирования/сбора данных (или, по крайней мере, для вывода данных):

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
pages = set()

def getLinks(pageUrl):
    global pages
    html = urlopen('http://en.wikipedia.org{}'.format(pageUrl))
    bs = BeautifulSoup(html, 'html.parser')
    try:
        print(bs.h1.get_text())
        print(bs.find(id ='mw-content-text').find_all('p')[0])
        print(bs.find(id='ca-edit').find('span').find('a').attrs['href'])
    except AttributeError:
        print('На этой странице что-то отсутствует! Продолжаем.')
    for link in bs.find_all('a', href=re.compile('^(/wiki/)')):
        if 'href' in link.attrs:
            if link.attrs['href'] not in pages:
                #Мы столкнулись с новой страницей
                newPage = link.attrs['href']
                print('-'*20)
                print(newPage)
                pages.add(newPage)
                getLinks(newPage)

getLinks('')

Цикл for в этой программе по сути такой же, как и в исходной программе для сканирования (с добавлением вывода черточек для ясности, разделяющих выводимый контент).

Поскольку нельзя быть полностью уверенным, что на каждой странице есть все данные, каждый оператор print расположен в порядке, в котором они, вероятно, появятся на сайте. То есть тег заголовка h1 появляется на каждой странице (по крайней мере, насколько я могу судить), поэтому первым извлекается эта информация. Текстовый контент появляется на большинстве страниц (за исключением страниц файлов), поэтому это вторая получаемая информация. Кнопка Редактировать появляется только на страницах, где уже есть и заголовок, и текстовый контент, но она не появляется на всех таких страницах.

Очевидно, использование обработчика исключений для нескольких строк кода сопряжено с определенными опасностями. Во-первых, нельзя определить, какая именно строка вызвала исключение. Кроме того, если по какой-то причине на странице есть кнопка «Редактировать», но нет заголовка, кнопка «Редактировать» никогда не будет зарегистрирована. Однако это достаточно для многих случаев, когда есть порядок вероятности появления элементов на сайте, и ненамеренное упущение нескольких точек данных или ведение подробных журналов не является проблемой.

Вы могли заметить, что в этом и всех предыдущих примерах вы скорее «выводили» данные, чем «собирали» их. Очевидно, что данные в вашем терминале трудно обрабатывать. В разделе 6 вы подробнее рассмотрите сохранение информации и создание баз данных.

Редиректы

Редиректы позволяют веб-серверу указывать на одно доменное имя или URL, чтобы перенаправить на контент в другом месте. Существуют два типа редиректов:

  • Редиректы на стороне сервера, где URL изменяется перед загрузкой страницы.
  • Редиректы на стороне клиента, иногда сопровождающиеся сообщением типа «Вы будете перенаправлены через 10 секунд», где страница загружается перед перенаправлением на новую.

С редиректами на стороне сервера обычно нет необходимости беспокоиться. Если вы используете библиотеку urllib с Python 3.x, она автоматически обрабатывает редиректы! Если вы используете библиотеку requests, убедитесь, что флаг allow_redirects установлен в True:

r = requests.get('http://github.com', allow_redirects=True)

Просто имейте в виду, что иногда URL страницы, которую вы сканируете, может не совпадать с URL, на который вы вошли.

Дополнительную информацию о редиректах на стороне клиента, которые выполняются с использованием JavaScript или HTML, см. в разделе 12.

Краулинг множества сайтов в интернете

Каждый раз, когда я выступаю на тему веб-скрапинга, кто-то неизменно спрашивает: «Как построить Google?» Мой ответ всегда двойной: «Во-первых, вам нужно получить много миллиардов долларов, чтобы купить самые большие на земле хранилища данных и разместить их в скрытых местах по всему миру. Во-вторых, вам нужно создать веб-краулер.»

Когда Google начинал в 1996 году, это были всего лишь два выпускника Стэнфорда с старым сервером и веб-краулером на Python. Теперь, когда вы знаете, как скрести веб, у вас официально есть все инструменты, чтобы стать следующим много миллиардером в сфере технологий!

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

Точно так же, как и в предыдущем примере, веб-краулеры, которые вы собираетесь создать, будут следовать за ссылками с страницы на страницу, составляя карту веба. Но на этот раз они не будут игнорировать внешние ссылки; они будут следовать за ними.

Имейте в виду, что код из следующего раздела может отправить вас куда угодно в интернете. Если мы чему-то научились из «Шести степеней от Википедии», так это тому, что совершенно возможно попасть с сайта вроде http://www.sesamestreet.org/ на что-то менее приятное всего за несколько переходов.

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

Прежде чем приступить к написанию веб-сканера, который без разбору следует по всем внешним ссылкам, вам следует задать себе несколько вопросов:

  • Какие данные я пытаюсь собрать? Могу ли я это сделать, просто сканируя несколько предопределенных веб-сайтов (почти всегда более простой вариант), или мой веб-сканер должен иметь возможность обнаруживать новые веб-сайты, о которых я могу не знать?
  • Когда мой веб-сканер достигает определенного веб-сайта, он сразу переходит к следующей внешней ссылке на новый веб-сайт, или останавливается ненадолго, чтобы подробно исследовать текущий веб-сайт?
  • Есть ли какие-либо условия, при которых я не хочу сканировать конкретный сайт? Меня интересует контент не на английском языке?
  • Как я защищаю себя от правовых действий, если мой веб-сканер привлечет внимание веб-мастера на одном из сайтов, которые он обнаружит? (Подробнее об этом смотрите в главе 18.)

Гибкий набор функций на Python, который можно объединить для выполнения различных типов веб-сканирования, можно легко написать в меньше чем 60 строк кода:

from urllib.request import urlopen
from urllib.parse import urlparse
from bs4 import BeautifulSoup
import re
import datetime
import random

pages = set()
random.seed(datetime.datetime.now())

# Получает список всех внутренних ссылок, найденных на странице
def getInternalLinks(bs, includeUrl):
    includeUrl = '{}://{}'.format(urlparse(includeUrl).scheme, urlparse(includeUrl).netloc)
    internalLinks = []
    # Находит все ссылки, которые начинаются с "/"
    for link in bs.find_all('a', href=re.compile('^(/|.*'+includeUrl+')')):
        if link.attrs['href'] is not None:
            if link.attrs['href'] not in internalLinks:
                if(link.attrs['href'].startswith('/')):
                    internalLinks.append(includeUrl+link.attrs['href'])
                else:
                    internalLinks.append(link.attrs['href'])
    return internalLinks

# Получает список всех внешних ссылок, найденных на странице
def getExternalLinks(bs, excludeUrl):
    externalLinks = []
    # Находит все ссылки, которые начинаются с "http", но не содержат текущий URL
    for link in bs.find_all('a', href=re.compile('^(http|www)((?!'+excludeUrl+').)*$')):
        if link.attrs['href'] is not None:
            if link.attrs['href'] not in externalLinks:
                externalLinks.append(link.attrs['href'])
    return externalLinks

def getRandomExternalLink(startingPage):
    html = urlopen(startingPage)
    bs = BeautifulSoup(html, 'html.parser')
    externalLinks = getExternalLinks(bs, urlparse(startingPage).netloc)
    if len(externalLinks) == 0:
        print('No external links, looking around the site for one')
        domain = '{}://{}'.format(urlparse(startingPage).scheme, urlparse(startingPage).netloc)
        internalLinks = getInternalLinks(bs, domain)
        return getRandomExternalLink(internalLinks[random.randint(0, len(internalLinks)-1)])
    else:
        return externalLinks[random.randint(0, len(externalLinks)-1)]

def followExternalOnly(startingSite):
    externalLink = getRandomExternalLink(startingSite)
    print('Random external link is: {}'.format(externalLink))
    followExternalOnly(externalLink)

followExternalOnly('http://oreilly.com')

В этой программе страница начинается с http://oreilly.com и случайным образом переходит от внешней ссылки к внешней ссылке. Вот пример вывода, который он создает:

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

Не стоит забывать, что приведенные в этой статье примеры программ не всегда содержат необходимые проверки и обработку исключений, необходимые для кода, готового к производству. Например, если внешняя ссылка не будет найдена на веб-сайте, который обходит этот веб-сканер (что маловероятно, но рано или поздно может произойти, если его запускать достаточно долго), этот программный код будет продолжать работу, пока не достигнет предельной глубины рекурсии в Python.

Простой способ повысить надежность этого веб-сканера — объединить его с кодом обработки исключений при подключении, описанным в разделе 1. Это позволит коду выбирать другой URL для перехода в случае возникновения ошибки HTTP или исключения сервера при получении страницы.

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

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

# Собирает список всех внешних URL, найденных на сайте
allExtLinks = set()
allIntLinks = set()
def getAllExternalLinks(siteUrl):
    html = urlopen(siteUrl)
    domain = '{}://{}'.format(urlparse(siteUrl).scheme,
                              urlparse(siteUrl).netloc)
    bs = BeautifulSoup(html, 'html.parser')
    internalLinks = getInternalLinks(bs, domain)
    externalLinks = getExternalLinks(bs, domain)
    for link in externalLinks:
        if link not in allExtLinks:
            allExtLinks.add(link)
            print(link)
    for link in internalLinks:
        if link not in allIntLinks:
            allIntLinks.add(link)
            getAllExternalLinks(link)

getAllExternalLinks('http://oreilly.com')

Этот код можно представить как два цикла — один собирает внутренние ссылки, а другой — внешние ссылки — работают в согласовании друг с другом. Схема алгоритма выглядит примерно как на рисунке.Блок-схема алгоритма сбора ссылок

Схема алгоритма для веб-сканера сайта, собирающего все внешние ссылки

Запись или создание диаграмм того, что должен делать код, до того, как вы начнете писать сам код, — отличная привычка, в которую стоит войти. Это может сэкономить вам много времени и избавить от разочарований, когда ваши веб-сканеры становятся более сложными.

Понравилась статья? Поделиться с друзьями:
Школа Виктора Комлева
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.