CoderCastrov logo
CoderCastrov
Питон

Как парсить веб-сайты с капчей с помощью Python, BeautifulSoup и MongoDB - Глава 1

Как парсить веб-сайты с капчей с помощью Python, BeautifulSoup и MongoDB - Глава 1
просмотров
10 мин чтение
#Питон

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

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

_Хотите прочитать эту историю позже? _Сохраните ее в журнале.

Теперь я расскажу вам историю о достижении этой цели. Она будет разделена на несколько частей.

  • Глава 1: Найти подходящий источник данных
  • Глава 2: Получить доступ к источнику данных
  • Глава 3: Извлечение информации
  • Глава 4: Создание приложения

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

Найдите подходящий источник

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

Поиск в Google не дает полного набора данных. Моя жена уже использует страницу verbformen.de. На этом сайте можно найти все немецкие глаголы вместе с их спряжениями. Она также использует translate.google.com для перевода основного глагола на тайский язык. Но, исходя из ее опыта, это часто неудовлетворительно. Это связано с тем, что Google переводит через английский язык (например, Google путает слово "plant", которое может быть как "производственным заводом", так и "растением"). Так что еще одна цель - реализовать дополнительные источники перевода с контекстом. Мы нашли небольшой набор данных от Longdo, который мы также хотим использовать.

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

Авторское право / Лицензия. Обязательно, чтобы лицензия нашего источника данных соответствовала нашему использованию. Содержимое, защищенное авторским правом, не должно использоваться вообще. Содержимое, лицензированное, часто может использоваться в коммерческих и/или личных целях. Если это не совсем ясно, лучше ожидать, что данные защищены авторским правом. В нашем случае мы везунчики. Владелец веб-сайта размещает все данные под лицензией CC-BY-SA 3.0, что делает их свободно доступными для личного и даже коммерческого использования.

Приветствуются боты? Мы можем найти ответ, просто взглянув на файл robots.txt, который содержит правила сайта. Файл robots.txt можно получить, добавив его после любого домена. Вот выдержка из verbformen.de/robots.txt:

User-agent: *
Disallow: *.pdf
Disallow: *.docx
Disallow: *.wav
Disallow: *.mp3User-agent: msnbot 
Crawl-delay: 2
Disallow: *.pdf
Disallow: *.docx
Disallow: *.wav
Disallow: *.mp3User-agent: SemrushBot
Disallow: /

Теперь нам нужно понять, что разрешено, а что нет. В данном случае, загрузка файлов .pdf, .docx, .wav и .mp3 запрещена для всех пользователей (*). msnbot должен ждать две секунды (crawl-delay) после каждой страницы, а SemrushBot вообще не может выполнять индексацию.

Исследование нашего источника данных

В идеальном мире мы просто загружаем полный набор данных (например, Wikipedia). Второй по счету лучший вариант - это использование API, например, такого, как предлагает Google для сервисов перевода. В случае с verformen.de нашим единственным вариантом является получение нужных данных из HTML-контента. Существуют два подхода для получения всех необходимых страниц.

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

Структурированный подходВ нашем случае мы везунчики. Verbformen.de предлагает индекс на отдельном веб-сайте verblisten.de. Эта страница является практически полным списком всех глаголов, доступных на verbformen.de.

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

Начало кодирования

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

Все, кто работал с Python, знают, что он предлагает отличные возможности для работы со строками и обработки данных "из коробки". Кроме того, существуют очень полезные библиотеки, которые делают быстрое прототипирование невероятно простым. По моему мнению, Python отлично подходит для парсинга данных. Мы будем работать с моим любимым редактором кода Visual Studio Code и стандартным терминалом MacOSX на основе zhs.

Мы начинаем с создания нового репозитория на Github и клонирования его на нашу машину. Затем мы настраиваем свежую среду Python 3. Таким образом, наш проект изолирован и имеет свои собственные зависимости.

$ git clone http://url/to/our/git
$ cd path/to/our/project
$ python3 -m venv .ENV
$ source .ENV/bin/activate

Парсинг индекса

Создадим новый файл с названием 01_get_data_sources_from_verblisten.py. Этот код будет использоваться только для сбора данных один раз. Для этого нам понадобятся несколько методов без классов.

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

(.ENV)$ pip install requests beautifulsoup4

В нашем файле Python мы импортируем только что установленные библиотеки, делаем первый запрос и создаем объект BeautifulSoup из содержимого ответа. Мы получаем доступ ко всему HTML-содержимому через объект soup.

import requests
from bs4 import BeautifulSoup

verb_index_url = f'[https://www.verblisten.de/listen/verben/anfangsbuchstabe/vollstaendig-1.html?i=](https://www.verblisten.de/listen/verben/anfangsbuchstabe/vollstaendig-%7Bi%7D.html?i=)^a'
response = requests.get(verb_index_url)
soup = BeautifulSoup(response.content, 'html.parser')

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

<html>
    <div class="class_1">
        <a href="[https://url1.com](https://url1.com)">Ссылка 1</a>
        <a href="[https://url2.com](https://url2.com)">Ссылка 2</a>
        <a href="[https://url3.com](https://url3.com)">Ссылка 3</a>
        <div class="class_2">
            <a href="[https://url1.com](https://url1.com)">Ссылка 1</a>
            <a href="[https://url2.com](https://url2.com)">Ссылка 2</a>
            <a href="[https://url3.com](https://url3.com)">Ссылка 3</a>
        </div>    
    </div>
    <section id="section_1">
        <a href="[https://url1.com](https://url1.com)">Ссылка 1</a>
        <a href="[https://url2.com](https://url2.com)">Ссылка 2</a>
        <a href="[https://url3.com](https://url3.com)">Ссылка 3</a>
    </section>
</html>

links = content.find_all('a') возвращает список со всеми девятью ссылками из всего документа.

divs_with_class_2 = content.find_all('div', class_='class_2') возвращает список всех div-элементов с классом class_2. Это также работает с атрибутом id, например id='section_1'.

Когда мы вызываем find_all('a') на divs_with_class_2[0], мы получаем только три ссылки внутри первого div, который имеет значение атрибута class_2: links_in_div = divs_with_class_2[0].find_all('a').

Если мы хотим получить содержимое определенного элемента i, мы используем links[i].content. Чтобы получить доступ к любому доступному параметру, такому как href, мы используем links[i].get('href').

На самом деле, это все, что нам нужно. Для получения более подробной информации, пожалуйста, ознакомьтесь с документацией.

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

data_sources  = []
base_url = 'https://www.verblisten.de/listen/verben/anfangsbuchstabe/vollstaendig'
verb_index_url = f'{base_url}-{counter}.html?i=^{letter}'

С помощью Python легко перебирать символы в строке, поэтому мы предоставляем строку со всеми немецкими буквами.

alphabet = (string.ascii_lowercase[:26]+'äöü').replace('y','')

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

alphabet = 'abcdefghijklmnopqrstuvwxzäöü'
**for letter in alphabet:**
    verb_index_url = f'{base_url}-{counter}.html?i=^{**letter**}'
    response = requests.get(verb_index_url)
    soup = BeautifulSoup(response.content, 'html.parser')

Мы увеличиваем счетчик в URL-адресе до тех пор, пока div с классом listen-spalte содержит ссылки. Как только мы больше не получаем ссылки, мы переходим к следующей букве. Для этой итерации мы используем цикл while.

**counter = 0**
**links_found = True**
**while links_found:**
    **counter+=1**
    verb_index_url = f'{base_url}-{**counter**}.html?i=^{letter}'
    response = requests.get(verb_index_url)
    soup = BeautifulSoup(response.content, 'html.parser')
    for div in soup.find_all('div', class_='listen-spalte'):
        links = div.find_all('a')
        for a in links:
            # создание объекта
       ** if len(links) < 1:            links_found = False**

Мы перебираем все links, создавая словарь, содержащий соответствующую информацию.

data_sources .append({
    'word': a.get('title').replace('Konjugation ', ''),
    'conjugations': {
        'download_status': False,
        'url': a.get('href')
    },
    'scrape_status': False
})

Мы сохраняем слово, URL-адрес и статус загрузки и скрапинга. Чтобы указать, загружена ли уже эта страница, мы используем логическое значение download_status, чтобы указать, скрапировано ли уже слово, мы используем логическое значение scrape_status. Это позволяет нам остановить и продолжить эти трудоемкие и подверженные ошибкам процессы загрузки и скрапинга без потери прогресса.

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

data_sources .append({
    'word': a.get('title').replace('Konjugation ', ''),
    'conjugations': {
        'download_status': False,
        'url': a.get('href')
    },
    **'definitions': {        'download_status': False,        'url': a.get('href').replace('verbformen.de/konjugation','woerter.net/verbs')    },    'examples': {        'download_status': False,        'url': a.get('href').replace('.de/konjugation','.de/konjugation/beispiele')    },**
    'scrape_status': False
})

Подключение к базе данныхСписок data_sources содержит все необходимые данные. Чтобы сохранить этот список без необходимости создавать множество таблиц и связей, мы используем базу данных noSQL. Еще одно преимущество для нас заключается в том, что нам не нужно определять схему на данный момент. Поскольку мы работаем в команде, нам обоим нужен простой доступ к базе данных. Поэтому сохранение наших данных в облаке MongoDB - хорошее решение. И для нашего случая это бесплатно.

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

Из портала MongoDB Atlas мы можем получить строку подключения, включая наши учетные данные. Чтобы сохранить учетные данные в секрете, мы помещаем их в отдельный файл config.py.

mongo_db_secret = 'mongodb+srv://<user>:<password>@verbcluster.qwvfi.mongodb.net/dict?retryWrites=true&w=majority'

Во время разработки у нас возникли проблемы с подключением к Интернету. Как обычно, я перезагрузил роутер. Что я не учел: после перезагрузки был присвоен новый IP-адрес. Некоторое время спустя мы продолжили отладку нашего скрипта, но столкнулись с проблемами подключения к нашему кластеру mogoDB. При создании кластера mongo он добавляет ваш текущий IP-адрес в белый список. Таким образом, новый IP-адрес не был включен в белый список. Это заняло некоторое время, чтобы разобраться в этом... (Немцы любят свой интернет... НЕТ)

Не забудьте хранить ваши учетные данные в секрете и добавить config.py в .gitignore перед фиксацией. Мы также добавляем .DS_Store, .ENV/ и .vscode/, потому что они содержат только временные данные с вашего компьютера, которые не являются важными для нашего проекта.

.DS_Store       # временные данные из osx
.ENV/           # наше окружение Python
.vscode/        # временные данные из Visual Studio
config.py       # секреты, пароли, ключи, которые никто не должен знать

Чтобы использовать нашу облачную базу данных MongoDB Atlas, мы устанавливаем библиотеку pymongo.

(.ENV)$ pip install pymongo

Мы создаем новый файл mongo_db.py для подключения к базе данных, чтобы мы могли импортировать его в любом месте, где это необходимо. Мы импортируем библиотеку pymongo и файл конфигурации config.py.

Поскольку нам не нужно использовать проверку схемы в данный момент, мы позволяем MongoDB автоматически создать базу данных. С помощью следующего метода мы подключаемся к MongoDB и возвращаем объект подключения для базы данных с именем dict.

import config
from pymongo import MongoClient

def connect_mongo_db():
    client = MongoClient(config.mongo_db_secret)
    return client.dict

Давайте расширим наш файл 01_get_data_sources_from_verblisten.py с помощью mongo_db.py и вызовем метод подключения.

from mongo_db import connect_mongo_db

db = connect_mongo_db()

С помощью всего одной простой команды мы можем создать коллекцию с названием data_sources и добавить в нее наш полный список data_sources.

db.data_sources.insert_many(data_sources)

Наконец, мы объединяем все наши фрагменты в один метод с названием get_data_sources() и делаем наш модуль основной программой.

import requests
import string
from bs4 import BeautifulSoup
from mongo_db import connect_mongo_db

db = connect_mongo_db()

def get_data_sources():
    base_url = '[https://www.verblisten.de/listen/verben/anfangsbuchstabe/vollstaendig'](https://www.verblisten.de/listen/verben/anfangsbuchstabe/vollstaendig')
    alphabet = 'abcdefghijklmnopqrstuvwxzäöü'
    data_sources = []
    for letter in alphabet:
        counter = 0
        links_found = True
        while links_found:
            counter+=1
            verb_index_url = f'{base_url}-{counter}.html?i=^{letter}'
            response = requests.get(verb_index_url)
            soup = BeautifulSoup(response.content, 'html.parser')
            for div in soup.find_all('div', class_='listen-spalte'):
                links = div.find_all('a')
                for a in links:
                    data_sources.append({
                        'word': a.get('title').replace('Konjugation ', ''),
                        'conjugations': {
                            'download_status': False,
                            'url': a.get('href')
                        },
                        'definitions': {
                            'download_status': False,
                            'url': a.get('href').replace('verbformen.de/konjugation','woerter.net/verbs')
                        },
                        'examples': {
                            'download_status': False,
                            'url': a.get('href').replace('.de/konjugation','.de/konjugation/beispiele')
                        },
                        'scrape_status': False
                    })
                if len(links) < 1:
                    links_found = False
    db.data_sources.insert_many(data_sources)

if __name__ == "__main__":
    get_data_sources()

Если вас интересует, что делает if __name__ == "__main__":, прочтите это.

Чтобы выполнить наш модуль, мы используем следующую команду в терминале. Выполнение занимает несколько минут.

(.ENV)$ python 01_get_data_sources_from_verblisten.py

Когда мы посмотрим на нашу коллекцию MongoDB, мы увидим все глаголы вместе с URL-адресами после завершения скрипта.

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

Вы можете найти весь исходный код здесь: https://github.com/michael-gerstenberg/GermanVerbScraper

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

С уважением, Майкл и Робин