ph Phantomjs không chờ tải trang đầy đủ của Wikipedia


137

Tôi đang sử dụng PhantomJS v1.4.1 để tải một số trang web. Tôi không có quyền truy cập vào phía máy chủ của họ, tôi chỉ nhận được các liên kết trỏ đến họ. Tôi đang sử dụng phiên bản Phantom lỗi thời vì tôi cần hỗ trợ Adobe Flash trên các trang web đó.

Vấn đề là nhiều trang web đang tải không đồng bộ nội dung nhỏ của họ và đó là lý do tại sao cuộc gọi lại onLoadFinished của Phantom (tương tự cho onLoad trong HTML) bị bắn quá sớm khi không phải mọi thứ vẫn được tải. Bất cứ ai cũng có thể đề xuất làm thế nào tôi có thể chờ tải toàn bộ trang web để tạo, ví dụ: ảnh chụp màn hình với tất cả nội dung động như quảng cáo?


3
Tôi nghĩ rằng đã đến lúc chấp nhận một câu trả lời
spartikus

Câu trả lời:


76

Một cách tiếp cận khác là chỉ yêu cầu PhantomJS đợi một chút sau khi trang được tải trước khi thực hiện kết xuất, theo ví dụ rasterize.js thông thường , nhưng với thời gian chờ lâu hơn để cho phép JavaScript hoàn tất tải tài nguyên bổ sung:

page.open(address, function (status) {
    if (status !== 'success') {
        console.log('Unable to load the address!');
        phantom.exit();
    } else {
        window.setTimeout(function () {
            page.render(output);
            phantom.exit();
        }, 1000); // Change timeout as required to allow sufficient time 
    }
});

1
Vâng, hiện tại tôi bị mắc kẹt với phương pháp này.
nilfalse

102
Đó là một giải pháp khủng khiếp, xin lỗi (đó là lỗi của PhantomJS!). Nếu bạn đợi một giây đầy đủ, nhưng phải mất 20ms để tải, thì hoàn toàn lãng phí thời gian (nghĩ các công việc hàng loạt) hoặc nếu mất nhiều thời gian hơn một giây, nó vẫn sẽ thất bại. Không hiệu quả và không đáng tin cậy như vậy là không thể chịu đựng cho công việc chuyên nghiệp.
CodeManX

9
Vấn đề thực sự ở đây là bạn không bao giờ biết khi nào javascript sẽ tải xong trang và trình duyệt cũng không biết điều đó. Hãy tưởng tượng trang web có một số javascript tải một cái gì đó từ máy chủ trong vòng lặp vô hạn. Từ quan điểm của trình duyệt - việc thực thi javascript không bao giờ kết thúc, vậy thời điểm nào bạn muốn ph Phantomjs nói với bạn rằng nó đã kết thúc? Vấn đề này là không thể giải quyết trong trường hợp chung trừ khi chờ đợi giải pháp hết thời gian và hy vọng điều tốt nhất.
Maxim Galushka

5
Đây vẫn là giải pháp tốt nhất như năm 2016? Có vẻ như chúng ta sẽ có thể làm tốt hơn thế này.
Adam Thompson

6
Nếu bạn đang kiểm soát mã bạn đang cố đọc, bạn có thể gọi lại cuộc gọi js ảo một cách rõ ràng: ph Phantomjs.org/api/webpage/handler/on-callback.html
Andy Smith

52

Tôi thà kiểm tra định kỳ document.readyStatetrạng thái ( https://developer.mozilla.org/en-US/docs/Web/API/document . YetState ). Mặc dù cách tiếp cận này hơi khó hiểu, nhưng bạn có thể chắc chắn rằng bên trong onPageReadychức năng bạn đang sử dụng tài liệu được tải đầy đủ.

var page = require("webpage").create(),
    url = "http://example.com/index.html";

function onPageReady() {
    var htmlContent = page.evaluate(function () {
        return document.documentElement.outerHTML;
    });

    console.log(htmlContent);

    phantom.exit();
}

page.open(url, function (status) {
    function checkReadyState() {
        setTimeout(function () {
            var readyState = page.evaluate(function () {
                return document.readyState;
            });

            if ("complete" === readyState) {
                onPageReady();
            } else {
                checkReadyState();
            }
        });
    }

    checkReadyState();
});

Giải thích thêm:

Sử dụng lồng nhau setTimeoutthay vì setIntervalngăn chặn checkReadyState"chồng chéo" và điều kiện chủng tộc khi việc thực thi của nó bị kéo dài vì một số lý do ngẫu nhiên. setTimeoutcó độ trễ mặc định là 4ms ( https://stackoverflow.com/a35380085/1011156 ) vì vậy việc bỏ phiếu chủ động sẽ không ảnh hưởng lớn đến hiệu suất chương trình.

document.readyState === "complete"có nghĩa là tài liệu được tải hoàn toàn với tất cả các tài nguyên ( https://html.spec.whatwg.org/multipage/dom.html#civerse-document-readiness ).


4
nhận xét về setTimeout so với setInterval là tuyệt vời.
Gal Bracha

1
readyStatesẽ chỉ kích hoạt khi DOM đã được tải đầy đủ, tuy nhiên mọi <iframe>yếu tố vẫn có thể được tải để nó không thực sự trả lời câu hỏi ban đầu
CodingIntrigue

1
@rgraham Điều đó không lý tưởng nhưng tôi nghĩ chúng ta chỉ có thể làm được rất nhiều với các trình kết xuất này. Sẽ có những trường hợp cạnh mà bạn sẽ không biết nếu một cái gì đó được tải đầy đủ. Hãy suy nghĩ về một trang mà nội dung bị trì hoãn, về mục đích, trong một hoặc hai phút. Thật không hợp lý khi mong đợi quá trình kết xuất sẽ diễn ra và chờ đợi một lượng thời gian không xác định. Điều tương tự cũng xảy ra với nội dung được tải từ các nguồn bên ngoài có thể chậm.
Brandon Elliott

3
Điều này không xem xét bất kỳ tải JavaScript nào sau khi DOM tải đầy đủ, chẳng hạn như với Backbone / Ember / Angular.
Adam Thompson

1
Không làm việc cho tôi cả. readyState hoàn thành có thể đã bị sa thải, nhưng trang này trống vào thời điểm này.
Steve Staple

21

Bạn có thể thử kết hợp các ví dụ Waitfor và rasterize:

/**
 * See https://github.com/ariya/phantomjs/blob/master/examples/waitfor.js
 * 
 * Wait until the test condition is true or a timeout occurs. Useful for waiting
 * on a server response or for a ui change (fadeIn, etc.) to occur.
 *
 * @param testFx javascript condition that evaluates to a boolean,
 * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
 * as a callback function.
 * @param onReady what to do when testFx condition is fulfilled,
 * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
 * as a callback function.
 * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used.
 */
function waitFor(testFx, onReady, timeOutMillis) {
    var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3000, //< Default Max Timout is 3s
        start = new Date().getTime(),
        condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()), //< defensive code
        interval = setInterval(function() {
            if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
                // If not time-out yet and condition not yet fulfilled
                condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code
            } else {
                if(!condition) {
                    // If condition still not fulfilled (timeout but condition is 'false')
                    console.log("'waitFor()' timeout");
                    phantom.exit(1);
                } else {
                    // Condition fulfilled (timeout and/or condition is 'true')
                    console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");
                    typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled
                    clearInterval(interval); //< Stop this interval
                }
            }
        }, 250); //< repeat check every 250ms
};

var page = require('webpage').create(), system = require('system'), address, output, size;

if (system.args.length < 3 || system.args.length > 5) {
    console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]');
    console.log('  paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"');
    phantom.exit(1);
} else {
    address = system.args[1];
    output = system.args[2];
    if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
        size = system.args[3].split('*');
        page.paperSize = size.length === 2 ? {
            width : size[0],
            height : size[1],
            margin : '0px'
        } : {
            format : system.args[3],
            orientation : 'portrait',
            margin : {
                left : "5mm",
                top : "8mm",
                right : "5mm",
                bottom : "9mm"
            }
        };
    }
    if (system.args.length > 4) {
        page.zoomFactor = system.args[4];
    }
    var resources = [];
    page.onResourceRequested = function(request) {
        resources[request.id] = request.stage;
    };
    page.onResourceReceived = function(response) {
        resources[response.id] = response.stage;
    };
    page.open(address, function(status) {
        if (status !== 'success') {
            console.log('Unable to load the address!');
            phantom.exit();
        } else {
            waitFor(function() {
                // Check in the page if a specific element is now visible
                for ( var i = 1; i < resources.length; ++i) {
                    if (resources[i] != 'end') {
                        return false;
                    }
                }
                return true;
            }, function() {
               page.render(output);
               phantom.exit();
            }, 10000);
        }
    });
}

3
Có vẻ như nó sẽ không hoạt động với các trang web, sử dụng bất kỳ công nghệ đẩy máy chủ nào, vì tài nguyên sẽ vẫn được sử dụng sau khi xảy ra onLoad.
nilfalse

Làm bất kỳ trình điều khiển, ví dụ. Poltergeist , có một tính năng như thế này?
Jared Beck

Có thể sử dụng WaitFor để thăm dò toàn bộ văn bản html và tìm kiếm một từ khóa đã xác định không? Tôi đã cố gắng thực hiện điều này nhưng có vẻ như việc bỏ phiếu không làm mới nguồn html được tải xuống mới nhất.
fpdragon

14

Có lẽ bạn có thể sử dụng onResourceRequestedonResourceReceivedgọi lại để phát hiện tải không đồng bộ. Đây là một ví dụ về việc sử dụng các cuộc gọi lại từ tài liệu của họ :

var page = require('webpage').create();
page.onResourceRequested = function (request) {
    console.log('Request ' + JSON.stringify(request, undefined, 4));
};
page.onResourceReceived = function (response) {
    console.log('Receive ' + JSON.stringify(response, undefined, 4));
};
page.open(url);

Ngoài ra, bạn có thể xem xét examples/netsniff.jsmột ví dụ làm việc.


Nhưng trong trường hợp này, tôi không thể sử dụng một phiên bản PhantomJS để tải nhiều trang cùng một lúc, phải không?
nilfalse

OnResourceRequested có áp dụng cho các yêu cầu AJAX / Cross Domain không? Hoặc nó chỉ áp dụng để thích css, hình ảnh .. vv?
CMCDragonkai

@CMCDragonkai Tôi chưa bao giờ sử dụng nó, nhưng dựa trên điều này có vẻ như nó bao gồm tất cả các yêu cầu. Trích dẫn:All the resource requests and responses can be sniffed using onResourceRequested and onResourceReceived
Supr

Tôi đã sử dụng phương pháp này với kết xuất PhantomJS quy mô lớn và nó hoạt động khá tốt. Bạn cần rất nhiều thông minh để theo dõi các yêu cầu và xem nếu chúng thất bại hoặc hết thời gian. Thông tin thêm: sorcery.smugmug.com/2013/12/17/USE-ph Phantomjs
Ryan Doherty

14

Đây là một giải pháp chờ tất cả các yêu cầu tài nguyên hoàn thành. Sau khi hoàn thành, nó sẽ ghi lại nội dung trang vào bàn điều khiển và tạo một ảnh chụp màn hình của trang được hiển thị.

Mặc dù giải pháp này có thể phục vụ như một điểm khởi đầu tốt, tôi đã quan sát thấy nó thất bại vì vậy nó chắc chắn không phải là một giải pháp hoàn chỉnh!

Tôi đã không có nhiều may mắn sử dụng document.readyState.

Tôi đã bị ảnh hưởng bởi ví dụ Waitfor.js được tìm thấy trên trang ví dụ ph Phantomjs .

var system = require('system');
var webPage = require('webpage');

var page = webPage.create();
var url = system.args[1];

page.viewportSize = {
  width: 1280,
  height: 720
};

var requestsArray = [];

page.onResourceRequested = function(requestData, networkRequest) {
  requestsArray.push(requestData.id);
};

page.onResourceReceived = function(response) {
  var index = requestsArray.indexOf(response.id);
  requestsArray.splice(index, 1);
};

page.open(url, function(status) {

  var interval = setInterval(function () {

    if (requestsArray.length === 0) {

      clearInterval(interval);
      var content = page.content;
      console.log(content);
      page.render('yourLoadedPage.png');
      phantom.exit();
    }
  }, 500);
});

Đã đưa ra một ngón tay cái, nhưng đã sử dụng setTimeout với 10, thay vì khoảng
GDmac

Bạn nên kiểm tra answer.stage bằng 'end' trước khi xóa nó khỏi mảng yêu cầu, nếu không nó có thể bị xóa sớm.
Reimund

Điều này không hoạt động nếu trang web của bạn tải DOM động
Buddy

13

Trong chương trình của tôi, tôi sử dụng một số logic để đánh giá xem nó có đang tải không: xem yêu cầu mạng của nó, nếu không có yêu cầu mới nào trong 200ms trước đó, tôi xử lý nó khi tải.

Sử dụng cái này, sau onLoadFinish ().

function onLoadComplete(page, callback){
    var waiting = [];  // request id
    var interval = 200;  //ms time waiting new request
    var timer = setTimeout( timeout, interval);
    var max_retry = 3;  //
    var counter_retry = 0;

    function timeout(){
        if(waiting.length && counter_retry < max_retry){
            timer = setTimeout( timeout, interval);
            counter_retry++;
            return;
        }else{
            try{
                callback(null, page);
            }catch(e){}
        }
    }

    //for debug, log time cost
    var tlogger = {};

    bindEvent(page, 'request', function(req){
        waiting.push(req.id);
    });

    bindEvent(page, 'receive', function (res) {
        var cT = res.contentType;
        if(!cT){
            console.log('[contentType] ', cT, ' [url] ', res.url);
        }
        if(!cT) return remove(res.id);
        if(cT.indexOf('application') * cT.indexOf('text') != 0) return remove(res.id);

        if (res.stage === 'start') {
            console.log('!!received start: ', res.id);
            //console.log( JSON.stringify(res) );
            tlogger[res.id] = new Date();
        }else if (res.stage === 'end') {
            console.log('!!received end: ', res.id, (new Date() - tlogger[res.id]) );
            //console.log( JSON.stringify(res) );
            remove(res.id);

            clearTimeout(timer);
            timer = setTimeout(timeout, interval);
        }

    });

    bindEvent(page, 'error', function(err){
        remove(err.id);
        if(waiting.length === 0){
            counter_retry = 0;
        }
    });

    function remove(id){
        var i = waiting.indexOf( id );
        if(i < 0){
            return;
        }else{
            waiting.splice(i,1);
        }
    }

    function bindEvent(page, evt, cb){
        switch(evt){
            case 'request':
                page.onResourceRequested = cb;
                break;
            case 'receive':
                page.onResourceReceived = cb;
                break;
            case 'error':
                page.onResourceError = cb;
                break;
            case 'timeout':
                page.onResourceTimeout = cb;
                break;
        }
    }
}

11

Tôi thấy cách tiếp cận này hữu ích trong một số trường hợp:

page.onConsoleMessage(function(msg) {
  // do something e.g. page.render
});

Hơn nếu bạn sở hữu trang đặt một số tập lệnh bên trong:

<script>
  window.onload = function(){
    console.log('page loaded');
  }
</script>

Điều này trông giống như một công việc thực sự tốt, tuy nhiên, tôi không thể nhận được bất kỳ thông điệp tường trình nào từ trang HTML / JavaScript của mình để chuyển qua ph PhantomJS ... sự kiện onConsoleMessage không bao giờ được kích hoạt trong khi tôi có thể thấy các thông báo hoàn hảo trên Bảng điều khiển trình duyệt và Tôi không có đầu mối tại sao.
Dirk

1
Tôi cần page.onConsoleMessage = function (dir) {};
Andy Balaam

5

Tôi thấy giải pháp này hữu ích trong ứng dụng NodeJS. Tôi sử dụng nó chỉ trong trường hợp tuyệt vọng vì nó khởi chạy thời gian chờ để chờ tải trang đầy đủ.

Đối số thứ hai là hàm gọi lại sẽ được gọi sau khi phản hồi đã sẵn sàng.

phantom = require('phantom');

var fullLoad = function(anUrl, callbackDone) {
    phantom.create(function (ph) {
        ph.createPage(function (page) {
            page.open(anUrl, function (status) {
                if (status !== 'success') {
                    console.error("pahtom: error opening " + anUrl, status);
                    ph.exit();
                } else {
                    // timeOut
                    global.setTimeout(function () {
                        page.evaluate(function () {
                            return document.documentElement.innerHTML;
                        }, function (result) {
                            ph.exit(); // EXTREMLY IMPORTANT
                            callbackDone(result); // callback
                        });
                    }, 5000);
                }
            });
        });
    });
}

var callback = function(htmlBody) {
    // do smth with the htmlBody
}

fullLoad('your/url/', callback);

3

Đây là một triển khai câu trả lời của Supr. Ngoài ra, nó sử dụng setTimeout thay vì setInterval như Mateusz Charytoniuk đề xuất.

Phantomjs sẽ thoát trong 1000ms khi không có bất kỳ yêu cầu hoặc phản hồi nào.

// load the module
var webpage = require('webpage');
// get timestamp
function getTimestamp(){
    // or use Date.now()
    return new Date().getTime();
}

var lastTimestamp = getTimestamp();

var page = webpage.create();
page.onResourceRequested = function(request) {
    // update the timestamp when there is a request
    lastTimestamp = getTimestamp();
};
page.onResourceReceived = function(response) {
    // update the timestamp when there is a response
    lastTimestamp = getTimestamp();
};

page.open(html, function(status) {
    if (status !== 'success') {
        // exit if it fails to load the page
        phantom.exit(1);
    }
    else{
        // do something here
    }
});

function checkReadyState() {
    setTimeout(function () {
        var curentTimestamp = getTimestamp();
        if(curentTimestamp-lastTimestamp>1000){
            // exit if there isn't request or response in 1000ms
            phantom.exit();
        }
        else{
            checkReadyState();
        }
    }, 100);
}

checkReadyState();

3

Đây là mã tôi sử dụng:

var system = require('system');
var page = require('webpage').create();

page.open('http://....', function(){
      console.log(page.content);
      var k = 0;

      var loop = setInterval(function(){
          var qrcode = page.evaluate(function(s) {
             return document.querySelector(s).src;
          }, '.qrcode img');

          k++;
          if (qrcode){
             console.log('dataURI:', qrcode);
             clearInterval(loop);
             phantom.exit();
          }

          if (k === 50) phantom.exit(); // 10 sec timeout
      }, 200);
  });

Về cơ bản được đưa ra thực tế bạn phải biết rằng trang được tải xuống đầy đủ khi một phần tử nhất định xuất hiện trên DOM. Vì vậy, kịch bản sẽ chờ cho đến khi điều này xảy ra.


3

Tôi sử dụng một hỗn hợp cá nhân của ví dụ ph Phantomjswaitfor.js .

Đây là main.jstập tin của tôi :

'use strict';

var wasSuccessful = phantom.injectJs('./lib/waitFor.js');
var page = require('webpage').create();

page.open('http://foo.com', function(status) {
  if (status === 'success') {
    page.includeJs('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js', function() {
      waitFor(function() {
        return page.evaluate(function() {
          if ('complete' === document.readyState) {
            return true;
          }

          return false;
        });
      }, function() {
        var fooText = page.evaluate(function() {
          return $('#foo').text();
        });

        phantom.exit();
      });
    });
  } else {
    console.log('error');
    phantom.exit(1);
  }
});

lib/waitFor.jstệp (chỉ là một bản sao và dán của waifFor()hàm từ waitfor.jsví dụ ph Phantomjs ):

function waitFor(testFx, onReady, timeOutMillis) {
    var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3000, //< Default Max Timout is 3s
        start = new Date().getTime(),
        condition = false,
        interval = setInterval(function() {
            if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
                // If not time-out yet and condition not yet fulfilled
                condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code
            } else {
                if(!condition) {
                    // If condition still not fulfilled (timeout but condition is 'false')
                    console.log("'waitFor()' timeout");
                    phantom.exit(1);
                } else {
                    // Condition fulfilled (timeout and/or condition is 'true')
                    // console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");
                    typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condi>
                    clearInterval(interval); //< Stop this interval
                }
            }
        }, 250); //< repeat check every 250ms
}

Phương pháp này không đồng bộ nhưng ít nhất tôi đảm bảo rằng tất cả các tài nguyên đã được tải trước khi tôi thử sử dụng chúng.


2

Đây là một câu hỏi cũ, nhưng vì tôi đang tìm tải trang đầy đủ nhưng cho Spookyjs (sử dụng casperjs và ph Phantomjs) và không tìm thấy giải pháp của mình, tôi đã tạo ra kịch bản của riêng mình cho điều đó, với cách tiếp cận tương tự như người dùng. Cách tiếp cận này là, trong một khoảng thời gian nhất định, nếu trang không nhận được hoặc bắt đầu bất kỳ yêu cầu nào, nó sẽ kết thúc thực hiện.

Trên tệp casper.js (nếu bạn đã cài đặt nó trên toàn cầu, đường dẫn sẽ có dạng như /usr/local/lib/node_modules/casperjs/modules/casper.js) thêm các dòng sau:

Ở đầu tệp có tất cả các vars toàn cầu:

var waitResponseInterval = 500
var reqResInterval = null
var reqResFinished = false
var resetTimeout = function() {}

Sau đó, bên trong hàm "createPage (casper)" ngay sau "var page = quiries ('trang web'). Create ();" thêm mã sau đây:

 resetTimeout = function() {
     if(reqResInterval)
         clearTimeout(reqResInterval)

     reqResInterval = setTimeout(function(){
         reqResFinished = true
         page.onLoadFinished("success")
     },waitResponseInterval)
 }
 resetTimeout()

Sau đó, bên trong "page.onResourceReceured = function onResourceReceured (resource) {" trên dòng đầu tiên thêm:

 resetTimeout()

Thực hiện tương tự cho "page.onResourceRequested = function onResourceRequested (requestData, request) {"

Cuối cùng, trên "page.onLoadFinished = function onLoadFinished (status) {" trên dòng đầu tiên thêm:

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

Và đó là, hy vọng điều này sẽ giúp ai đó gặp rắc rối như tôi. Giải pháp này dành cho casperjs nhưng hoạt động trực tiếp cho Spooky.

Chúc may mắn !


0

đây là giải pháp của tôi nó làm việc cho tôi

page.onConsoleMessage = function(msg, lineNum, sourceId) {

    if(msg=='hey lets take screenshot')
    {
        window.setInterval(function(){      
            try
            {               
                 var sta= page.evaluateJavaScript("function(){ return jQuery.active;}");                     
                 if(sta == 0)
                 {      
                    window.setTimeout(function(){
                        page.render('test.png');
                        clearInterval();
                        phantom.exit();
                    },1000);
                 }
            }
            catch(error)
            {
                console.log(error);
                phantom.exit(1);
            }
       },1000);
    }       
};


page.open(address, function (status) {      
    if (status !== "success") {
        console.log('Unable to load url');
        phantom.exit();
    } else { 
       page.setContent(page.content.replace('</body>','<script>window.onload = function(){console.log(\'hey lets take screenshot\');}</script></body>'), address);
    }
});
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.