Время на прочтение
5 мин
Количество просмотров 6.3K
Современные программы в значительной степени строятся из готовых кирпичиков — библиотек. Уникального кода и архитектурных решений в каждой программе относительно мало. Очень часто бывает, что существующие библиотеки не слишком высокого качества, но даже самый крутой программист не станет их переписывать.
Этот факт находит отражение и в изменении учебных курсов. Сассман, автор SICP, самого известного курса по программирование, сказал: “ инженерное дело в середине 90-ых, а уж тем более в 2000-ых сильно отличается от инженерного дела 80-ых. В 80-ых хорошие программисты проводили много времени в размышлениях, а потом писали немного кода, который работал. Код работал близко к «железу», даже Scheme — все было прозрачно на всех стадиях. Как с резистором, достаточно посмотреть на цветную маркировку, чтобы узнать номинальную мощность, допустимые отклонения, сопротивление и V=IR — это все, что нужно знать. 6.001 был задуман как курс для обучения инженеров тому, как из маленьких кубиков, в которых они досконально разбираются, посредством простых техник составлять сложные конструкции, которые делают то, что от них хотят. Но программирование сейчас далеко не то же самое. Теперь вы ковыряетесь в непонятной или несуществующей документацией для софта, даже неизвестно, кем написанного. Вы должны досконально исследовать библиотеки, чтобы узнать, как они работают, пробовать разные исходные данные и смотреть, как реагирует код. Это в корне иная работа, и для нее требуется иной курс обучения.“
Строительные кирпичики стандартизированы — каменщику обычно не приходится выбирать подходящий именно для этого места кирпич. С библиотеками все наоборот — то, что предназначено для обработки PDF не подойдет для создания распределенной вычислительной системы. Возникает потребность найти нужную библиотеку, в ней нужную функцию и понять, как ее встроить в свою программу. Google, как и любая другая ориентированная на естественный язык поисковая система пока помогает мало. Так что рассмотрим другие подходы.
В статически типизированных о том, что делает функция можно догадаться по ее типу. И наоборот — зная, что должна делать функция, предположить ее тип (сигнатуру). Например, функция извлечения элемента из списка должна иметь тип
[a] -> a
— здесь «a» не определенный в данный момент тип элемента, а “[a]” — тип списка из элементов типа «a». Поисковики по типам есть (для некоторых языков) и сильно упрощают жизнь разработчика — это ocamlfind для OCaml, Hoogle и Hayoo для Haskell, Scalex для Scala.
Для динамически типизированных языков этот подход тоже может работать, если каждой функции приписать тип, пусть даже игнорируемый компилятором. Иногда это можно сделать автоматически — современные системы часто умеют выводить тип из кода (так поступают многие компиляторы статически типизированных функциональных языков), но для языков с наследованием реализовать его тяжело (так в Scala тип аргументов функций приходится описывать явно, а он может быть очень громоздкий). Можно собрать статистику по использованию функции в реальных программах — хоть это и не гарантирует правильно описанный обобщенный тип, но может оказаться полезным для поиска и документирования (не все же пишут документацию до того, как их библиотека начинает использоваться — почему бы не упростить себе жизнь :-)).
Ted Kaehler, сотрудник Алона Кая (автора языка Smalltalk) предлагает более радикальные методики поиска. Один из них — написать тест для этой функции и запустить его на всех существующих. Звучит пугающе, но лично мне часто приходилось искать метод из Java-библиотеки, вызывая все, что показалось подходящим по названию из Scala REPL. А все, что можно автоматизировать, должно быть автоматизировано. :-).
Вторая методика — как можно разнообразнее аннотировать все библиотечные функции и искать по аннотациям. По аннотациям строится таксономия функций и библиотек, которая позволила бы управляться с этим зоопарком (термин «таксономия» очень кстати придуман биологами).
В таксономический/онтологический подход еще дальше углубились программисты на R (статья, презентация). Это и не удивительно — R часто применяют для анализа больших сложно структурированных данных — там же, где и онтологии.
Все знают, что онтология — это точная спецификация концептуализации. Но что такое спецификация и концептуализация знают не многое. Я к их числу не отношусь, по тому попробую описать как я это понимаю.
Онтология более или менее формально описывает предметную область — какие в ней бывают объекты и какие между ними бывают отношения. Есть множество языков для онтологий, но наибольшее распространение получил стандарт W3C RDF (для более сложных случаев OWL).
В RDF все задается «тройками» субъект-предикат-объект. F1.R is_a RFunction. «F1.R» есть R-функция. Для описания троек W3C пытался навязать XML, но вовремя одумался и разработал человеческий синтаксис.
RDF часто называют графом, а базы данных, способные его обрабатывать относят к графовым. Основным языком общения с таким базами является SPARQL — достаточно удачный гибрид SQL и Prolog. Типичный запрос представляет их себя шаблон фрагмента графа, который требуется найти. В таком виде не сложно сформулировать достаточно экзотические условия — например «найти функцию из доступной под GPLv3 библиотеки, результат которой можно передать на вход функции с именем createPDF». Это несколько сложнее, чем поиск по шаблону типа, описанный в начале статьи, но гораздо гибче.
Можно хранить более подробную информацию об аргументах функции, которая плохо описывается типами. Например, что у метода executeSelect класса org.w3.banana.SparqlEngine первый аргумент — строка с запросом SPARQL select. Найти такую функцию из всех получающих строковый аргумент без такой аннотации было бы не легко. Кроме поисковика этой информацией мог бы пользоваться верификатор кода (аналог lint из C) и IDE для подсветки синтаксиса.
Хотя иногда подобную информацию умудряются засунуть в тип, но это мало помогает в поиске. Вот как это сделано в OCaml (на уровне компилятора)
# (fun a -> Printf.sprintf a 1 ; a) "%d";; - : (int -> string, unit, string) format = <abstr>
Понятно, что что-бы найти функцию подстановки целого в строковый шаблон, ни кто не будет искать функцию с типом
(int -> string, unit, string) format -> int -> string
В лучшем из миров библиотеку на одном языке программирования сравнительно легко можно вызвать из любого другого. В реальности это не так, но помечтать об этом можно.
Современные языки очень разные и термины одного могут отображаться на другой нетривиальным образом. Например, в некоторых языках есть понятие «класс типов» — совокупность типов, к которым есть общий интерфейс (в отличие от интерфейсов ООП, эти интерфейсы не являются частью типа, а живут отдельно). Например функция
max :: Ord a => a -> a -> a
ждет два параметра одинакового типа a, возвращает результат того же типа и этот тип должен принадлежать классу Ord (то, что можно сравнивать). То есть ее можно вызвать с двумя целыми и получить целое, или с двумя действительными и получить действительное. А с целым и действительным — нельзя.
Классы типов очень удобны в использовании (и полезны при поиске), и в языках, где из нет, придумали способ их эмулировать.
Обычно реализуются они передачей неявного параметра, содержащего реализацию интерфейса для конкретного типа. В языках, где есть возможность описать параметры со сложными умолчаниями, таких как Scala и C++ это делается сравнительно не сложно. Правда, в Haskell неявный аргумент передается первым (для возможности оптимизации), а в Scala и C++ — последним (ввиду устройства языка), и кроссязыковая поисковая система должна будет это учитывать.
API может описывать не только сигнатуры методов и функций, но и последовательность их вызова (примерно как в сетевых протоколах). В упомянутой выше статье для этого вводится предикат couldBeUsedBefore. Особенно это актуально, если в языке допускаются более-менее автономные сущности, такие как процессы Erlang и акторы Scala. Erlang допускает опциональную типизацию функций, а Scala статически типизирован, но устройство сообщений процессам/акторам формально описать они не позволяют.
Как говорится, в каждой библиотеке сидит DSL и просится наружу. Если с тем, как найти синус в Smalltalk мы более-менее разобрались, то как найти loop в Common Lisp пока совершенно не понятно.
Many APIs are publicly available. All APIs on the RapidAPI platform are publicly available. But there are also so-called internal APIs, which are created and used by developers that need APIs for private applications.
When it comes to web and mobile applications, there is a process to follow in order to determine what API the website uses. In some cases, you can also use the internal API without interacting with the website (i.e. programmatically, by issuing requests directly from the code). In this tutorial, we will demonstrate how to find an API of a website.
View the Best Free APIs List
How websites use APIs
There are two main ways in which websites use APIs. For the sake of keeping things simple, we’ll call them the backend-tied method and frontend-tied method.
The backend-tied approach is the method where the application makes requests to the API on the server’s side. The pipeline looks like this:
- The user wants to visit a page of the website. He/she uses a browser to send an HTTP request to the server where the site is hosted.
- To provide a response to the user, the server needs to use an API.
- The server sends the request(s) to the API and processes the response of the API. The response to the user is prepared using the response of the API.
- The server sends the prepared response (most often it is the HTML page) to the user.
- The user’s browser renders the response and shows the result to the user.
The key peculiarity of this method is that the user’s browser knows nothing about what is going on on the server’s side (how the response was prepared). This means that we (as users) cannot detect whether any API was used by this site. The exception comes in when the information about used APIs are available on the website, or when t is obvious that the website uses an API (for example, when the website displays information about flight tickets from the Skyscanner).
A vivid example of the backend-tied approach of API usage can be found in our How To Build a Sentiment Analysis App tutorial. The Django web application there sends requests to the API on the server’s side.
Another method is the frontend-tied approach.
Most modern web frameworks use client-side rendering. They send a blank HTML file to the browser along with JavaScript that fills it with data. In this case, it takes data from the internal API. And this is handy for us because if it is done with our browser we can find it. Let’s explore how this works in more detail.
How to see whether a website uses an API
Let’s create a little sandbox to demonstrate how it works. We can use Flask to make a simple internal API which will send only one string of text data. The ‘Access-Control-Allow-Origin’ header is added so we can simply open the HTML file in the browser and make this request from it.
from flask import Flask from flask import jsonify from flask import after_this_request app = Flask(__name__) @app.route('/test_data', methods=['GET']) def get_data(): @after_this_request def add_header(response): response.headers['Access-Control-Allow-Origin'] = '*' return response data = 'An internal API is an interface that enables access to a company’s backend information and application functionality for use by the organization’s developers. The new applications created by these developers can then be distributed publicly although the interface itself is not visible to anyone not working directly with the API publisher.' return jsonify(data) if __name__ == '__main__': app.run(debug=True)
Then we will write an HTML page that will request this string and view it.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Test page</title> </head> <body> <h1>Hello world</h1> <p id="text"></p> <script> const text_el = document.getElementById('text') const url = 'http://127.0.0.1:5000/test_data' fetch(url) .then((resp) => resp.json()) .then(function(data) { text_el.innerHTML = data }) </script> </body> </html>
If we open this page in the browser and view the page source code we will not see the data and this is typical for a frontend-tied approach using the API. If it was not a test page, we wouldn’t see the script either.
Let’s go back to our page in the browser and open the Developer Tools.
Now we need to open the Network tab and choose the XHR filter. XHR refers to the XMLHttpRequest which is the JavaScript object that is used to retrieve data from a server. (We actually use the fetch() request but it’s almost the same.) In some cases, we’ll need to repeat an action or refresh the page to see requests here. With real sites, there could also be a lot of requests. We need to choose the ones that look similar to what we expect to find.
If we click on a request we’ll see the full URL, the HTTP method, and any other necessary information. With our page, we see the “http://127.0.0.1:5000/test_data” address and the GET method.
Now we can open the Response tab and suppose that the data is what we are looking for.
All that we still need to do is to simply make a similar request programmatically and check if we get the same answer.
Test your hypothesis from Python code
Python has a Requests library that is ideal for this purpose. For our example, the code will look pretty simple.
import requests url = 'http://127.0.0.1:5000/test_data' response = requests.get(url) print(response.text)
Here is what the code above returns:
"An internal API is an interface that enables access to a companyu2019s backend information and application functionality for use by the organizationu2019s developers. The new applications created by these developers can then be distributed publicly although the interface itself is not visible to anyone not working directly with the API publisher."
But Requests itself has a lot more options. Any other HTTP method can be used with requests the same way as the GET method. We may also add data to the request body.
In most cases, we’ll get our response in JSON or XML format, so we’ll need to make some transformations or parsing.
Conclusion
There are two ways in which websites can use APIs. If we can see the data on the page but can’t find it in the page source, that’s okay. Finding an API with browser tools is not that complicated. Working with it may be easier than writing parsers for the HTML.
But if we can’t see requests from the browser it does not mean that the site doesn’t use API. If it sends server-side requests we may have trouble finding out which API a website uses.
View the Best Free APIs List
The best place to find out about a JSON-based API is the documentation provided by API’s maintainer. Typically, maintainers who would like their API to be consumed by clients provide documentation. Sometimes, they build a navigable API where responses have http links to other resources of their API and do not provide an explicit documentation. If they do so, that could be called as implicit documentation.
Networks tab in a web browser’s developers’ tools provides a way to capture network traffic going in and out of the browser. If a website makes any asynchronous http request, that will be captured and shown here. Again depending on the design and strategy of the website maker, this could be a JSON based response coming from a URL or a HTML or any other type. Content-Type
header hints at the type of response. You can use this cue as well to see if the content of the response is application/json
. Thus, obtaaining a url that you can use in your scripts.
What you are trying to do here is observing a website’s network traffic from your browser and are trying to see if there is any link that brings you a JSON based response instead of HTML. So, that you can use a JSON parser instead of a HTML parser. Possibly, because HTML parsers are slow.
The Answer to your specific question is, if you get lucky you might find a link that serves a JSON response. If not, you might wanna fall back on HTML parsing.
Обнаружение — это любые действия, которые направлены на выяснения того, как работает REST API. Например:
- По какому URL находится корневой каталог API.
- Какую схему имеет ресурс.
- Какие параметры имеет конкретный эндпоинт.
- Какие плагины/темы расширили (используют) REST API.
Как правило все виды обнаружения сводятся к чтению схемы маршрута или схемы всего WP API, но чтобы можно было прочитать такую схему нужно выяснить (обнаружить) какой у REST API корневой URL. В PHP получить корневой URL можно с помощью функции rest_url().
Обнаружение через Link в заголовке ответа
Рекомендуемый путь обнаружить URL на REST API это отправить HEAD запрос на любую страницу сайта и проверить есть ли в ответе параметр Link:. Rest API автоматически добавляет параметр Link в заголовки ответа для всех страниц во фронтэнде.
Link: <http://example.com/wp-json/>; rel="https://api.w.org/"
Указанная в параметре ссылка ведет на корневой маршрут REST API (/
). Его можно использовать для дальнейшего обнаружения маршрутов.
Для сайтов с отключенными ЧПУ /wp-json/
автоматически не обрабатывается в WordPress. И в таком случае ссылка на REST API будет выглядеть так:
Link: <http://example.com/?rest_route=/>; rel="https://api.w.org/"
Для клиентов которые не умеют читать HEADER заголовки ответа (которые парсят HTML или запускаются в браузере), URL на REST API можно получить в HTML метатеге link в <head> части документа. Такой метатег также добавляется на всех страницах во вонтэнде.
<link rel='https://api.w.org/' href='http://example.com/wp-json/' />
В Javascript эту ссылку можно получить через DOM:
// jQuery вариант var api_root = jQuery( 'link[rel="https://api.w.org/"]' ).attr( 'href' ); // Нативный JS var links = document.getElementsByTagName( 'link' ); var link = Array.prototype.filter.call( links, function ( item ) { return item.rel === 'https://api.w.org/'; } ); var api_root = link[0].href;
Такое обнаружение также справедливо для обнаружения Atom/RSS фидов. Таким образом этот код можно адаптировать пот эту потребность.
Обнаружение через RSD
Для клиентов с поддержкой XML-RPC обнаружения, возможно удобнее будет получить ссылку на REST API через XML. Тут нужно сделать два шага:
Первый шаг: найти конечную точку (URL) RSD, она расположена также в <link> элементе в <head> части на любой странице фронтэнда:
<link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://example.com/xmlrpc.php?rsd" />
Второй шаг: получить XML код по обнаруженной ссылке и распарсить его. Выглядит он примерно так:
<?xml version="1.0" encoding="utf-8"?> <rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd"> <service> <engineName>WordPress</engineName> <engineLink>https://wordpress.org/</engineLink> <homePageLink>http://example.com/</homePageLink> <apis> <api name="WordPress" blogID="1" preferred="true" apiLink="http://example.com/xmlrpc.php" /> <!-- ... --> <api name="WP-API" blogID="1" preferred="false" apiLink="http://example.com/wp-json/" /> </apis> </service> </rsd>
Элемент со ссылкой на REST API всегда будет иметь атрибут name="WP-API"
.
RDS обнаружение это НЕ рекомендуемый способ, потому что он наиболее сложный, тут нужно сначала найти ссылку на RDS затем распарсить код по этой ссылки и только потом получить URL самого REST API.
По возможности рекомендуется избегать такого RDS обнаружения!
Обнаружение способов Авторизации
Обнаружение также позволяет узнать какие методы аутентификации имеются в REST API. Ответ главного маршрута REST API /wp-json/ содержит объект, который полностью описывает API. В этом объекте под ключом authentication
находятся данные о возможных способах авторизации в REST API:
{ "name": "Example WordPress Site", "description": "YOLO", "routes": { ... }, "authentication": { "oauth1": { "request": "http://example.com/oauth/request", "authorize": "http://example.com/oauth/authorize", "access": "http://example.com/oauth/access", "version": "0.1" } } }
Подробнее про аутентификацию в REST API читайте в соответствующем разделе.
Обнаружение имеющихся Расширений
После того, как REST API обнаружен, нужно узнать что API поддерживает. Узнать какие расширения есть в API можно из ответа на корневой маршрут REST API /wp-json/
. Там под ключом namespaces
находятся все имеющиеся расширения API:
Для WP версий от 4.4 до 4.6, доступны только базовое расширение oEmbed (полное API описанное в этом руководстве еще недоступно).
{ "name": "Тестовый сайт", "namespaces": [ "oembed/1.0/" ] }
С версии WP 4.7 доступно уже все API, так можно видеть новое расширение wp/v2:
{ "name": "Тестовый сайт", "namespaces": [ "wp/v2", "oembed/1.0/" ] }
Прежде чем пытаться использовать любую из конечных точек, нужно убедиться, что она поддерживается в API, для этого нужно проверить наличие нужного пространства имен, например wp/v2.
WordPress 4.4 включает инфраструктуру API для всех сайтов, но не включает основные конечные точки под wp/v2. Конечные точки ядра работают с версии WordPress 4.7.
Этот же механизм можно использовать для определения поддержки REST расширений у плагинов. Например, представим что плагин регистрирует следующий маршрут:
register_rest_route( 'testplugin/v1', '/testroute', array( /* ... */ ) );
Тогда в данных API появится новое расширение testplugin/v1:
{ "name": "Тестовый сайт", "namespaces": [ "wp/v2", "oembed/1.0/", "testplugin/v1" ] }
Поиск открытого API сайта или Ускоряем парсинг в 10 раз
Цель статьи — описать алгоритм действий поиска открытого API сайта.
Целевая аудитория статьи — программисты, которым интересен парсинг и анализ уязвимостей сайтов.
В статье рассмотрим пример поиска API сайта edadeal.ru, познакомимся с протоколом google protobuf и сравним скорость различных подходов парсинга
1 Введение
Парсинг (в контексте статьи) — это автоматизированный процесс извлечение данных из Интернета.
Существует 2 подхода к извлечению данных со страниц сайта
-
Извлекать данные из HTML-кода страницы сайта
Плюсы — этот способ прост и работает всегда, так как код страницы всегда доступен пользователю
Минусы — этот способ может работать долго (несколько секунд), если часть данных генерирует java script (например, данные появляются только после прокручивания страницы или нажатия кнопки) -
Использовать API сайта
Плюсы — быстрее первого способа и не зависит от изменений структуры html-страницы
Минус — не у всех сайтов есть открытое API
В статье рассмотрим пример поиска API сайта edadeal.ru, познакомимся с протоколом google protobuf и сравним скорость двух подходов парсинга
2 Постановка задачи
Задача — извлечь данные о продуктах с сайта Едадил (название продукта, цена, размер скидки, магазин, город и т.д)
3 Решение
1 Делаем запрос к странице, которую мы хотим парсить.
2 Перебираем все запросы, которые делает сайт. Для этого используем DevTools браузера
3 Анализируем запросы
Из названия запроса понимаем, что нам нужен запрос
https://squark.edadeal.ru/web/search/offers?count=30&locality=moskva&page=1&retailer=5ka
В ответ на запрос получаем файл (назовем его binary_file.bin). Как узнать кодировку этого файла?
Формат файла из пункта 3 нам подсказывает строка-хедер content-type: application/x-protobuf
4 Определим структуру данных (.proto файл)
с помощью утилиты protoc (http://google.github.io/proto-lens/installing-protoc.html) преобразуем закодированный файл в понятный человеку формат
protoc --decode_raw < binary_file.bin
Получаем список словарей:
1 {
1: "e341_26007177W202222O32631623332600A"
2: "320242321203320260320273320265321202320275320260321217 320261321203320274320260320263320260 Familia Plus, 2 321201320273320276321217, 12 321200321203320273320276320275320276320262, 1 321203320277."
3: "https://leonardo.edadeal.io/dyn/cr/catalyst/offers/u4nf6zbkjc3m5lss46ucvxjafm.jpg"
4: 0x43ad7eb8
5: 0x4347e666
7: ";5332^c2121346204237RT002026610"
8: 0x41400000
9: "321210321202"
10: 0x422c0000
11: "%"
13: 43
15: "2022-07-26T00:00:00Z"
16: "2022-08-01T00:00:00Z"
19: "A105L332nPg230342q3753133514336"
20 {
1: 0x3f800000
2: 0x418547ae
3: "321210321202"
4: 1
}
21: "224331203202B3032134622431RT002026610"
22: "K3202537{O271273374K351376224310*"
22: "300336d(224kL25224300355256247327R35"
22: "303O:202330262A32624623307D314F303G"
22: "210"22?250|L.272375345{335c,26"
22: "=3yP2604N33426737732036F326331\"
22: "E21100246e6EI22300)2423348216M"
22: "V#26322367324H350232r1310_KX273"
23: "320232320276320273320270321207320265321201321202320262320276"
24: 1
}
5 Формируем .proto файл
Используем номера из предыдущего пункта, по контенту из предыдущего пункта нужно догадаться, какие поля, что означают (например 3 — это ссылка на изображение продукта)
Методом проб и ошибок получаем следующую структуру:
syntax = "proto2";
message Offers {
repeated Offer offer = 1;
}
message Offer {
optional string name = 2;
optional string image_url = 3;
optional float price_before = 4;
optional float price_after = 5;
optional float amount = 8;
optional float discount = 10;
optional string start_date = 15;
optional string end_date = 16;
}
4 Переходим к написанию кода
Создаем питоновский файл с описанием структуры из .proto файла
protoc --proto_path=proto_files --python_out=proto_structs offers.proto
proto_files
— имя директории с .proto файлами
proto_structs
— в этой директории сохраняются результаты (_pb2.py файлы)
Код работает следующим образом:
- Делает запрос к API сайта
- Преобразует ответ сайта в json
- Выводит результат
import json
import requests
from google.protobuf.json_format import MessageToJson
from proto_structs import offers_pb2
def parse_page(city = "moskva", shop = "5ka", page_num = 1):
"""
:param city: location of the shop
:param shop: shop name
:param page_num: parsed page number
:return: None
"""
url = f"https://squark.edadeal.ru/web/search/offers?count=30&locality={city}&page={page_num}&retailer={shop}"
data = requests.get(url, allow_redirects=True) # data.content is a protobuf message
offers = offers_pb2.Offers() # protobuf structure
offers.ParseFromString(data.content) # parse binary data
products: str = MessageToJson(offers) # convert protobuf message to json
products = json.loads(products)
print(json.dumps(products, indent=4, ensure_ascii=False,))
if __name__ == "__main__":
parse_page()
Результат работы программы — список продуктов с описанием
{
"offer": [
{
"name": "Наггетсы, куриные с ветчиной, Мираторг, 300 г",
"imageUrl": "https://leonardo.edadeal.io/dyn/cr/catalyst/offers/necnmkv43splbm3hr5636snpry.jpg",
"priceBefore": 218.99000549316406,
"priceAfter": 109.48999786376953,
"amount": 300.0,
"discount": 51.0,
"startDate": "2022-08-02T00:00:00Z",
"endDate": "2022-08-08T00:00:00Z"
},
...
5 Сравним результаты
Время выполнения кода из предыдущего пункта 0.3 — 0.4 секунды
Альтернативный вариант парсинга — загрузка всего html-кода страницы и извлечения нужной информации из этого кода
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://edadeal.ru/moskva/retailers/5ka")
# извлечение данных из html кода
Время полной загрузки страницы 5 — 6 секунд.
6 Выводы
Лучше использовать API сайта для извлечения данных, если есть такая возможность
Использование API сайта позволяет не зависеть от измений в html-коде страниы