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

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

Итак, начнем.

Для работы нам потребуются библиотеки:

  • request
  • BeautifulSoup

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

В нашем скрипте будем использовать получение данных при помощи HTTP-метода GET. Метод GET является инструкцией web-ресурсу о том, что с него запрашиваются некоторые данные, указанные как параметр к GET. Чтобы выполнить такой запрос, используя библиотеку requests, достаточно ввести requests.get ( <URL>; <дополнительные параметры> ). При существовании URL и при наличии прав на доступ к ресурсу, в ответ придут данные HTML-страницы, которые уже можно обработать различными инструментами языка Python. Натболее распространенным инструментом для данной задачи является библиотека BeautifulSoup.

Библиотека BeautifulSoup используется для анализа документов HTML и XML. Она создает дерево синтаксического анализа для полученных страниц (DOM-дерево), которое можно использовать для извлечения данных из HTML в удобные для обработки на языке Python конструкции (словари, списки, строки), что полезно для очистки веб-страниц от ненужной информации.

Алгоритм работы нашей программы будет заключаться в том, что при запуске скрипта Python из командной строки и с указанием URL требуемой нам статьи в качестве параметра, мы будем получать HTML-страницу при помощи метода get библиотеки request. Затем создаем объект BeautifulSoup, который непосредственно и помогает нам очистить статью от лишней информации.

Но для того, чтобы обозначить, что именно считать лишней информацией, а что полезной, нам необходимо проанализировать источник, из которого мы будем брать статьи. Сейчас распространенной практикой является разработка web-ресурсов в блочном формате. Все данные, которые необходимо разместить на страницах разбиваются по логическим блокам и помещаются в теги <div> </div>. Далее, уже внутри этих блоков информация может принимать различные вариации в зависимости от назначения. В блоках, предназначенных для навигации преобладают ссылочные теги <a> </a>, в блоках, где имеется много изображений, соответственно, много тегов <img>. Нас же интересуют блоки web-страниц, в которых хранятся тексты статей.

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

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

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

class GrabberArticle:
    url = ""
    filename = ""
    path = ""
    content_tags = ['p']
    wrap = 80

Описание объектов:

url – URL статьи, которую хотим обработать.

filename – имя файла для сохранения.  Соответствует последнему элементу в URL. Например, для  host.net/path1/path2 — это path2

path – Путь для сохранения обработанного текста. Соответствует ([Каталог с файлом скрипта]/host.net/path1/path2/…)

content_tags – HTML-теги для обработки. По умолчанию установлен тег <p>, так как мы приняли его за основной тег, в котором наиболее часто размещён основной полезный контент статьи.

wrap – количество символов в строке очищенного текста. По умолчанию установлено значение в 80 символов — негласное правило читаемости текста на экране.

Давайте теперь напишем главный метод для нашего класса GrabberArticle, который будет получать ответ от web-ресурса и при помощи BeautifulSoup отфильтровывать только нужные нам данные:

def get_text(self):
    r = requests.get(self.url).text
    soup = BeautifulSoup(r, 'html.parser')
    # найдем все теги по списку self.content_tags
    content = soup.find_all(self.content_tags)
    wrapped_text = ""
    for p in content:
        # пропускаем теги без значений
        if p.text != '':
            # форматирование ссылок в вид [ссылка]
            links = p.find_all('a')
            if links != '':
                for link in links:
                    p.a.replace_with(link.text + str("[" + link['href'] + "]"))
            # устанавливаем ширину строки равной self.wrap (по умолчанию, 80 символов)
            wrapped_text += ''.join(textwrap.fill(p.text, self.wrap)) + "\n\n"
    self.write_in_file(wrapped_text)

В качестве результата будем выводить текстовый файл, который будет называться по имени последнего элемента URL- адреса. Также создадим иерархию для размещения этого файла в файловой системе, исходя из оставшихся элементов URL-адреса. Например, для URL: https://lenta.ru/news/2020/04/09/centenarian : файл будет сохранен под именем centenarian.txt в каталоге [Директория со скриптом]/lenta.ru/news/2020/04/09/. Такая иерархия на первый взгляд может выглядеть избыточной. Но такой подход может быть полезен, если с одного и того же источника (и тем более нескольких) будет выгружаться большое количество статей. В такой структуре данные будет легко сгруппировать по определенным признакам.  Это позволит гораздо проще обращаться к массиву данных из статей, если, например, они будут использованы для проведения тематических исследований с помощью моделей машинного обучения.

def __init__(self, url_address):
    self.url = url_address
    # Get path and filename for saving article by splitting URL.
    # If the URL ends with some.html, then the previous (-2) element
    # of the path is taken to form the path and the filename = some.html.txt respectively.
    path_arr = self.url.split('/')
    if path_arr[-1] != '':
        self.filename = path_arr[-1] + ".txt"
        self.path = os.getcwd() + "/".join(path_arr[1:-1])
    else:
        self.filename = path_arr[-2] + ".txt"
        self.path = os.getcwd() + "/".join(path_arr[1:-2])
    if not os.path.exists(self.path):
        os.makedirs(self.path)

Допишем метод, который будет записывать в файл полученный «чистый» текст:

def write_in_file(self, text):
    # записывает text в каталог self.path:"[CUR_DIR]/host.ru/path_item1/path_item2/..."
    file = open(str(self.path) + '/' + str(self.filename), mode="a")
    file.write(text)
    file.close()

Готово! Теперь наш скрипт можно запускать из командной строки, просто передавая ему URL статьи:

>python grabber.py https://lenta.ru/news/2020/04/09/centenarian

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

В дальнейшем данный скрипт можно усовершенствовать так, чтобы он в качестве входных данных использовал файл со списком URL и выгружал уже готовый массив данных целиком. А для написания более детального скрипта выгрузки для Ваших источников необходимо просто дописать нужные теги в объект GrabberArticle.content_tags, либо передавать их через отдельный файл настроек.

Примечание:

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

Полный код из статьи размещен по адресу: https://github.com/ZveRuss/GrabberArticle