Python - памочнік у пошуку недарагіх авіябілетаў для тых, хто любіць падарожнічаць

Аўтар артыкула, пераклад якога мы публікуем сёння, кажа, што яго мэта - распавесці аб распрацоўцы вэб-скрапера на Python з выкарыстаннем Selenium, які выконвае пошук коштаў на авіябілеты. Пры пошуку білетаў выкарыстоўваюцца гнуткія даты (+- 3 дні адносна ўказаных дат). Скрапер захоўвае вынікі пошуку ў Excel-файле і адпраўляе таму, хто яго запусціў, электронны ліст з агульнымі звесткамі аб тым, што яму ўдалося знайсці. Задача гэтага праекта - дапамога падарожнікам у пошуку найбольш выгадных прапаноў.

Python - памочнік у пошуку недарагіх авіябілетаў для тых, хто любіць падарожнічаць

Калі вы, разбіраючыся з матэрыялам, адчуеце, што згубіліся - зірніце на гэтую артыкул.

Што будзем шукаць?

Вы вольныя скарыстацца апісванай тут сістэмай так, як вам жадаецца. Я, напрыклад, ужываў яе для пошуку тураў выходнага дня і квіткоў у мой родны горад. Калі вы сур'ёзна настроены на пошук выгадных білетаў - вы можаце запусціць скрыпт на серверы (просты сервер, за 130 рублёў у месяц, цалкам для гэтага падыдзе) і зрабіць так, каб ён запускаўся б адзін-два разы на дзень. Вынікі пошуку будуць прыходзіць да вас па электроннай пошце. Акрамя таго, я рэкамендую наладзіць усё так, каб скрыпт захоўваў бы Excel-файл з вынікамі пошуку ў Dropbox-тэчцы, што дазволіць вам праглядаць падобныя файлы адкуль заўгодна і ў любы час.

Python - памочнік у пошуку недарагіх авіябілетаў для тых, хто любіць падарожнічаць
Я пакуль не знайшоў тарыфаў з памылкамі, але мяркую, што гэта магчыма

Пры пошуку, як ужо было сказана, выкарыстоўваецца "гнуткая дата", скрыпт знаходзіць прапановы, якія знаходзяцца ў межах трох дзён ад зададзеных дат. Хоць пры запуску скрыпту выконваецца пошук прапаноў толькі па адным кірунку, яго нескладана дапрацаваць для таго, каб ён мог бы збіраць дадзеныя па некалькіх кірунках палётаў. З яго дапамогай можна нават шукаць памылковыя тарыфы, падобныя знаходкі могуць аказацца вельмі цікавымі.

Навошта патрэбен чарговы вэб-скрапер?

Калі я ўпершыню заняўся вэб-скрапінгам, мне, сапраўды кажучы, гэта было не асабліва цікава. Мне хацелася рабіць больш праектаў у сферы прагнознага мадэлявання, фінансавага аналізу і, магчыма, у галіне аналізу эмацыйнай афарбоўкі тэкстаў. Але аказалася, што гэта вельмі цікава - разбірацца з тым, як стварыць праграму, якая збірае дадзеныя з вэб-сайтаў. Па меры таго, як я ўнікаў у гэтую тэму, я зразумеў, што менавіта вэб-скрапінг з'яўляецца "рухавіком" інтэрнэту.

Магчыма, вы вырашыце, што гэта - занадта смелая заява. Але падумайце аб тым, што кампанія Google пачалася з вэб-скрапера, які Лары Пэйдж стварыў з выкарыстаннем Java і Python. Робаты Google даследавалі і даследуюць інтэрнэт, спрабуючы падаць сваім карыстачам найлепшыя адказы на іх пытанні. У вэб-скрапінгу ёсць бясконцае мноства варыянтаў ужывання, і нават калі вам, у сферы Data Science, цікава нешта іншае, то вам, каб абзавесціся дадзенымі для аналізу, спатрэбяцца некаторыя навыкі скрапінгу.

Некаторыя з скарыстаных тут прыёмаў я знайшоў у выдатнай кнізе пра вэб-скрапінг, які нядаўна займеў. У ёй можна знайсці мноства простых прыкладаў і ідэй па практычным ужыванні вывучанага. Акрамя таго, там ёсць вельмі цікавая частка аб абыходзе праверак reCaptcha. Для мяне гэта стала навіной, бо я і не ведаў аб тым, што існуюць спецыяльныя інструменты і нават цэлыя сэрвісы для вырашэння такіх задач.

Любіце падарожнічаць?!

На простае і даволі бяскрыўднае пытанне, вынесенае ў загаловак гэтай часткі, часта можна пачуць дадатны адказ, забяспечаны парай гісторый з вандраванняў таго, каму яго задалі. Большасць з нас пагодзіцца з тым, што падарожжы – гэта выдатны спосаб апускання ў новыя культурныя асяроддзі і пашырэнні ўласнага кругагляду. Аднак калі задаць каму-небудзь пытанне аб тым, ці падабаецца яму шукаць авіябілеты, то я ўпэўнены, што адказ на яго будзе ўжо далёка не такім пазітыўным. Уласна кажучы, тут нам на дапамогу прыходзіць Python.

Першай задачай, якую нам трэба вырашыць на шляху стварэння сістэмы пошуку звестак па авіябілетах, будзе падбор прыдатнай платформы, з якой мы будзем браць інфармацыю. Рашэнне гэтай задачы далося мне нялёгка, але ў выніку я абраў сэрвіс Kayak. Я спрабаваў сэрвісы Momondo, Skyscanner, Expedia, ды і яшчэ некаторыя, але механізмы абароны ад робатаў на гэтых рэсурсах былі непрабіўнымі. Пасля некалькіх спроб, падчас якіх, спрабуючы пераканаць сістэмы ў тым, што я чалавек, мне прыйшлося мець справу са святлафорамі, пешаходнымі пераходамі і роварамі, я вырашыў, што мне больш за ўсё падыходзіць Kayak, нягледзячы нават на тое, што і тут, калі за кароткі час загрузіць занадта шмат старонак, таксама пачынаюцца праверкі. Мне ўдалося зрабіць так, каб бот адпраўляў бы запыты да сайта ў інтэрвалах ад 4 да 6 гадзін, і ўсё нармальна працавала. Перыядычна складанасці ўзнікаюць і пры працы з Kayak, але калі вас пачынаюць даймаць праверкамі, то вам трэба або разабрацца з імі ўручную, пасля чаго запусціць бот, або пачакаць некалькі гадзін, і праверкі павінны спыніцца. Вы, калі трэба, цалкам можаце адаптаваць код для іншай платформы, а калі вы так і зробіце - можаце паведаміць пра гэта ў каментарах.

Калі вы толькі пачынаеце знаёмства з вэб-скрапінгам, і не ведаеце пра тое, чаму некаторыя вэб-сайты ўсімі сіламі з ім змагаюцца, то, перш чым прыступаць да свайго першага праекту ў гэтай галіне – акажыце сабе паслугу і пашукайце ў Google матэрыялы па словах. "web scraping etiquette". Вашы эксперыменты могуць завяршыцца хутчэй, чым вы думаеце, у тым выпадку, калі вы будзеце займацца вэб-скрапінгам неразумна.

Пачатак працы

Вось агульны агляд таго, што будзе адбывацца ў кодзе нашага вэб-скрапера:

  • Імпарт неабходных бібліятэк.
  • Адкрыццё ўкладкі Google Chrome.
  • Выклік функцыі, якая запускае бота, перадаючы яму гарады і даты, якія будуць выкарыстоўвацца пры пошуку білетаў.
  • Гэтая функцыя атрымлівае першыя вынікі пошуку, адсартаваныя па крытэрыі найбольшай прывабнасці (best), і націскае на кнопку для загрузкі дадатковых вынікаў.
  • Яшчэ адна функцыя збірае дадзеныя са ўсёй старонкі і вяртае фрэйм ​​дадзеных.
  • Два папярэднія крокі выконваюцца з выкарыстаннем тыпаў сартавання па кошце квіткоў (cheap) і па хуткасці пералёту (fastest).
  • Карыстальніку скрыпту адпраўляецца электронны ліст, які змяшчае кароткую зводку аб коштах квіткоў (самыя танныя квіткі і сярэдні кошт), а фрэйм ​​дадзеных са звесткамі, адсартаванымі па трох вышэйзгаданых паказчыках, захоўваецца ў выглядзе Excel-файла.
  • Усе вышэйапісаныя дзеянні выконваюцца ў цыкле праз зададзены прамежак часу.

Трэба адзначыць, што кожны праект Selenium пачынаецца з вэб-драйвера. Я выкарыстоўваю Chromedriver, працую з Google Chrome, але ёсць і іншыя варыянты. Папулярнасцю карыстаюцца яшчэ PhantomJS і Firefox. Пасля загрузкі драйвера яго трэба змясціць у адпаведную тэчку, на гэтым падрыхтоўка да яго выкарыстання сканчаецца. У першых радках нашага скрыпту ажыццяўляецца адкрыццё новай укладкі Chrome.

Улічвайце, што я, у сваім апавяданні, не спрабую адкрыць новыя гарызонты пошуку выгадных прапаноў на авіябілеты. Існуюць і значна больш прасунутыя прыёмы пошуку падобных прапаноў. Мне толькі жадаецца прапанаваць чытачам гэтага матэрыялу просты, але дастасавальны на практыку спосаб рашэння гэтай задачы.

Вось код, аб якім мы казалі вышэй.

from time import sleep, strftime
from random import randint
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import smtplib
from email.mime.multipart import MIMEMultipart

# Используйте тут ваш путь к chromedriver!
chromedriver_path = 'C:/{YOUR PATH HERE}/chromedriver_win32/chromedriver.exe'

driver = webdriver.Chrome(executable_path=chromedriver_path) # Этой командой открывается окно Chrome
sleep(2)

У пачатку кода можна бачыць каманды імпарту пакетаў, якія выкарыстоўваюцца ва ўсім нашым праекце. Так, randint ужываецца для таго, каб бот «засынаў» бы на выпадковую колькасць секунд перш чым пачаць новую аперацыю пошуку. Звычайна без гэтага не абыходзіцца ні адзін робат. Калі запусціць вышэйпрыведзены код, будзе адчынена акно Chrome, якое робат будзе выкарыстоўваць для працы з сайтамі.

Правядзем невялікі эксперымент і адкрыем у асобным акне сайт kayak.com. Выберам горад, з якога збіраемся ляцець, і горад, у які жадаем патрапіць, а таксама даты палётаў. Пры выбары дат праверым, каб выкарыстоўваўся дыяпазон +-3 дні. Я напісаў код з улікам таго, што выдае сайт у адказ на такія запыты. Калі ж вам, напрыклад, трэба шукаць квіткі толькі на зададзеныя даты, то высокая верагоднасць таго, што вам давядзецца дапрацаваць код бота. Расказваючы аб кодзе, я раблю адпаведныя тлумачэнні, але калі вы адчуеце, што заблыталіся - дайце мне ведаць.

Цяпер націскаем на кнопку запуску пошуку і глядзім на спасылку ў радку адрасу. Яна павінна быць падобная на тую спасылку, якую я выкарыстоўваю ў прыкладзе ніжэй, там, дзе аб'яўляецца пераменная. kayak, якая захоўвае URL, і выкарыстоўваецца метад get вэб-драйвера. Пасля націску на кнопку пошуку на старонцы павінны з'явіцца вынікі.

Python - памочнік у пошуку недарагіх авіябілетаў для тых, хто любіць падарожнічаць
Калі я выкарыстаў каманду get больш за два-тры разы на працягу некалькіх хвілін, мне прапаноўвалі прайсці праверку з выкарыстаннем reCaptcha. Гэтую праверку можна прайсці ўручную і працягнуць эксперыменты да таго часу, пакуль сістэма не вырашыць задаволіць новую праверку. Калі я тэставаў скрыпт, стварылася такое адчуванне, што першы сеанс пошуку заўсёды праходзіць без праблем, таму, калі вы жадаеце паэксперыментаваць з кодам, вам давядзецца толькі перыядычна ўручную праходзіць праверку і пакідаць код выконвацца, выкарыстоўваючы працяглыя інтэрвалы паміж сеансамі пошуку. Ды і, калі ўдумацца, чалавеку ці наўрад спатрэбяцца звесткі аб коштах на квіткі, атрыманыя з 10-хвіліннымі інтэрваламі паміж аперацыямі пошуку.

Праца са старонкай з выкарыстаннем XPath

Такім чынам, мы адчынілі акно і загрузілі сайт. Для таго каб атрымаць звесткі аб коштах і іншую інфармацыю, нам трэба скарыстацца тэхналогіяй XPath ці CSS-селектарамі. Я вырашыў спыніцца на XPath і не адчуў патрэбы ва ўжыванні CSS-селектараў, але суцэль магчыма працаваць і так. Перамяшчэнне па старонцы з выкарыстаннем XPath можа апынуцца няпростай справай, і нават калі вы скарыстаецеся тымі метадамі, якія я апісваў у гэтай артыкуле, дзе ўжывалася капіяванне адпаведных ідэнтыфікатараў з кода старонкі, я зразумеў, што гэта, насамрэч, не аптымальны спосаб звароту да неабходных элементаў. Дарэчы, у гэтай кнізе можна знайсці выдатнае апісанне асноў працы са старонкамі з выкарыстаннем XPath і CSS-селектараў. Вось як выглядае адпаведны метад вэб-драйвера.

Python - памочнік у пошуку недарагіх авіябілетаў для тых, хто любіць падарожнічаць
Такім чынам, працягваем працу над робатам. Скарыстаемся магчымасцямі праграмы для выбару самых танных білетаў. На наступным малюнку чырвоным выдзелены код селектара XPath. Для таго каб прагледзець код, трэба пстрыкнуць правай кнопкай мышы па які цікавіць вас элементу старонкі і ў якое з'явілася меню абраць каманду Прагледзець код (Inspect). Гэтую каманду можна выклікаць для розных элементаў старонкі, код якіх будзе выводзіцца і выдзяляцца ў акне прагляду кода.

Python - памочнік у пошуку недарагіх авіябілетаў для тых, хто любіць падарожнічаць
Прагляд кода старонкі

Для таго каб знайсці пацверджанне маім развагам аб недахопах капіявання селектараў з кода, звернеце ўвагу на наступныя асаблівасці.

Вось што атрымліваецца пры капіяванні кода:

//*[@id="wtKI-price_aTab"]/div[1]/div/div/div[1]/div/span/span

Для таго каб скапіяваць нешта падобнае, трэба пстрыкнуць правай кнопкай мышы па які цікавіць вас участку кода і абраць у якое з'явілася меню каманду Copy > Copy XPath.

Вось што я выкарыстаў для вызначэння кнопкі Cheapest:

cheap_results = ‘//a[@data-code = "price"]’

Python - памочнік у пошуку недарагіх авіябілетаў для тых, хто любіць падарожнічаць
Каманда Copy > Copy XPath

Зусім відавочна тое, што другі варыянт выглядае значна прасцей. Пры ім выкарыстанні выконваецца пошук элемента a, у якога ёсць атрыбут data-code, роўны price. Пры выкарыстанні першага варыянту ажыццяўляецца пошук элемента id якога роўны wtKI-price_aTab, пры гэтым XPath-шлях да элемента выглядае як /div[1]/div/div/div[1]/div/span/span. Падобны XPath-запыт да старонкі зробіць сваю справу, але – толькі адзін раз. Я прама зараз магу сказаць, што id зменіцца пры наступнай загрузцы старонкі. Паслядоўнасць сімвалаў wtKI дынамічна змяняецца пры кожнай загрузцы старонкі, у выніку код, у якім яна выкарыстоўваецца, пасля чарговай перазагрузкі старонкі апынецца бескарысным. Таму надасце некаторы час на тое, каб разабрацца з XPath. Гэтыя веды саслужаць вам добрую службу.

Аднак трэба адзначыць, што капіраванне XPath-селектараў можа спатрэбіцца пры працы з досыць простымі сайтамі, і калі вас гэта задавальняе, нічога дрэннага ў гэтым няма.

Цяпер падумаем аб тым, як быць, калі трэба атрымаць усе вынікі пошуку ў некалькіх радках, усярэдзіне спісу. Вельмі проста. Кожны вынік знаходзіцца ўнутры аб'екта з класам resultWrapper. Загрузка ўсіх вынікаў можа быць выканана ў цыкле, які нагадвае той, што будзе паказаны ніжэй.

Трэба адзначыць, што калі вам зразумела вышэйсказанае, тыя вы без праблем павінны зразумець большасць кода, які мы будзем разбіраць. Падчас працы гэтага кода да таго, што нам трэба (фактычна - гэта элемент, у які абгорнуты вынік), мы звяртаемся з выкарыстаннем нейкага механізму для ўказання шляху (XPath). Робіцца гэта для таго каб атрымаць тэкст элемента і змясціць яго ў аб'ект, з якога можна счытваць дадзеныя (спачатку выкарыстоўваецца flight_containers, затым - flights_list).

Python - памочнік у пошуку недарагіх авіябілетаў для тых, хто любіць падарожнічаць
Выводзяцца першыя тры радкі і мы можам дакладна бачыць усё, што нам трэба. Аднак у нас ёсць і цікавейшыя спосабы атрымання інфармацыі. Нам трэба браць дадзеныя з кожнага элемента паасобку.

За працу!

Лягчэй за ўсё напісаць функцыю для загрузкі дадатковых вынікаў, таму з яе і пачнем. Мне жадалася б максымізаваць лік пералётаў, звесткі пра якія атрымлівае праграма, і пры гэтым не выклікаць у сэрвісу падазроны, якія прыводзяць да праверкі, таму я аднойчы пстрыкаю па кнопцы Load more results кожны раз калі адлюстроўваецца старонка. У гэтым кодзе варта звярнуць увагу на блок try, Які я дадаў з-за таго, што часам кнопка нармальна не загружаецца. Калі вы таксама з гэтым сутыкнецеся - закаментуйце выклікі гэтай функцыі ў кодзе функцыі start_kayak, якую мы разгледзім ніжэй.

# Загрузка большего количества результатов для того, чтобы максимизировать объём собираемых данных
def load_more():
    try:
        more_results = '//a[@class = "moreButton"]'
        driver.find_element_by_xpath(more_results).click()
        # Вывод этих заметок в ходе работы программы помогает мне быстро выяснить то, чем она занята
        print('sleeping.....')
        sleep(randint(45,60))
    except:
        pass

Цяпер, пасля доўгага разбору гэтай функцыі (часам я магу і захапіцца), мы гатовы да таго, каб аб'явіць функцыю, якая будзе займацца скрапінгам старонкі.

Я ўжо сабраў большую частку таго, што трэба, у наступнай функцыі, званай page_scrape. Часам якія вяртаюцца дадзеныя аб этапах шляху апыняюцца аб'яднанымі, для іх падзелу я выкарыстоўваю просты метад. Напрыклад, калі ў першы раз карыстаюся зменнымі section_a_list и section_b_list. Наша функцыя вяртае фрэйм ​​дадзеных flights_df, гэта дазваляе нам падзяліць вынікі, атрыманыя пры выкарыстанні розных метадаў сартавання дадзеных, а пазней - аб'яднаць іх.

def page_scrape():
    """This function takes care of the scraping part"""
    
    xp_sections = '//*[@class="section duration"]'
    sections = driver.find_elements_by_xpath(xp_sections)
    sections_list = [value.text for value in sections]
    section_a_list = sections_list[::2] # так мы разделяем информацию о двух полётах
    section_b_list = sections_list[1::2]
    
    # Если вы наткнулись на reCaptcha, вам может понадобиться что-то предпринять.
    # О том, что что-то пошло не так, вы узнаете исходя из того, что вышеприведённые списки пусты
    # это выражение if позволяет завершить работу программы или сделать ещё что-нибудь
    # тут можно приостановить работу, что позволит вам пройти проверку и продолжить скрапинг
    # я использую тут SystemExit так как хочу протестировать всё с самого начала
    if section_a_list == []:
        raise SystemExit
    
    # Я буду использовать букву A для уходящих рейсов и B для прибывающих
    a_duration = []
    a_section_names = []
    for n in section_a_list:
        # Получаем время
        a_section_names.append(''.join(n.split()[2:5]))
        a_duration.append(''.join(n.split()[0:2]))
    b_duration = []
    b_section_names = []
    for n in section_b_list:
        # Получаем время
        b_section_names.append(''.join(n.split()[2:5]))
        b_duration.append(''.join(n.split()[0:2]))

    xp_dates = '//div[@class="section date"]'
    dates = driver.find_elements_by_xpath(xp_dates)
    dates_list = [value.text for value in dates]
    a_date_list = dates_list[::2]
    b_date_list = dates_list[1::2]
    # Получаем день недели
    a_day = [value.split()[0] for value in a_date_list]
    a_weekday = [value.split()[1] for value in a_date_list]
    b_day = [value.split()[0] for value in b_date_list]
    b_weekday = [value.split()[1] for value in b_date_list]
    
    # Получаем цены
    xp_prices = '//a[@class="booking-link"]/span[@class="price option-text"]'
    prices = driver.find_elements_by_xpath(xp_prices)
    prices_list = [price.text.replace('$','') for price in prices if price.text != '']
    prices_list = list(map(int, prices_list))

    # stops - это большой список, в котором первый фрагмент пути находится по чётному индексу, а второй - по нечётному
    xp_stops = '//div[@class="section stops"]/div[1]'
    stops = driver.find_elements_by_xpath(xp_stops)
    stops_list = [stop.text[0].replace('n','0') for stop in stops]
    a_stop_list = stops_list[::2]
    b_stop_list = stops_list[1::2]

    xp_stops_cities = '//div[@class="section stops"]/div[2]'
    stops_cities = driver.find_elements_by_xpath(xp_stops_cities)
    stops_cities_list = [stop.text for stop in stops_cities]
    a_stop_name_list = stops_cities_list[::2]
    b_stop_name_list = stops_cities_list[1::2]
    
    # сведения о компании-перевозчике, время отправления и прибытия для обоих рейсов
    xp_schedule = '//div[@class="section times"]'
    schedules = driver.find_elements_by_xpath(xp_schedule)
    hours_list = []
    carrier_list = []
    for schedule in schedules:
        hours_list.append(schedule.text.split('n')[0])
        carrier_list.append(schedule.text.split('n')[1])
    # разделяем сведения о времени и о перевозчиках между рейсами a и b
    a_hours = hours_list[::2]
    a_carrier = carrier_list[1::2]
    b_hours = hours_list[::2]
    b_carrier = carrier_list[1::2]

    
    cols = (['Out Day', 'Out Time', 'Out Weekday', 'Out Airline', 'Out Cities', 'Out Duration', 'Out Stops', 'Out Stop Cities',
            'Return Day', 'Return Time', 'Return Weekday', 'Return Airline', 'Return Cities', 'Return Duration', 'Return Stops', 'Return Stop Cities',
            'Price'])

    flights_df = pd.DataFrame({'Out Day': a_day,
                               'Out Weekday': a_weekday,
                               'Out Duration': a_duration,
                               'Out Cities': a_section_names,
                               'Return Day': b_day,
                               'Return Weekday': b_weekday,
                               'Return Duration': b_duration,
                               'Return Cities': b_section_names,
                               'Out Stops': a_stop_list,
                               'Out Stop Cities': a_stop_name_list,
                               'Return Stops': b_stop_list,
                               'Return Stop Cities': b_stop_name_list,
                               'Out Time': a_hours,
                               'Out Airline': a_carrier,
                               'Return Time': b_hours,
                               'Return Airline': b_carrier,                           
                               'Price': prices_list})[cols]
    
    flights_df['timestamp'] = strftime("%Y%m%d-%H%M") # время сбора данных
    return flights_df

Я пастараўся называць зменныя так, каб код аказаўся б зразумелым. Памятайце аб тым, што зменныя, якія пачынаюцца з a адносяцца да першага этапу шляху, а b - да другога. Пераходзім да наступнай функцыі.

Дапаможныя механізмы

Цяпер у нас ёсць функцыя, якая дазваляе загружаць дадатковыя вынікі пошуку і функцыя для апрацоўкі гэтых вынікаў. Дадзены артыкул на гэтым можна было б і скончыць, бо гэтыя дзве функцыі даюць усё неабходнае для скрапінгу старонак, якія можна адчыняць самастойна. Але мы пакуль не разгледзелі некаторыя дапаможныя механізмы, пра якія казалі вышэй. Напрыклад - гэта код для адпраўкі электронных лістоў і яшчэ некаторыя рэчы. Усё гэта можна знайсці ў функцыі start_kayak, якую мы зараз і разгледзім.

Для працы гэтай функцыі патрэбны звесткі аб гарадах і датах. Яна, выкарыстоўваючы гэтыя звесткі, фармуе спасылку ў зменнай kayak, якая выкарыстоўваецца для пераходу на старонку, на якой будуць знаходзіцца вынікі пошуку, адсартаваныя па іх найлепшай адпаведнасці запыту. Пасля першага сеансу скрапінгу мы папрацуем з коштамі, якія знаходзяцца ў табліцы, у верхняй частцы старонкі. А менавіта, знойдзем мінімальную цану білета і сярэднюю цану. Усё гэта, разам з прадказаннем, якое выдаецца сайтам, будзе адпраўлены па электроннай пошце. На старонцы адпаведная табліца павінна быць у левым верхнім куце. Праца з гэтай табліцай, дарэчы, можа выклікаць памылку пры пошуку з выкарыстаннем дакладных дат, бо ў такім выпадку табліца на старонцы не выводзіцца.

def start_kayak(city_from, city_to, date_start, date_end):
    """City codes - it's the IATA codes!
    Date format -  YYYY-MM-DD"""
    
    kayak = ('https://www.kayak.com/flights/' + city_from + '-' + city_to +
             '/' + date_start + '-flexible/' + date_end + '-flexible?sort=bestflight_a')
    driver.get(kayak)
    sleep(randint(8,10))
    
    # иногда появляется всплывающее окно, для проверки на это и его закрытия можно воспользоваться блоком try
    try:
        xp_popup_close = '//button[contains(@id,"dialog-close") and contains(@class,"Button-No-Standard-Style close ")]'
        driver.find_elements_by_xpath(xp_popup_close)[5].click()
    except Exception as e:
        pass
    sleep(randint(60,95))
    print('loading more.....')
    
#     load_more()
    
    print('starting first scrape.....')
    df_flights_best = page_scrape()
    df_flights_best['sort'] = 'best'
    sleep(randint(60,80))
    
    # Возьмём самую низкую цену из таблицы, расположенной в верхней части страницы
    matrix = driver.find_elements_by_xpath('//*[contains(@id,"FlexMatrixCell")]')
    matrix_prices = [price.text.replace('$','') for price in matrix]
    matrix_prices = list(map(int, matrix_prices))
    matrix_min = min(matrix_prices)
    matrix_avg = sum(matrix_prices)/len(matrix_prices)
    
    print('switching to cheapest results.....')
    cheap_results = '//a[@data-code = "price"]'
    driver.find_element_by_xpath(cheap_results).click()
    sleep(randint(60,90))
    print('loading more.....')
    
#     load_more()
    
    print('starting second scrape.....')
    df_flights_cheap = page_scrape()
    df_flights_cheap['sort'] = 'cheap'
    sleep(randint(60,80))
    
    print('switching to quickest results.....')
    quick_results = '//a[@data-code = "duration"]'
    driver.find_element_by_xpath(quick_results).click()  
    sleep(randint(60,90))
    print('loading more.....')
    
#     load_more()
    
    print('starting third scrape.....')
    df_flights_fast = page_scrape()
    df_flights_fast['sort'] = 'fast'
    sleep(randint(60,80))
    
    # Сохранение нового фрейма в Excel-файл, имя которого отражает города и даты
    final_df = df_flights_cheap.append(df_flights_best).append(df_flights_fast)
    final_df.to_excel('search_backups//{}_flights_{}-{}_from_{}_to_{}.xlsx'.format(strftime("%Y%m%d-%H%M"),
                                                                                   city_from, city_to, 
                                                                                   date_start, date_end), index=False)
    print('saved df.....')
    
    # Можно следить за тем, как прогноз, выдаваемый сайтом, соотносится с реальностью
    xp_loading = '//div[contains(@id,"advice")]'
    loading = driver.find_element_by_xpath(xp_loading).text
    xp_prediction = '//span[@class="info-text"]'
    prediction = driver.find_element_by_xpath(xp_prediction).text
    print(loading+'n'+prediction)
    
    # иногда в переменной loading оказывается эта строка, которая, позже, вызывает проблемы с отправкой письма
    # если это прозошло - меняем её на "Not Sure"
    weird = '¯_(ツ)_/¯'
    if loading == weird:
        loading = 'Not sure'
    
    username = '[email protected]'
    password = 'YOUR PASSWORD'

    server = smtplib.SMTP('smtp.outlook.com', 587)
    server.ehlo()
    server.starttls()
    server.login(username, password)
    msg = ('Subject: Flight Scrapernn
Cheapest Flight: {}nAverage Price: {}nnRecommendation: {}nnEnd of message'.format(matrix_min, matrix_avg, (loading+'n'+prediction)))
    message = MIMEMultipart()
    message['From'] = '[email protected]'
    message['to'] = '[email protected]'
    server.sendmail('[email protected]', '[email protected]', msg)
    print('sent email.....')

Я пратэставаў гэты скрыпт з выкарыстаннем уліковага запісу Outlook (hotmail.com). Я не правяраў яго на правільнасць працы з акаўнтам Gmail, гэтая паштовая сістэма вельмі папулярная, але ёсць маса магчымых варыянтаў. Калі ж вы выкарыстоўваеце ўліковы запіс Hotmail, то вам, для таго каб усё зарабіла, дастаткова ўвесці ў код свае дадзеныя.

Калі вы жадаеце разабрацца з тым, што менавіта выконваецца ў асобных участках кода гэтай функцыі, вы можаце скапіяваць іх і паэксперыментаваць з імі. Эксперыменты з кодам - ​​гэта адзіны спосаб як след яго зразумець.

Гатовая сістэма

Цяпер, калі зроблена ўсё тое, пра што мы казалі, мы можам стварыць просты цыкл, у якім выклікаюцца нашыя функцыі. Скрыпт запытвае ў карыстальніка дадзеныя аб гарадах і датах. Пры тэставанні з сталым перазапускам скрыпту вам ці наўрад захочацца кожны раз уводзіць гэтыя дадзеныя ўручную, таму якія адпавядаюць радкі, на час тэставання, можна закаментаваць, раскаментаваўшы тыя, што ідуць ніжэй іх, у якіх цвёрда зададзены патрэбныя скрыпту дадзеныя.

city_from = input('From which city? ')
city_to = input('Where to? ')
date_start = input('Search around which departure date? Please use YYYY-MM-DD format only ')
date_end = input('Return when? Please use YYYY-MM-DD format only ')

# city_from = 'LIS'
# city_to = 'SIN'
# date_start = '2019-08-21'
# date_end = '2019-09-07'

for n in range(0,5):
    start_kayak(city_from, city_to, date_start, date_end)
    print('iteration {} was complete @ {}'.format(n, strftime("%Y%m%d-%H%M")))
    
    # Ждём 4 часа
    sleep(60*60*4)
    print('sleep finished.....')

Вось як выглядае тэставы запуск скрыпту.
Python - памочнік у пошуку недарагіх авіябілетаў для тых, хто любіць падарожнічаць
Тэставы запуск скрыпту

Вынікі

Калі вы дабраліся да гэтага моманту - прыміце віншаванні! Цяпер у вас ёсць працоўны вэб-скрапер, хоць я ўжо бачу мноства шляхоў яго паляпшэння. Напрыклад, яго можна інтэграваць з Twilio, каб ён, замест электронных лістоў, дасылаў бы тэкставыя паведамленні. Можна карыстацца VPN ці яшчэ чым-небудзь для таго, каб адначасова атрымліваць вынікі з некалькіх сервераў. Ёсць яшчэ і перыядычна якая ўзнікае праблема з праверкай карыстальніка сайта на тое, ці з'яўляецца ён чалавекам, але і гэтую праблему можна вырашыць. У любым выпадку, зараз у вас маецца база, якую вы, пры жаданні, можаце пашыраць. Напрыклад, зрабіць так, каб Excel-файл адпраўляўся б карыстачу ў выглядзе ўкладання ў электронны ліст.

Python - памочнік у пошуку недарагіх авіябілетаў для тых, хто любіць падарожнічаць

Толькі зарэгістраваныя карыстачы могуць удзельнічаць у апытанні. Увайдзіце, Калі ласка.

Карыстаецеся Ці вы тэхналогіі вэб-скрапінгу?

  • Так

  • Няма

Прагаласавалі 8 карыстальнікаў. Устрымаўся 1 карыстач.

Крыніца: habr.com

Дадаць каментар