Алгоритм парсинга веб-страниц

Привет, как дела?
Это моя первая публикация на Medium и первая из серии из трех статей, где я продемонстрирую весь процесс проекта данных со следующими шагами:

– Парсинг данных
– Очистка, обработка и анализ данных
– Создание диаграмм и интерактивной панели инструментов

Проект занимается регистрацией авиационных происшествий по всему миру, произошедших в период с 1919 по 2020 год. Данные доступны на сайте Aviation Safety Network от Flight Safety Foundation, а итоговую панель можно посмотреть здесь.

В этой первой статье я продемонстрирую процесс очистки данных с помощью алгоритма Python с использованием библиотек Selenium, Beautiful Soup и Pandas. Я разработал две версии алгоритма парсинга, одну попроще, а другую посложнее. В этой статье я приведу только более простой вариант, который быстрее выполняет очистку, но информация о месте аварии является обобщенной и/или неполной, тогда как в более сложном алгоритме эта информация предоставляется полностью, являясь только разница между ними. Однако эта небольшая разница представляет собой значительную разницу во времени выполнения кода, примерно в 300 раз между самым простым и самым сложным алгоритмом.

Предыстория…

Этот проект начался, когда я искал фреймы данных, чтобы попрактиковаться в навыках анализа данных на Kaggle, и нашел фрейм данных с авиакатастрофами на сайте Aviation-safety.net. В то время я не знал веб-сайт Flight Safety Foundation и нашел набор данных очень интересным. В то время я изучал парсинг данных с помощью Python, и, поскольку на сайте не было информации, запрещающей парсинг его содержимого, я решил разработать код для парсинга сайта и сбора данных об авариях в качестве личного обучения. Это был очень полезный опыт, и я многому научился в процессе.

Ниже я пошагово опишу формулировку алгоритма и полученные результаты. Этот алгоритм представляет собой простой последовательный код без использования классов или функций.

Процесс

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

Хотя алгоритм имеет логическую последовательность, его построение не является линейным, поэтому я прошу читателя внимательно изучить каждую строчку кода, так как в процессе разработки столько раз в его середину будут добавляться новые строчки и функции.

Шаг 1

  • Импорт веб-драйвера Selenium.
  • Создание экземпляра Chrome.
  • Доступ к главной странице веб-сайта.
from selenium import webdriver

browser = webdriver.Chrome()
browser.get('https://aviation-safety.net/')

Шаг 2

  • Импорт прекрасного супа.
  • Получение HTML-контента страницы.
  • Оформление содержимого страницы.
from selenium import webdriver
from bs4 import BeautifulSoup

browser = webdriver.Chrome() 
browser.get('https://aviation-safety.net/')

page_content_cont = browser.page_source
pag_cont = BeautifulSoup(page_content_cont, 'html.parser')

Шаг 3

  • Создание списка для записи количества вхождений (occurrence_list).
  • Установка конкретного года для проверки кода (я выбрал 1935).
  • Внедрение фрагмента кода, чтобы узнать, сколько аварий произошло в указанном году, и, если на странице не отображается информация о том, сколько аварий произошло, дополнить кодом для подсчета аварий в этом году.
  • Выяснить, сколько аварий произошло.
from selenium import webdriver
from bs4 import BeautifulSoup

list_occurrences = []

year = 1935

browser = webdriver.Chrome() 
browser.get(f'https://aviation-safety.net/database/year/{year}/1')

page_content_cont = browser.page_source
pag_cont = BeautifulSoup(page_content_cont, 'html.parser')

elements_cont = pag_cont.find('span', attrs = {'class' : 'caption'})
elements_cont2 = pag_cont.findAll('tr')

if elements_cont != None:
    amount_elem = elements_cont.text.split(" ")[0]
    print(f'Ano: 1935 - Ocorrências: {amount_elem}')
    list_occurrences.append(int(amount_elem))
else:
    count_elem = 0
    
    for element in elements_cont2[1:]: 
        for category in ['A1', 'A2', 'A3', 'A4', 'A5']:
            if category in element.text.split('\n'):
                count_elem += 1
    amount_elem = count_elem
    print(f'Year: {year} - Occurrences: {amount_elem}')
    list_occurrences.append(int(amount_elem))
    
    count_elem = 0

Шаг 4

  • Создание 2 списков для хранения информации.
  • Список необработанных данных: хранение информации о каждой аварии на странице.
  • Список стран: запоминание стран каждой аварии на странице.
  • Доступ к данным об авариях, строка за строкой.
  • Сохранение необработанных данных в созданные списки.
from selenium import webdriver
from bs4 import BeautifulSoup

list_occurrences = []
list_raw_data = []
list_countries = []

year = 1935

browser = webdriver.Chrome() 
browser.get(f'https://aviation-safety.net/database/year/{year}/1')

page_content_cont = browser.page_source
pag_cont = BeautifulSoup(page_content_cont, 'html.parser')

elements_cont = pag_cont.find('span', attrs = {'class' : 'caption'})
elements_cont2 = pag_cont.findAll('tr')

if elements_cont != None:
    amount_elem = elements_cont.text.split(" ")[0]
    print(f'Year: {year} - Occurrences: {amount_elem}')
    list_occurrences.append(int(amount_elem))
else:
    count_elem = 0
    
    for element in elements_cont2[1:]: 
        for category in ['A1', 'A2', 'A3', 'A4', 'A5']:
            if category in element.text.split('\n'):
                count_elem += 1
    amount_elem = count_elem
    print(f'Year: {ano} - Occurrences: {quantidade_elem}')
    list_occurrences.append(int(amount_elem))
    
    count_elem = 0

elements = pag_cont.findAll('td', attrs={'class':['list','listdata']})

for element in elements:            
    data_line = element.text.splitlines()
    if data_line == [] or data_line == '':
        list_raw_data.append('')
    else:
        list_raw_data.append(data_line[0])
        
    img = 'https:'+str(element.img)[10:-3]
    if 'country' in img:
        data_line = img.split('="')[1]
        list_countries.append(data_line)

Шаг 5

  • Разделение сохраненных данных об авариях по строкам.
  • Создание нового списка для сохранения разделенных данных.
  • Импорт панд.
  • Создание кадра данных с разделенными данными.
from selenium import webdriver
from bs4 import BeautifulSoup
import pandas as pd

list_occurrences = []
list_raw_data = []
list_countries = []
list_data = []

year = 1935

browser = webdriver.Chrome() 
browser.get(f'https://aviation-safety.net/database/year/{year}/1')

page_content_cont = browser.page_source
pag_cont = BeautifulSoup(page_content_cont, 'html.parser')

elements_cont = pag_cont.find('span', attrs = {'class' : 'caption'})
elements_cont2 = pag_cont.findAll('tr')

if elements_cont != None:
    amount_elem = elements_cont.text.split(" ")[0]
    print(f'Year: {year} - Occurrences: {amount_elem}')
    list_occurrences.append(int(amount_elem))
else:
    count_elem = 0
    
    for element in elements_cont2[1:]: 
        for category in ['A1', 'A2', 'A3', 'A4', 'A5']:
            if category in element.text.split('\n'):
                count_elem += 1
    amount_elem = count_elem
    print(f'Year: {year} - Occcurrences: {amount_elem}')
    lista_occurrences.append(int(amount_elem))
    
    count_elem = 0

elements = pag_cont.findAll('td', attrs={'class':['list','listdata']})

for element in elements:            
    data_line = element.text.splitlines()
    if data_line == [] or data_line == '':
        list_raw_data.append('')
    else:
        list_raw_data.append(data_line[0])
        
    img = 'https:'+str(element.img)[10:-3]
    if 'country' in img:
        data_line = img.split('="')[1]
        list_countries.append(data_line)

for i in range(0, len(list_raw_data), 9):
    list_data.append(list_raw_data[i:i+9])

df = pd.DataFrame(list_data, columns=['Date', 'Air_craft_type', 'Registration', 'Operator', 'Fatilites', 'Location', 'None', 'None', 'Category'])
df['Country'] = list_countries[:]
df = df[['Date', 'Air_craft_type', 'Registration', 'Operator', 'Fatilites', 'Location', 'Country', 'Category']]

Шаг 6

  • Адаптируя код, созданный за годы, имеющие более одной страницы, в качестве примера я выбрал 2001 год, в котором было 226 аварий и всего 3 страницы.
  • Очистка содержимого списка необработанных данных.
  • Создание ссылки для доступа к следующей странице, если в рассматриваемом году более одной страницы.
from selenium import webdriver
from bs4 import BeautifulSoup
import pandas as pd

list_occurrences = []
list_raw_data = []
list_countries = []
list_data = []

year = 2001

browser = webdriver.Chrome() 
browser.get(f'https://aviation-safety.net/database/year/{year}/1')

page_content_cont = browser.page_source
pag_cont = BeautifulSoup(page_content_cont, 'html.parser')

elements_cont = pag_cont.find('span', attrs = {'class' : 'caption'})
elements_cont2 = pag_cont.findAll('tr')

if elements_cont != None:
    amount_elem = elements_cont.text.split(" ")[0]
    print(f'Year: {year} - Occurrences: {amount_elem}')
    list_occurrences.append(int(amount_elem))
else:
    count_elem = 0
    
    for element in elements_cont2[1:]: 
        for category in ['A1', 'A2', 'A3', 'A4', 'A5']:
            if category in element.text.split('\n'):
                count_elem += 1
    amount_elem = count_elem
    print(f'Year: {year} - Occurrences: {amount_elem}')
    list_occurrences.append(int(amount_elem))
    
    count_elem = 0

if int(amount_elem) > 100:
    amount_pag = int(amount_elem) / 100
    if amount_pag % int(amount_pag) != 0:
        amount_pag = int(amount_pag) + 1
    else:
        pass
else:
    amount_pag = 1

count = 0 

for pag in range(1, amount_pag+1, 1):        
    count = count + 1
    
    page_content = browser.page_source
    site = BeautifulSoup(page_content, 'html.parser')
    elements = site.findAll('td', attrs={'class':['list','listdata']})

    for element in elements:            
        data_line = element.text.splitlines()
        if data_line == [] or data_line == '':
            list_raw_data.append('')
        else:
            list_raw_data.append(data_line[0])
            
        img = 'https:'+str(element.img)[10:-3]
        if 'country' in img:
            data_line = img.split('="')[1]
            list_countries.append(data_line)
    
    for i in range(0, len(list_raw_data), 9):
        list_data.append(list_raw_data[i:i+9])

    list_raw_data.clear()

    browser.get(f'https://aviation-safety.net/database/dblist.php?Year={year}&lang=&page={count+1}')


df = pd.DataFrame(list_data, columns=['Date', 'Air_craft_type', 'Registration', 'Operator', 'Fatilites', 'Location', 'None', 'None', 'Category'])
df['Country'] = list_countries[:]
df = df[['Date', 'Air_craft_type', 'Registration', 'Operator', 'Fatilites', 'Location', 'Country', 'Category']]

Шаг 7

  • Адаптируя код для поиска более чем за один год, в качестве примера я выбрал годы с 2001 по 2003 год, что в совокупности составило 670 аварий.
from selenium import webdriver
from bs4 import BeautifulSoup
import pandas as pd

list_occurrences = []
list_raw_data = []
list_countries = []
list_data = []

initial_year = 2001
final_year = 2003

for year in range(initial_year, final_year + 1):
    browser = webdriver.Chrome() 
    browser.get(f'https://aviation-safety.net/database/year/{year}/1')
    
    page_content_cont = browser.page_source
    pag_cont = BeautifulSoup(page_content_cont, 'html.parser')
    
    elements_cont = pag_cont.find('span', attrs = {'class' : 'caption'})
    elements_cont2 = pag_cont.findAll('tr')
    
    if elements_cont != None:
        amount_elem = elements_cont.text.split(" ")[0]
        print(f'Year: {year} - Occurrences: {amount_elem}')
        list_occurrences.append(int(amount_elem))
    else:
        count_elem = 0
        
        for element in elements_cont2[1:]: 
            for category in ['A1', 'A2', 'A3', 'A4', 'A5']:
                if category in element.text.split('\n'):
                    count_elem += 1
        amount_elem = count_elem
        print(f'Year: {year} - Occurrences: {amount_elem}')
        list_occurrences.append(int(amount_elem))
        
        count_elem = 0
    
    if int(amount_elem) > 100:
        amount_pag = int(amount_elem) / 100
        if amount_pag % int(amount_pag) != 0:
            amount_pag = int(amount_pag) + 1
        else:
            pass
    else:
        amount_pag = 1
    
    count = 0 
    
    for pag in range(1, amount_pag+1, 1):        
        count = count + 1
        
        page_content = browser.page_source
        site = BeautifulSoup(page_content, 'html.parser')
        elements = site.findAll('td', attrs={'class':['list','listdata']})
    
        for element in elements:            
            data_line = element.text.splitlines()
            if data_line == [] or data_line == '':
                list_raw_data.append('')
            else:
                list_raw_data.append(data_line[0])
                
            img = 'https:'+str(element.img)[10:-3]
            if 'country' in img:
                data_line = img.split('="')[1]
                list_countries.append(data_line)
        
        for i in range(0, len(list_raw_data), 9):
            list_data.append(list_raw_data[i:i+9])
    
        list_raw_data.clear()
    
        browser.get(f'https://aviation-safety.net/database/dblist.php?Year={year}&lang=&page={count+1}')


df = pd.DataFrame(list_data, columns=['Date', 'Air_craft_type', 'Registration', 'Operator', 'Fatilites', 'Location', 'None', 'None', 'Category'])
df['Country'] = list_countries[:]
df = df[['Date', 'Air_craft_type', 'Registration', 'Operator', 'Fatilites', 'Location', 'Country', 'Category']]

Шаг 8

  • Подведение итогов по всем событиям.
  • Импорт ChromiumOptions из Selenium. (Я использую Chromium, а не Chrome, поэтому не импортирую ChromeOptions).
  • Добавление аргумента «—headless» в ChromiumOptions для выполнения кода без открытия графической версии браузера.
from selenium import webdriver
from selenium.webdriver.chromium.options import ChromiumOptions
from bs4 import BeautifulSoup
import pandas as pd

list_occurrences = []
list_raw_data = []
list_countries = []
list_data = []

initial_year = 2001
final_year = 2003

for year in range(initial_year, final_year + 1):
    options = ChromiumOptions()
    options.add_argument('--headless')
    browser = webdriver.Chrome(options=options) 
    browser.get(f'https://aviation-safety.net/database/year/{year}/1')
    
    page_content_cont = browser.page_source
    pag_cont = BeautifulSoup(page_content_cont, 'html.parser')
    
    elements_cont = pag_cont.find('span', attrs = {'class' : 'caption'})
    elements_cont2 = pag_cont.findAll('tr')
    
    if elements_cont != None:
        amount_elem = elements_cont.text.split(" ")[0]
        print(f'Year: {year} - Occurrences: {amount_elem}')
        list_occurrences.append(int(amount_elem))
    else:
        count_elem = 0
        
        for element in elements_cont2[1:]: 
            for category in ['A1', 'A2', 'A3', 'A4', 'A5']:
                if category in element.text.split('\n'):
                    count_elem += 1
        amount_elem = count_elem
        print(f'Year: {year} - Occurrences: {amount_elem}')
        list_occurrences.append(int(amount_elem))
        
        count_elem = 0
    
    if int(amount_elem) > 100:
        amount_pag = int(amount_elem) / 100
        if amount_pag % int(amount_pag) != 0:
            amount_pag = int(amount_pag) + 1
        else:
            pass
    else:
        amount_pag = 1
    
    count = 0 
    
    for pag in range(1, amount_pag+1, 1):        
        count = count + 1
        
        page_content = browser.page_source
        site = BeautifulSoup(page_content, 'html.parser')
        elements = site.findAll('td', attrs={'class':['list','listdata']})
    
        for element in elements:            
            data_line = element.text.splitlines()
            if data_line == [] or data_line == '':
                list_raw_data.append('')
            else:
                list_raw_data.append(data_line[0])
                
            img = 'https:'+str(element.img)[10:-3]
            if 'country' in img:
                data_line = img.split('="')[1]
                list_countries.append(data_line)
        
        for i in range(0, len(list_raw_data), 9):
            list_data.append(list_raw_data[i:i+9])
    
        list_raw_data.clear()
    
        browser.get(f'https://aviation-safety.net/database/dblist.php?Year={year}&lang=&page={count+1}')


df = pd.DataFrame(list_data, columns=['Date', 'Air_craft_type', 'Registration', 'Operator', 'Fatilites', 'Location', 'None', 'None', 'Category'])
df['Country'] = list_countries[:]
df = df[['Date', 'Air_craft_type', 'Registration', 'Operator', 'Fatilites', 'Location', 'Country', 'Category']]

print('\nTotal occurrences:', sum(list_occurrences), '\n')

Запуск кода для сбора данных в период с 2001 по 2003 год привел к 670 записям об авариях. Между тем, выполнение алгоритма в период с 1919 по 2020 год привело к созданию DataFrame с 23 642 записями об авариях за этот период.

Этот набор данных все еще обновляется, поэтому объем извлеченной информации может быть больше, если исследование будет проведено в ближайшем будущем.

Вот и все. Я надеюсь, что читателю понравилась эта первая статья, и что она будет в чем-то полезна.

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