web-dev-qa-db-ja.com

casperjsを使用してXHR応答からデータをキャッチして処理するにはどうすればよいですか?

Webページのデータは動的に表示され、htmlのすべての変更をチェックしてデータを抽出することは非常に困難な作業であり、非常に信頼性の低いXPathを使用する必要があるようです。したがって、XHRパケットからデータを抽出できるようにしたいと思います。

XHRパケットから情報を抽出し、サーバーに送信する「XHR」パケットを生成できるようにしたいと考えています。 casperjsを使用してhtml要素を自動的にトリガーすることで情報の送信を簡単に処理できるため、情報の抽出部分は私にとってより重要です。

私が言っていることのスクリーンショットを添付しています。enter image description here

[応答]タブのテキストは、後で処理する必要のあるデータです。 (このXHR応答はサーバーから受信されました。)

12
aste123

_resource.received_ イベントハンドラーはurlheadersstatusなどのメタデータのみを提供し、提供しないため、これは簡単には不可能です。実際のデータ。基になる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_ に追加することをお勧めします。そうすれば、通話を完了するために道を譲る必要はありません。

これは、次のように制御フロー内で行うこともできます(ソース: 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呼び出しで収集する必要があります。

sinon.jsのXHRフェイカー を確認するか、次のXMLHttpRequestの完全なプロキシを使用することをお勧めします(私は どのように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呼び出しを最初からキャプチャする場合は、これを最初のイベントハンドラーの1つに追加する必要があります

_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リクエスト

プレフィックス(ロードされたJSONで呼び出す関数(例:insert({"data":["Some", "JSON", "here"],"id":"asdasda")))がわかっている場合、JSONPのプロキシを作成するのはさらに簡単です。ページコンテキストでinsertを上書きできます

  1. ページが読み込まれた後

    _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();
    _
  2. またはリクエストを受信する前(リクエストが呼び出される直前に関数が登録されている場合)

    _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();
    _
22
Artjom B.

私はパーティーに遅れるかもしれませんが、答えは将来この問題に陥る私のような誰かを助けるかもしれません。

PhantomJSから始めて、CasperJSに移行しましたが、最終的にSlimerJSに落ち着きました。 SlimerはPhantomに基づいており、Casperと互換性があり、「response.body」部分で同じonResponseReceivedメソッドを使用して応答本文を送り返すことができます。

参照: https://docs.slimerjs.org/current/api/webpage.html#webpage-onresourcereceived

7
openrijal

@ Artjomの回答 最近のChromeおよびCasperJSバージョンでは、私には機能しません。

@ Artjomの回答XMLHttpRequestの置き換え方法に関するgilly3の回答 に基づいて、さまざまなブラウザーのほとんど/すべてのバージョンで機能する新しいソリューションを作成しました。私のために働きます。

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);
    };

}

XHRのロード時にカスタムイベントを発行するCasperJSコードは次のとおりです。

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)
});
1
naviram