Извлечение всех ссылок на веб-странице — обычная задача для веб-парсеров, полезно создавать продвинутые парсеры, которые сканируют каждую страницу определенного веб-сайта для извлечения данных, его также можно использовать для процесса диагностики SEO или даже на этапе сбора информации для проникновения. тестеры. В этом руководстве я расскажу, как с нуля на Python создать инструмент для извлечения ссылок, используя только запросы и библиотеки BeautifulSoup.
Установим зависимости:
pip3 install requests bs4 colorama
Откроем новый файл Python и импортируем необходимые нам модули:
import requests from urllib.parse import urlparse, urljoin from bs4 import BeautifulSoup import colorama
Мы собираемся использовать colorama только для использования разных цветов при печати, чтобы различать внутренние и внешние ссылки:
# init the colorama module colorama.init() GREEN = colorama.Fore.GREEN GRAY = colorama.Fore.LIGHTBLACK_EX RESET = colorama.Fore.RESET YELLOW = colorama.Fore.YELLOW
Нам понадобятся две глобальные переменные, одна для всех внутренних ссылок сайта, а другая для всех внешних ссылок:
# initialize the set of links (unique links) internal_urls = set() external_urls = set()
- Внутренние ссылки — это URL-адреса, которые ведут на другие страницы того же веб-сайта.
- Внешние ссылки — это URL-адреса, которые ведут на другие веб-сайты.
Поскольку не все ссылки в тегах привязки (теги) действительны (я экспериментировал с этим), некоторые из них являются ссылками на части веб-сайта, некоторые — javascript, поэтому давайте наишем функцию для проверки URL-адресов:
def is_valid(url): """ Проверяет, является ли url допустимым """ parsed = urlparse(url) return bool(parsed.netloc) and bool(parsed.scheme)
Здесь проверяем наличие в URL-адресе правильной схемы (протокола, например http или https) и имени домена.
Теперь создадим функцию для возврата всех действительных URL-адресов веб-страницы:
def get_all_website_links(url): """ Возвращает все найденные URL-адреса на `url, того же веб-сайта. """ # все URL-адреса `url` urls = set() # доменное имя URL без протокола domain_name = urlparse(url).netloc soup = BeautifulSoup(requests.get(url).content, "html.parser")
Во-первых, инициализирована переменную urls
набора URL-адресов, использован set
Python, так-как нам не нужны лишние повторяющиеся ссылки.
Во-вторых, извлекается доменное имя из URL-адреса, которое понадобится для проверки полученных ссылок, на предмет внешних или внутренних.
В третьих, загружен HTML-контент веб-страницы, который обернул объектом soup
для облегчения синтаксического анализа HTML.
Получим все 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
Завершим функцию:
if not is_valid(href): # недействительный URL continue if href in internal_urls: # уже в наборе continue if domain_name not in href: # внешняя ссылка if href not in external_urls: print(f"{GRAY}[!] External link: {href}{RESET}") external_urls.add(href) continue print(f"{GREEN}[*] Internal link: {href}{RESET}") urls.add(href) internal_urls.add(href) return urls
В сухом остатке всё, что здесь сделано и проверяется:
- Если URL-адрес недействителен, перейдём к следующей ссылке.
- Если URL-адрес уже находится в
internal_urls
, нам он уже не нужен. - Если URL-адрес является внешней ссылкой, распечатаем её серым цветом и добавив в наш глобальный набор
external_urls
, перейдём к следующей ссылке.
Наконец, после всех проверок URL будет внутренней ссылкой, распечатываем её и добавляем в наши наборы urls
и internal_urls
.
Вышеупомянутая функция будет захватывать только ссылки одной конкретной страницы, что, если мы хотим извлечь все ссылки всего веб-сайта? Сделаем это:
# количество посещенных URL-адресов будет сохранено здесь total_urls_visited = 0 def crawl(url, max_urls=30): """ Сканирует веб-страницу и извлекает все ссылки. Все ссылки будут в глобальных переменных набора external_urls и internal_urls. параметры: max_urls (int): максимальное количество URL-адресов для сканирования, по умолчанию 30 """ global total_urls_visited total_urls_visited += 1 print(f"{YELLOW}[*] Crawling: {url}{RESET}") links = get_all_website_links(url) for link in links: if total_urls_visited > max_urls: break crawl(link, max_urls=max_urls)
Эта функция сканирует веб-сайт, что означает, что она получает все ссылки первой страницы, а затем рекурсивно вызывает себя, чтобы перейти по всем ранее извлеченным ссылкам. Однако это может вызвать некоторые проблемы, программа будет зависать на крупных веб-сайтах (на которых есть много ссылок), таких как google.com, поэтому, добавлен параметр max_urls
для выхода, когда мы достигаем определенного количества проверенных URL.
Хорошо, давайте проверим и убедимся, что вы используете это на веб-сайте, на который вы авторизованы, в противном случае я не несу ответственности за любой вред, который вы причиняете.
if __name__ == "__main__": crawl("https://www.thepythoncode.com") print("[+] Total Internal links:", len(internal_urls)) print("[+] Total External links:", len(external_urls)) print("[+] Total URLs:", len(external_urls) + len(internal_urls)) print("[+] Total crawled URLs:", max_urls)
Тестирую на этом сайте. Тем не менее, я настоятельно рекомендую вам не делать этого, это вызовет много запросов, приведет к переполнению веб-сервера и может заблокировать ваш IP-адрес.
Вот часть вывода:
Тут нужна картинка
После завершения сканирования будет выведено общее количество извлеченных и просканированных ссылок:
[+] Total Internal links: 90 [+] Total External links: 137 [+] Total URLs: 227 [+] Total crawled URLs: 30
Классно, правда? Я надеюсь, что этот рецепт будет вам полезен и вдохновит на создание подобных инструментов с использованием Python.
Я собрал все фрагменты и немного отредактировал код для сохранения найденных URL-адресов в файле, а также для получения URL-адреса из аргументов командной строки функции main, посмотрите содержимое файла link_extractor.py.
import requests from urllib.parse import urlparse, urljoin from bs4 import BeautifulSoup import colorama # запускаем модуль colorama colorama.init() GREEN = colorama.Fore.GREEN GRAY = colorama.Fore.LIGHTBLACK_EX RESET = colorama.Fore.RESET YELLOW = colorama.Fore.YELLOW # инициализировать set's для внутренних и внешних ссылок (уникальные ссылки) internal_urls = set() external_urls = set() total_urls_visited = 0 def is_valid(url): """ Проверка url """ parsed = urlparse(url) return bool(parsed.netloc) and bool(parsed.scheme) def get_all_website_links(url): """ Возвращает все найденные URL-адреса на `url, того же веб-сайта. """ # все URL-адреса `url` urls = set() # доменное имя URL без протокола domain_name = urlparse(url).netloc soup = BeautifulSoup(requests.get(url).content, "html.parser") for a_tag in soup.findAll("a"): href = a_tag.attrs.get("href") if href == "" or href is None: # пустой тег href continue # присоединяемся к URL, если он относительный (не абсолютная ссылка) href = urljoin(url, href) parsed_href = urlparse(href) # удалить параметры URL GET, фрагменты URL и т. д. href = parsed_href.scheme + "://" + parsed_href.netloc + parsed_href.path if not is_valid(href): # неверный URL continue if href in internal_urls: # уже в наборе continue if domain_name not in href: # внешняя ссылка if href not in external_urls: print(f"{GRAY}[!] Внешняя ссылка: {href}{RESET}") external_urls.add(href) continue print(f"{GREEN}[*] Внутреннея ссылка: {href}{RESET}") urls.add(href) internal_urls.add(href) return urls def crawl(url, max_urls=30): """ Сканирует веб-страницу и извлекает все ссылки. Вы найдете все ссылки в глобальных переменных набора external_urls и internal_urls. параметры: max_urls (int): максимальное количество URL-адресов для сканирования, по умолчанию 30. """ global total_urls_visited total_urls_visited += 1 print(f"{YELLOW}[*] Проверено: {url}{RESET}") links = get_all_website_links(url) for link in links: if total_urls_visited > max_urls: break crawl(link, max_urls=max_urls) if __name__ == "__main__": import argparse """ parser = argparse.ArgumentParser(description="Link Extractor Tool with Python") parser.add_argument("url", help="The URL to extract links from.") parser.add_argument("-m", "--max-urls", help="Number of max URLs to crawl, default is 30.", default=30, type=int) args = parser.parse_args() url = args.url max_urls = args.max_urls """ url = 'https://alumnus.susu.ru' max_urls = 500 crawl(url, max_urls=max_urls) print("[+] Total Internal links:", len(internal_urls)) print("[+] Total External links:", len(external_urls)) print("[+] Total URLs:", len(external_urls) + len(internal_urls)) print("[+] Total crawled URLs:", max_urls) domain_name = urlparse(url).netloc # сохранить внутренние ссылки в файле with open(f"{domain_name}_internal_links.txt", "w") as f: for internal_link in internal_urls: print(internal_link.strip(), file=f) # сохранить внешние ссылки в файле with open(f"{domain_name}_external_links.txt", "w") as f: for external_link in external_urls: print(external_link.strip(), file=f)
Есть некоторые веб-сайты, которые загружают большую часть своего контента с помощью JavaScript, в результате нам нужно вместо этого использовать библиотеку
from requests_html import HTMLSession from urllib.parse import urlparse, urljoin from bs4 import BeautifulSoup import colorama # init the colorama module colorama.init() GREEN = colorama.Fore.GREEN GRAY = colorama.Fore.LIGHTBLACK_EX RESET = colorama.Fore.RESET YELLOW = colorama.Fore.YELLOW # initialize the set of links (unique links) internal_urls = set() external_urls = set() total_urls_visited = 0 def is_valid(url): """ Checks whether `url` is a valid URL. """ parsed = urlparse(url) return bool(parsed.netloc) and bool(parsed.scheme) def get_all_website_links(url): """ Returns all URLs that is found on `url` in which it belongs to the same website """ # all URLs of `url` urls = set() # domain name of the URL without the protocol domain_name = urlparse(url).netloc # initialize an HTTP session session = HTMLSession() # make HTTP request & retrieve response response = session.get(url) # execute Javascript try: response.html.render() except: pass soup = BeautifulSoup(response.html.html, "html.parser") for a_tag in soup.findAll("a"): href = a_tag.attrs.get("href") if href == "" or href is None: # href empty tag continue # join the URL if it's relative (not absolute link) href = urljoin(url, href) parsed_href = urlparse(href) # remove URL GET parameters, URL fragments, etc. href = parsed_href.scheme + "://" + parsed_href.netloc + parsed_href.path if not is_valid(href): # not a valid URL continue if href in internal_urls: # already in the set continue if domain_name not in href: # external link if href not in external_urls: print(f"{GRAY}[!] External link: {href}{RESET}") external_urls.add(href) continue print(f"{GREEN}[*] Internal link: {href}{RESET}") urls.add(href) internal_urls.add(href) return urls def crawl(url, max_urls=30): """ Crawls a web page and extracts all links. You'll find all links in `external_urls` and `internal_urls` global set variables. params: max_urls (int): number of max urls to crawl, default is 30. """ global total_urls_visited total_urls_visited += 1 print(f"{YELLOW}[*] Crawling: {url}{RESET}") links = get_all_website_links(url) for link in links: if total_urls_visited > max_urls: break crawl(link, max_urls=max_urls) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="Link Extractor Tool with Python") parser.add_argument("url", help="The URL to extract links from.") parser.add_argument("-m", "--max-urls", help="Number of max URLs to crawl, default is 30.", default=30, type=int) args = parser.parse_args() url = args.url max_urls = args.max_urls crawl(url, max_urls=max_urls) print("[+] Total Internal links:", len(internal_urls)) print("[+] Total External links:", len(external_urls)) print("[+] Total URLs:", len(external_urls) + len(internal_urls)) print("[+] Total crawled URLs:", max_urls) domain_name = urlparse(url).netloc # save the internal links to a file with open(f"{domain_name}_internal_links.txt", "w") as f: for internal_link in internal_urls: print(internal_link.strip(), file=f) # save the external links to a file with open(f"{domain_name}_external_links.txt", "w") as f: for external_link in external_urls: print(external_link.strip(), file=f)
Многократный запрос одного и того же веб-сайта за короткий период времени может привести к тому, что веб-сайт заблокирует ваш IP-адрес. В этом случае вам необходимо использовать прокси-сервер для таких целей.
Если вы хотите вместо этого получать изображения, проверьте это руководство: Как загрузить все изображения с веб-страницы в Python, или, если вы хотите извлечь таблицы HTML, проверьте это руководство.
По мотивам
Как с помощью в Python извлечь все ссылки на веб‑сайты, опубликовано К ВВ, лицензия — Creative Commons Attribution-NonCommercial 4.0 International.
Респект и уважуха