Синхронный парсер интернет страниц

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

Мы будем собирать все заголовки с сайта https://www.python.org.

Начнём мы с главной страницы, а затем будем проходить по каждой внутренней ссылке на странице. С каждой из них мы будем собирать заголовок (то, что идёт в метатеге title).

Приведу код парсера, и объясню, как он работает.

Для работы данной программы необходимо установить сторонние библиотеки requests и beautifulsoup4. Сделать это можно командой pip install requests beautifulsoup4[lxml].
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from urllib.parse import urljoin, urlparse, urldefrag

import requests
from bs4 import BeautifulSoup


def fetch_and_validate_page_contents(url):
    while True:
        try:
            response = requests.get(
                url, timeout=15,
                stream=True
            )
            if "text/html" not in response.headers.get("Content-Type"):
                return None
            if urlparse(response.url).hostname != HOSTNAME:
                return None
            return response.text
        except requests.RequestException:
            continue


def parse_page(url):
    text = fetch_and_validate_page_contents(url)
    if text is None:
        return None, set()
    soup = BeautifulSoup(text, "lxml")
    title = soup.find("title")
    if title is not None:
        title = title.text
    all_internal_links = set()
    for a in soup.find_all("a"):
        link = a.get("href")
        if link is None:
            continue
        if link.startswith("#"):
            continue
        absolute_link = urljoin(WEBSITE, link)
        if urlparse(absolute_link).hostname == HOSTNAME:
            all_internal_links.add(
                urldefrag(absolute_link)[0]
            )
    return title, all_internal_links


def main():
    visited_urls = {}
    urls_to_visit = {WEBSITE}
    while urls_to_visit:
        current_url_to_parse = urls_to_visit.pop()
        title, internal_links = parse_page(current_url_to_parse)
        visited_urls[current_url_to_parse] = title
        # if len(visited_urls) > 100:
        #     break
        for url in internal_links:
            if url not in visited_urls:
                urls_to_visit.add(url)

    return visited_urls


if __name__ == "__main__":
    WEBSITE = "https://www.python.org"
    HOSTNAME = urlparse(WEBSITE).hostname
    titles = main()
    print(titles)

Пройдёмся по каждой из функций.

fetch_and_validate_page_contents

В бесконечном цикле получает заголовки ответа страницы с заданным адресом.

  1. Если это не html-контент (например, URL картинки, или исполняемого файла), то возвращаем None
  2. Если это перенаправление на внешний сайт (например, некоторые страницы https://www.python.org перенаправляют на https://peps.python.org), то также возвращаем None
  3. Если произошла ошибка запроса (таймаут сервера, или проблемы с интернет-соединением), то пробуем ещё раз. По-хорошему, для таких случаев необходимо настроить логирование, однако сейчас мы не будем углубляться в данную тему

parse_page

Для заданного адреса возвращает кортеж (заголовок, множество внутренних ссылок)

  1. Если страница невалидна, возвращаем заголовок None и пустое множество ссылок (считаем, что их там нет)
  2. Далее, с помощью BeautifulSoup обрабатываем HTML и ищем заголовок
  3. Находим все гиперссылки на странице, фильтруем их, игнорируя якорные ссылки (начинающиеся с #, эти ссылки по факту ведут на ту же страницу) и ссылки с другими доменами. Для каждой ссылки мы убираем фрагмент (то, что после #, например https://www.python.org/community/diversity/#diversity-appendix и https://www.python.org/community/diversity/ суть одна страница)
  4. Возвращаем заголовок страницы и множество внутренних ссылок

main

Главная функция, она, собственно, и собирает все заголовки страниц воедино.

  1. Словарь visited_urls служит для хранения уже посещенных URL и их заголовков
  2. Множество urls_to_visit необходимо для сбора ссылок, на которые ещё нужно перейти
  3. Пока есть URL для посещения, извлекаем один из них, и добавляем заголовок, полученный из Интернет-страницы в visited_urls
  4. Добавляем все найденные внутренние ссылки в urls_to_visit, если они еще не были посещены
  5. Возвращаем словарь всех посещенных URL и их заголовков

Главный блок

В главном блоке программы мы устанавливаем начальный URL (на самом деле, мы можем парсить не только python.org) и доменное имя (HOSTNAME).

Затем получаем у функции main() словарь заголовков и печатаем его.

Обратите внимание на строки 53-54. Поскольку сайт достаточно объёмный, его парсинг будет происходить очень долго. Можно раскомментировать данные строки, чтобы получить хоть какой-то результат за разумное время (будут посещены только первые 100 страниц).

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