Как я могу улавливать и обрабатывать данные из ответов XHR с помощью casperjs?

Данные на веб-странице отображаются динамически, и кажется, что проверка каждого изменения в html и извлечение данных - очень сложная задача, а также мне нужно использовать очень ненадежные XPath. Поэтому я хотел бы иметь возможность извлекать данные из пакетов XHR.

Я надеюсь, что вы сможете извлечь информацию из пакетов XHR, а также сгенерировать пакеты XHR, которые будут отправлены на сервер. Часть извлекающей информации более важна для меня, потому что отправка информации может быть легко обработана путем автоматического запуска элементов html с использованием casperjs.

Я прикрепляю снимок экрана о том, что я имею в виду. enter image description here

Текст на вкладке ответа - это данные, которые мне нужно обработать впоследствии. (Этот XHR-ответ был получен с сервера.)

Ответ 1

Это невозможно, потому что обработчик событий resource.received предоставляет только метаданные, такие как url, headers или status, но не фактические данные. Основной обработчик событий phantomjs действует одинаково.


Запрос AJAX без гражданства

Если вызов ajax не имеет статуса, вы можете повторить запрос

casper.on("resource.received", function(resource){
    // somehow identify this request, here: if it contains ".json"
    // it also also only does something when the stage is "end" otherwise this would be executed two times
    if (resource.url.indexOf(".json") != -1 && resource.stage == "end") {
        var data = casper.evaluate(function(url){
            // synchronous GET request
            return __utils__.sendAJAX(url, "GET");
        }, resource.url);
        // do something with data, you might need to JSON.parse(data)
    }
});
casper.start(url); // your script

Вы можете добавить прослушиватель событий в resource.requested. Таким образом, вам не нужно использовать способ завершения вызова.

Вы также можете сделать это прямо внутри потока управления, как это (source: A: CasperJS waitForResource: как получить ресурс, который я ждал):

casper.start(url);

var res, resData;
casper.waitForResource(function check(resource){
    res = resource;
    return resource.url.indexOf(".json") != -1;
}, function then(){
    resData = casper.evaluate(function(url){
        // synchronous GET request
        return __utils__.sendAJAX(url, "GET");
    }, res.url);
    // do something with the data here or in a later step
});

casper.run();

Запрос состояния AJAX

Если он не является апатридом, вам нужно будет заменить реализацию XMLHttpRequest. Вам нужно будет ввести собственную реализацию обработчика onreadystatechange, собрать информацию в объекте window, а затем собрать ее в другом вызове evaluate.

Вы можете посмотреть XHR faker в файле sinon.js или использовать следующий полный прокси для XMLHttpRequest (я смоделировал его после метода 3 из Как я могу создать обертку/прокси XMLHttpRequest?):

function replaceXHR(){
    (function(window, debug){
        function args(a){
            var s = "";
            for(var i = 0; i < a.length; i++) {
                s += "\t\n[" + i + "] => " + a[i];
            }
            return s;
        }
        var _XMLHttpRequest = window.XMLHttpRequest;

        window.XMLHttpRequest = function() {
            this.xhr = new _XMLHttpRequest();
        }

        // proxy ALL methods/properties
        var methods = [ 
            "open", 
            "abort", 
            "setRequestHeader", 
            "send", 
            "addEventListener", 
            "removeEventListener", 
            "getResponseHeader", 
            "getAllResponseHeaders", 
            "dispatchEvent", 
            "overrideMimeType"
        ];
        methods.forEach(function(method){
            window.XMLHttpRequest.prototype[method] = function() {
                if (debug) console.log("ARGUMENTS", method, args(arguments));
                if (method == "open") {
                    this._url = arguments[1];
                }
                return this.xhr[method].apply(this.xhr, arguments);
            }
        });

        // proxy change event handler
        Object.defineProperty(window.XMLHttpRequest.prototype, "onreadystatechange", {
            get: function(){
                // this will probably never called
                return this.xhr.onreadystatechange;
            },
            set: function(onreadystatechange){
                var that = this.xhr;
                var realThis = this;
                that.onreadystatechange = function(){
                    // request is fully loaded
                    if (that.readyState == 4) {
                        if (debug) console.log("RESPONSE RECEIVED:", typeof that.responseText == "string" ? that.responseText.length : "none");
                        // there is a response and filter execution based on url
                        if (that.responseText && realThis._url.indexOf("whatever") != -1) {
                            window.myAwesomeResponse = that.responseText;
                        }
                    }
                    onreadystatechange.call(that);
                };
            }
        });

        var otherscalars = [
            "onabort",
            "onerror",
            "onload",
            "onloadstart",
            "onloadend",
            "onprogress",
            "readyState",
            "responseText",
            "responseType",
            "responseXML",
            "status",
            "statusText",
            "upload",
            "withCredentials",
            "DONE",
            "UNSENT",
            "HEADERS_RECEIVED",
            "LOADING",
            "OPENED"
        ];
        otherscalars.forEach(function(scalar){
            Object.defineProperty(window.XMLHttpRequest.prototype, scalar, {
                get: function(){
                    return this.xhr[scalar];
                },
                set: function(obj){
                    this.xhr[scalar] = obj;
                }
            });
        });
    })(window, false);
}

Если вы хотите с самого начала захватывать вызовы AJAX, вам нужно добавить это к одному из первых обработчиков событий

casper.on("page.initialized", function(resource){
    this.evaluate(replaceXHR);
});

или evaluate(replaceXHR), когда вам это нужно.

Управляющий поток будет выглядеть следующим образом:

function replaceXHR(){ /* from above*/ }

casper.start(yourUrl, function(){
    this.evaluate(replaceXHR);
});

function getAwesomeResponse(){
    return this.evaluate(function(){
        return window.myAwesomeResponse;
    });
}

// stops waiting if window.myAwesomeResponse is something that evaluates to true
casper.waitFor(getAwesomeResponse, function then(){
    var data = JSON.parse(getAwesomeResponse());
    // Do something with data
});

casper.run();

Как описано выше, я создаю прокси для XMLHttpRequest, так что каждый раз, когда он используется на странице, я могу что-то с этим сделать. На странице, которую вы очищаете, используется обратный вызов xhr.onreadystatechange для приема данных. Проксирование выполняется путем определения конкретной функции сеттера, которая записывает полученные данные в window.myAwesomeResponse в контексте страницы. Единственное, что вам нужно сделать, это получить этот текст.


Запрос JSONP

Написание прокси для JSONP еще проще, если вы знаете префикс (функция для вызова с загруженным JSON, например, insert({"data":["Some", "JSON", "here"],"id":"asdasda")). Вы можете перезаписать insert в контексте страницы

  • после загрузки страницы

    casper.start(url).then(function(){
        this.evaluate(function(){
            var oldInsert = insert;
            insert = function(json){
                window.myAwesomeResponse = json;
                oldInsert.apply(window, arguments);
            };
        });
    }).waitFor(getAwesomeResponse, function then(){
        var data = JSON.parse(getAwesomeResponse());
        // Do something with data
    }).run();
    
  • или до получения запроса (если функция зарегистрирована непосредственно перед вызовом запроса)

    casper.on("resource.requested", function(resource){
        // filter on the correct call
        if (resource.url.indexOf(".jsonp") != -1) {
            this.evaluate(function(){
                var oldInsert = insert;
                insert = function(json){
                    window.myAwesomeResponse = json;
                    oldInsert.apply(window, arguments);
                };
            });
        }
    }).run();
    
    casper.start(url).waitFor(getAwesomeResponse, function then(){
        var data = JSON.parse(getAwesomeResponse());
        // Do something with data
    }).run();
    

Ответ 2

Возможно, я опаздываю на вечеринку, но ответ может помочь кому-то вроде меня, который позже попадет в эту проблему.

Я должен был начать с PhantomJS, затем перешел в CasperJS, но, наконец, устроился с SlimerJS. Slimer основан на Phantom, совместим с Каспером и может отправить вам тело ответа с использованием того же метода onResponseReceived в части "response.body".

Ссылка: https://docs.slimerjs.org/current/api/webpage.html#webpage-onresourcereceived

Ответ 3

@Artjom answer не работает для меня в последних версиях Chrome и CasperJS.

На основе @Artjom answer и gilly3 ответа о том, как заменить XMLHttpRequest, я разработал новое решение, которое должно работать в большинстве/всех версиях различных браузеров. Работает для меня.

SlimerJS не может работать с более новой версией FireFox, поэтому мне это не подходит.

Вот общий код для добавления списка для загрузки XHR (не зависит от CasperJS):

var addXHRListener = function (XHROnStateChange) {

    var XHROnLoad = function () {
        if (this.readyState == 4) {
            XHROnStateChange(this)
        }
    }

    var open_original = XMLHttpRequest.prototype.open;

    XMLHttpRequest.prototype.open = function (method, url, async, unk1, unk2) {
        this.requestUrl = url
        open_original.apply(this, arguments);
    };

    var xhrSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function () {

        var xhr = this;
        if (xhr.addEventListener) {
            xhr.removeEventListener("readystatechange", XHROnLoad);
            xhr.addEventListener("readystatechange", XHROnLoad, false);
        } else {
            function readyStateChange() {
                if (handler) {
                    if (handler.handleEvent) {
                        handler.handleEvent.apply(xhr, arguments);
                    } else {
                        handler.apply(xhr, arguments);
                    }
                }
                XHROnLoad.apply(xhr, arguments);
                setReadyStateChange();
            }

            function setReadyStateChange() {
                setTimeout(function () {
                    if (xhr.onreadystatechange != readyStateChange) {
                        handler = xhr.onreadystatechange;
                        xhr.onreadystatechange = readyStateChange;
                    }
                }, 1);
            }

            var handler;
            setReadyStateChange();
        }
        xhrSend.apply(xhr, arguments);
    };

}

Вот код CasperJS для создания пользовательского события при загрузке XHR:

casper.on("page.initialized", function (resource) {
    var emitXHRLoad = function (xhr) {
        window.callPhantom({eventName: 'xhr.load', eventData: xhr})
    }
    this.evaluate(addXHRListener, emitXHRLoad);
});

casper.on('remote.callback', function (data) {
    casper.emit(data.eventName, data.eventData)
});

Вот код для прослушивания события "xhr.load" и получения тела ответа XHR:

casper.on('xhr.load', function (xhr) {
    console.log('xhr load', xhr.requestUrl)
    console.log('xhr load', xhr.responseText)
});

Ответ 4

Кроме того, вы также можете напрямую загрузить контент и манипулировать им позже. Вот пример script, который я использую для извлечения JSON и сохранения его локально:

var casper = require('casper').create({
    pageSettings: {
        webSecurityEnabled: false
    }
});

var url = 'https://twitter.com/users/username_available?username=whatever';

casper.start('about:blank', function() {
   this.download(url, "hop.json");
});

casper.run(function() {
    this.echo('Done.').exit();
});