Время прочтения: 4 мин.

Одна из задач, которая стояла в рамках проекта, нацеленного на исследование мер поисковой оптимизации (SEO, search engine optimization) информационных ресурсов дочерних структур организации, предполагала поиск всех ссылок и выявление среди них так называемых «мертвых (битых) ссылок», отсылающих на несуществующий сайт, страницу, файл, что в свою очередь понижает рейтинг информационного ресурса.

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

Посмотрим, как можно создать инструмент извлечения ссылок в Python, используя пакет requests и библиотеку BeautifulSoup. Итак,

Установим зависимости:

pip install requests bs4

Импортируем необходимые модули:

import requests
from urllib.parse import urlparse, urljoin
from bs4 import BeautifulSoup

Затем определим две переменные: одну для всех внутренних ссылок (это URL, которые ссылаются на другие страницы того же сайта), другую для внешних ссылок вэб-сайта (это ссылки на другие сайты).

# Инициализировать набор ссылок (уникальные ссылки)
int_url = set()
ext_url = set()

Далее создадим функцию для проверки URL – адресов. Это обеспечит правильную схему в ссылке — протокол, например, http или https  и имя домена в URL.

# Проверяем URL
def valid_url(url):
    parsed = urlparse(url)
    return bool(parsed.netloc) and bool(parsed.scheme)

На следующем шаге создадим функцию, возвращающую все действительные URL-адреса одной конкретной веб-страницы:

# Возвращаем все URL-адреса
def website_links(url):
    urls = set()
    # извлекаем доменное имя из URL
    domain_name = urlparse(url).netloc
    # скачиваем HTML-контент вэб-страницы
    soup = BeautifulSoup(requests.get(url).content, "html.parser")

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

    for a_tag in soup.findAll("a"):
        href = a_tag.attrs.get("href")
        if href == "" or href is None:
            # href пустой тег
            continue

В итоге получаем атрибут href и проверяем его. Так как не все ссылки абсолютные, возникает необходимость выполнить соединение относительных URL-адресов и имени домена. К примеру, когда найден href — «/search» и URL — «google.com» , то в результате получим «google.com/search».

# присоединить URL, если он относительный (не абсолютная ссылка)
        href = urljoin(url, href)

В следующем шаге удаляем параметры HTTP GET из URL-адресов:

parsed_href = urlparse(href)
# удалить параметры URL GET, фрагменты URL и т. д.
href = parsed_href.scheme + "://" + parsed_href.netloc + parsed_href.path

Завершаем функцию.

Если URL-адрес недействителен/URL уже находится в int_url , следует перейти к следующей ссылке.

Если URL является внешней ссылкой, вывести его и добавить в глобальный набор ext_url и перейдти к следующей ссылке.

И наконец, после всех проверок  получаем URL, являющийся  внутренней ссылкой; выводим ее и добавляем в наборы urls и int_url

        if not valid_url(href):
            # недействительный URL
            continue
        if href in int_url:
            # уже в наборе
            continue
        if domain_name not in href:
            # внешняя ссылка
            if href not in ext_url:
                print(f"[!] External link: {href}")
                ext_url.add(href)
            continue
        print(f"[*] Internal link: {href}")
        urls.add(href)
        int_url.add(href)
    return urls

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

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

# Количество посещенных URL-адресов
visited_urls = 0
# Просматриваем веб-страницу и извлекаем все ссылки.
def crawl(url, max_urls=50):
    # max_urls (int): количество макс. URL для сканирования
    global visited_urls
    visited_urls += 1
    links = website_links(url)
    for link in links:
        if visited_urls > max_urls:
            break
        crawl(link, max_urls=max_urls)

Итак, проверим на сайте, к которому имеется разрешение, как все это работает:

if __name__ == "__main__":
    crawl("https://newtechaudit.ru")
    print("[+] Total External links:", len(ext_url))
    print("[+] Total Internal links:", len(int_url))
    print("[+] Total:", len(ext_url) + len(int_url))

Вот фрагмент результата работы программы:

Обратите внимание, что многократный запрос к одному и тому же сайту за короткий промежуток времени может привести к тому, что ваш IP-адрес будет заблокирован. Ссылка на оригинал поста.