Aliexpress i GreaseMonkey, podejście drugie.

Jakiś czas temu, postanowiłem zapoznać się z GreaseMonkey, czego owocem był pewien skrypt, opublikowany tutaj. Pobawiłem się z nim trochę więcej i dodałem sporo nowych rzeczy. Warto je opisać.

Zwiększanie rezultatów wyszukiwania.

Jeśli na Aliexpress, rezultaty wyszukiwania są dziwne, to najprawdopodobniej jest to wina ciasteczek. Czemu się tak dzieje?
W serwisie działa program afiliacyjny (partnerski). Innymi słowy, większość linków do przedmiotów w serwisie, zamieszczonych w internecie, to reflinki. Powodują ustawienie ciasteczek w ten sposób, że jeśli potem kupimy coś w serwisie, to afiliant dostaje od tego prowizję. Z nieznanych mi przyczyn, jeśli mamy takie ciastka, to wyniki wyszukiwania są zawężane. Czyżby jakaś zryta polityka w programie afiliacyjnym? Nie wiem, ale da radę to usunąć i powiedzieć afiliantom „spieprzaj dziadu”.
Skrypt w tej chwili filtruje URLa i ciastka, a następnie przeładowuje stronę.

var targethost = "www.aliexpress.com";

eraseCookie ("xman_us_f"); //always erase this cookie. Fucks up search results!

//language switching
if(getCookie("intl_locale")!=="en_US"){
    eraseCookie ("intl_locale");
    eraseCookie ("aep_usuc_f");
    window.location.host=targethost;
    window.search='';
}

var qs = parseQueryParameters(window.location.search.substr(1).split('&'));
var prohibitedParams = ['aff_platform', 'sk', 'cpt', 'aff_trace_key', 'af', 'cn', 'cv', 'afref', 'ws_test'];
var contains = false;
prohibitedParams.forEach(function(element){
    if(qs[element]!==undefined){
        contains = true;
        delete qs[element];
    }
});

if(contains){
    eraseCookie ("aeu_cid");
    var newQuery='?';
    Object.keys(qs).forEach(function (element){
        newQuery = newQuery + element + '=' + qs[element]+'&';
    });
    newQuery = newQuery.substring(0, newQuery.length - 1);
    window.location.search=newQuery;
}

Za samo kaszanienie wyników odpowiada ciastko „xman_us_f”, stąd zawsze jest kasowane. Jeśli skrypt wykryje reflinka, to kasuje ciastko od programu afiliacyjnego „aeu_cid”, a następnie składa URL na nowo, usuwając parametry go ustawiające.

Pobieranie mobilnej ceny na komputerze.

Aliexpress ma swoją aplikację na androida. Niektóre przedmioty mają „mobile discount”, czyli jeśli dokonamy zakupu w aplikacji mobilnej, to zniżka jest większa. Niestety, o wartości tej zniżki możemy się przekonać dopiero w momencie załadowania strony przedmiotu na androidzie, bądź ubogiej, mobilnej stronie (m.aliexpress.com).

Na szczęście, JavaScriptem możemy wykorzystać technikę AJAX, czyli załadować tą cenę w tle i podmienić ją na ekranie.

function getMobilePrice(url, callback){
    var loc = document.createElement('a');
    loc.href=url;
    loc.search="";
    loc.host = 'm.aliexpress.com';
    GM_xmlhttpRequest({
        method: "GET",
        url: loc.href,
        onload: function(response) {
            if (response.status == 200) {
                var el = document.createElement( 'html' );
                el.innerHTML =response.responseText;
                var price = el.getElementsByClassName('ms-price-tag-price');	
                if(price[0]!==undefined)
                {
                    price = price[0].textContent;
                    var match = priceRegex.exec(price);
                    callback(match[2]+match[3]);
                }
            }
        }
    });

}
if(/\/item\/.*/.test(window.location.pathname)){
    document.addEventListener("DOMContentLoaded", function(event) { 
        var element = document.getElementById("mobile-discount-trigger");
        if(element!==null){
            getMobilePrice(window.location.href, function(price){
                element.textContent += ' - ' + price;
            });
        }
    });
}

Warto zauważyć użycie funkcji zapewnianej przez GreaseMonkey, czyli GM_xmlhttpRequest. Czemu nie możemy użyć zwykłego XMLhttpRequest? Tutaj dochodzi kwestia bezpieczeństwa, czyli mechanizmu Same-Origin Policy. Skrypt jest wywoływany dla domeny aliexpress.com, a pobieramy html z domeny m.aliexpress.com. Czyli domena się nie zgadza, przeglądarka nie pozwoli normalnie działającemu skryptowi na coś takiego. Przez funkcje GreaseMonkey już nas to nie ogranicza.
Skrypt działa tylko na stronie przedmiotu. Czemu nie można pobrać wszystkich cen mobilnych na raz? Zbyt duża ilość requestów spowoduje aktywację zabezpieczeń w serwisie, np. captcha.

Ceny z dostawą.

Podstawowe wyszukiwanie w wielu serwisach – cena wraz z wysyłką, na serwisie Aliexpress nie jest obsługiwane. Stąd większość przedmiotów ma cenę wysyłki zawartą w cenie towaru. Da jednak radę przeszukać wszystkie elementy na stronie, policzyć łączną cenę, a następnie posortować. Wady? Na następnej stronie mogą się znaleźć przedmioty, które mają mniejszą łączną cenę.

document.addEventListener("DOMContentLoaded", function(event) { 
    var filterBar = document.getElementById("filter-options");
    if(filterBar !==null){
        var text = document.createTextNode("Show final prices");
        var inner = document.createElement("span");
        inner.appendChild(text);
        outer = document.createElement("a");
        switchButton(GM_getValue('final-price'));
        outer.onclick = showFinalPricesButtonCallback;
        outer.appendChild(inner);
        filterBar.appendChild(outer);
    }
});
function switchButton(value){
    if(value===false)
        outer.className = "filter-item";
    else{
        outer.className = "filter-item selected";
        showFinalPrices();
    }
}

function showFinalPricesButtonCallback(){
    var savedValue = GM_getValue('final-price');
    if(savedValue===undefined)
        savedValue=false;
    else
        savedValue=!savedValue;
    GM_setValue('final-price', savedValue);
    switchButton(savedValue);
    if(!savedValue)
        window.location.reload();
}
// code, symbol, value, postfix
//US $139.99 - 160.99
var priceRegex=/(\w+)\ (.)(\d+\.\d+)(\ \-\ (\d+\.\d+))?/;
function showFinalPrices(){
    var externalList = document.getElementById("hs-list-items");
    var innerList = document.getElementById("hs-below-list-items");
    if(externalList.tagName=="UL"){
        var elementsToCopy = innerList.children[1];
        var scriptTemp = innerList.children[0];
        innerList.innerHTML='';
        innerList.appendChild(scriptTemp);
        Array.prototype.forEach.call(elementsToCopy.children, function (element){
            externalList.appendChild(element);
        });
        innerList=externalList;
    }
    var listItems=innerList;
    var pricesMap = new Array();
    Array.prototype.forEach.call (listItems.children, function (element){

        var shippingElement = element.getElementsByClassName('pnl-shipping')[0];
        var priceElement = element.getElementsByClassName('price-m')[0];
        var priceDecimal = getPriceValue('value',priceElement);
        var finalPrice= new Array();
        if(shippingElement!==undefined)
        {
            var shippingDecimal = getPriceValue('value',shippingElement); 
            for(i=0;i<priceDecimal.length;i++)
                finalPrice[i] = (shippingDecimal[0] + priceDecimal[i]).toFixed(2);
            replaceValue('value', priceElement, finalPrice);
        }
        else
            finalPrice = priceDecimal;
        pricesMap.push([finalPrice[0],element]);

    });
    if(/price.*/.test(qs.SortType)){
        listItems.innerHTML='';
        if(qs.SortType==='price_asc')
            pricesMap.sort(function(a,b){return a[0]-b[0];});
        else
            pricesMap.sort(function(a,b){return b[0]-a[0];});
        for(i=0;i<pricesMap.length;i++)
            listItems.appendChild(pricesMap[i][1]);
    }
}
function getPriceValue(clas, element){
    priceElement = element.getElementsByClassName(clas)[0];
    if(priceElement!==undefined){
        var match = priceRegex.exec(priceElement.textContent);
        return [parseFloat(match[3]), parseFloat(match[5])];
    }
}
function replaceValue(clas, element, value){
    element = element.getElementsByClassName(clas)[0];
    if(element!==undefined){
        if(value[1]=="NaN")
            element.textContent=element.textContent.replace(priceRegex, "Final $1 $2"+value[0]);
        else
            element.textContent=element.textContent.replace(priceRegex, "Final $1 $2"+value[0]+" - "+value[1]);

    }
}

Warto odnotować, funkcje GreaseMonkey, GM_getValue i GM_setValue służą do zapisu stanu przycisku, który aktywuje tą funkcję.

Automatyczny update

Skrypt aktualizuje się u użytkowników automatycznie, jeśli zostanie podniesiona jego wersja i zostanie wgrana na serwer. W nagłówku skryptu można zawrzeć URL, skąd GreaseMonkey będzie sprawdzał/pobierał nową wersję. Musi być to jakiś stały adres, a w dodatku po połączeniu szyfrowanym (https). Nadaje się do tego np. sekcja „Downloads” w serwisie Bitbucket.org

// ==UserScript==
// @name        Aliexpress redirector
// @description Redirects from national versions of aliexpress to global english version. No more switching!
// @namespace globalbus
// @downloadURL https://bitbucket.org/globalbus/aliexpress-redirector/downloads/aliexpress-redirector.user.js
// @match     http://*.aliexpress.com/*
// @run-at document-start
// @version     9
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// ==/UserScript==

Jest to powód, dla którego skrypt dalej ma pierwotną nazwę i jest dostępny pod tym samym linkiem 🙂

Podsumowanie

GreaseMonkey pozwala na stworzenie narzędzi, które ułatwiają poruszanie się po wielu stronach, z których nie chcemy przestać korzystać, a których twórcy są na bakier z naszymi potrzebami. Może jednak pisać je samemu, warto poszukać w internecie?
Jest ich cała masa.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *