Алгоритм парсинга веб-страниц
Привет, как дела?
Это моя первая публикация на 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 записями об авариях за этот период.
Этот набор данных все еще обновляется, поэтому объем извлеченной информации может быть больше, если исследование будет проведено в ближайшем будущем.
Вот и все. Я надеюсь, что читателю понравилась эта первая статья, и что она будет в чем-то полезна.
В следующей статье этой серии я расскажу об очистке, обработке и анализе собранных данных. Увидимся в следующий раз!