Po tym, jak Tesco wyprowadziło się z Krakowa, musiałem znaleźć alternatywny sklep do zamawiania zakupów przez internet. Po krótkich poszukiwaniach natrafiłem na Everli. Everli przypomina działaniem Glovo - jest to platforma kojarząca sklepy spożywcze z kurierami chętnymi dostarczać zakupy. Sklepów jest kilka, więc w teorii jest nawet lepiej, niż było w Tesco.
Problem z aplikacją Everli jest taki, że nie pozwala sortować produktów po cenach. Wpisuję w wyszukiwarkę "mleko", i widzę wyniki w losowej kolejności. Postanowiłem rozwiązać ten problem przy użyciu JavaScriptu i Tampermonkey. Dla tych, co nie wiedzą - Tampermonkey to wtyczka do Chrome'a, która pozwala wykonać kod JavaScript na kodzie właśnie ładowanej strony.
Pierwszy krok to podejrzenie ruchu strony Everli za pomocą inspektora przeglądarki.
Taka ciekawostka: Everli ma jakąś dziwaczną zależność renderowania strony z autoryzacją. Włączając inspektora, zmienia się rozmiar okna przeglądarki, więc strona przechodzi w tryb mobilny. Następne kliknięcie w cokolwiek na stronie wywala mnie na ekran logowania. Zostałem wylogowany, zmieniając rozmiar okienka...
Wracając. Interesują nas dwa rodzaje interakcji, których wyniki chcemy posortować: wyszukiwanie oraz wybór kategorii.
Po wpisaniu do wyszukiwarki hasła "mleko", strona przechodzi na adres
https://pl.everli.com/s#/locations/21674/stores/6027/search/mleko
W inspektorze zauważam zapytanie na adres API
https://api.everli.com/sm/api/v3/locations/21674/stores/6027/search?q=mleko&skip=0&take=40
Po wybraniu kategorii "Nabiał i jaja -> Mleko i śmietana", strona przechodzi na adres
https://pl.everli.com/s#/locations/21674/stores/6027/categories/100852/101270
W inspektorze zauważam zapytanie na adres API
https://api.everli.com/sm/api/v3/locations/21674/stores/6027/categories/101278/101280?skip=0&take=40
Te dwa zapytania API są tutaj kluczowe. To one zwracają listę produktów w postaci JSONa. Dlatego nasz skrypt będzie wyczekiwał tylko ich, a całą resztę ignorował. Stwórzmy funkcję, ktora sprawdzi, czy to właśnie te zapytania są wykonywane.
function isSearchOrCategories(URL) {
var apiRegex = new RegExp(
"https://api.everli.com/sm/api/v3/locations/[0-9]+/stores/[0-9]+/"),
searchRegex = new RegExp("/search[^/]"),
categoriesRegex = new RegExp("/categories/[0-9]+/[0-9]+[^/]");
var isApi = apiRegex.test(URL),
isSearch = searchRegex.test(URL),
isCategories = categoriesRegex.test(URL);
return isApi && (isSearch || isCategories);
}
Idźmy dalej. Odpowiedź na obydwa zapytania zawiera listę produktów, która jest wyświetlana zgodnie z kolejnością, w jakiej przyszła. Naszym celem jest przechwycenie tej odpowiedzi, zmodyfikowanie jej - czyli posortowanie, i zwrócenie jej z powrotem przeglądarce.
Przyjrzyjmy się strukturze odpowiedzi. Są one gigantyczne i pozbawione formatowania, dlatego formatuję je w JSON Formatterze. Do analizy potrzebuję tylko listy produktów - resztę ignoruję.
{
"data":{
"body":[
{
"widget_type":"vertical-list",
"list":[
{
"widget_type":"product",
"name":"Łaciate Mleko Uht 2,0%",
"price":"2,79 zł",
"price_per_type":"2,79 zł/l"
}
]
}
]
}
}
Żeby dostać się do listy produktów, musimy z obiektu data
pobrać listę body
, następnie odszukać obiekt, dla którego pole widget_type
ma wartość vertical-list
*, i dla tego obiektu posortować elementy listy list
na podstawie pól price
oraz price_per_type
.
Tutaj natknąłem się na pierwszy problem. W pierwotnym rozwiązaniu pobrałem po prostu pierwszy element z listy body
, zakładając że jest tam zawsze tylko jeden element. Było to błędne założenie, ponieważ przy wyborze kategorii, na tej liście znajduje się jeszcze element breadcrumbs
, i jest on właśnie jako pierwszy. Stąd konieczność wyszukania elementu na podstawie wartości pola widget_type
.
Oto funkcja, która wyciąga nam cenę z obiektu produktu.
const priceRegex = /^\d+,\d{2}/;
function getPrice(product) {
var pricePerTypeMatch = product.price_per_type.match(priceRegex);
if (pricePerTypeMatch != null) {
var pricePerTypeString = pricePerTypeMatch[0].replace(/,/g, ".");
return parseFloat(pricePerTypeString);
}
var priceMatch = product.price.match(priceRegex);
if (priceMatch != null) {
var priceString = priceMatch[0].replace(/,/g, ".");
return parseFloat(priceString);
}
return 99999;
}
Teraz napiszmy funkcję, która otrzyma obiekt odpowiedzi na zapytanie API, odpowiednio go zmodyfikuje, i zwróci w takiej samej formie.
function sortProductsInPayload(payload) {
var verticalListPredicate = function (bodyObject) {
return bodyObject.widget_type === "vertical-list";
}
var productPriceSort = function (a, b) {
return getPrice(a) - getPrice(b);
}
var verticalListWidget = payload.data.body.find(verticalListPredicate);
verticalListWidget.list.sort(productPriceSort);
return payload
}
Na sam koniec dodajemy kod, który przechwytuje zapytania, następnie modyfikuje odpowiedź, używając wcześniejszych funkcji, i zwraca podmienioną odpowiedź przeglądarce.
Przyznaję, że ten kod po prostu znalazłem w internecie - nie jestem pewien, jak on działa, nie znam w ogóle przeglądarkowych SDK ani JavaScriptu. Ale działa, i na ten moment to mi wystarczy.
var _open = XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function (method, URL) {
var _onreadystatechange = this.onreadystatechange,
_this = this;
_this.onreadystatechange = function () {
/* USER SCRIPT GOES HERE */
if (isSearchOrCategories(URL) && _this.readyState == 4) {
var payload = JSON.parse(_this.responseText);
var sortedPayload = sortProductsInPayload(payload);
var sortedString = JSON.stringify(sortedPayload);
Object.defineProperty(_this, "responseText", { value: sortedString });
}
/* USER SCRIPT ENDS HERE */
if (_onreadystatechange) _onreadystatechange.apply(this, arguments);
};
Object.defineProperty(this, "onreadystatechange", {
get: function () { return _onreadystatechange; },
set: function (value) { _onreadystatechange = value; },
});
return _open.apply(_this, arguments);
};
Nie zapomnijmy też o nagłówku Tampermonkey, inaczej wtyczka będzie narzekać, że nie może sparsować kodu.
// ==UserScript==
// @name Everli Sort
// @author mtsz
// @description Sort Everli search/categories response
// @match *://*.everli.com/*
// @version 1.0
// @grant none
// ==/UserScript==
Gotowe. Funkcja sortowania w Everli dodana. Teraz wystarczy dodać skrypt do Tampermonkey, i cieszyć się z nowych możliwości portalu.