如何在 Puppeteers .evaluate() 方法中传递函数?

Posted

技术标签:

【中文标题】如何在 Puppeteers .evaluate() 方法中传递函数?【英文标题】:How to pass a function in Puppeteers .evaluate() method? 【发布时间】:2018-04-28 12:23:19 【问题描述】:

每当我尝试传递一个函数时,像这样:

var myFunc = function()  console.log("lol"); ;

await page.evaluate(func => 
 func();
 return true;
, myFunc);

我明白了:

(node:13108) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Evaluation failed: TypeError: func is not a function
at func (<anonymous>:9:9)
(node:13108) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

为什么?如何正确操作?

谢谢!

€:让我澄清一下:我这样做是因为我想先找到一些 DOM 元素并在该函数中使用它们,更像这样(简化):

var myFunc = function(element)  element.innerhtml = "baz" ;

await page.evaluate(func => 
  var foo = document.querySelector('.bar');
  func(foo);
  return true;
, myFunc);

【问题讨论】:

这能回答你的问题吗? How can I dynamically inject functions to evaluate using Puppeteer? 【参考方案1】:

您不能将函数直接传递给page.evaluate(),但您可以调用另一个特殊方法(page.exposeFunction),它将您的函数公开为全局函数(也可作为页面window 对象的属性使用) ,所以当你在page.evaluate()里面时可以调用它:

var myFunc = function()  console.log("lol"); ;
await page.exposeFunction("myFunc", myFunc);

await page.evaluate(async () => 
   await myFunc();
   return true;
);

请记住page.exposeFunction() 将使您的函数返回一个Promise,然后,您需要使用asyncawait。发生这种情况是因为您的函数不是running inside your browser,而是在您的nodejs 应用程序中。

    exposeFunction() does not work after goto() Why can't I access 'window' in an exposeFunction() function with Puppeteer? How to use evaluateOnNewDocument and exposeFunction? exposeFunction remains in memory? Puppeteer: pass variable in .evaluate() Puppeteer evaluate function allow to pass a parameterized funciton as a string to page.evaluate Functions bound with page.exposeFunction() produce unhandled promise rejections exposed function queryseldtcor not working in puppeteer How can I dynamically inject functions to evaluate using Puppeteer?

【讨论】:

【参考方案2】:

木偶师issue讨论了类似的问题。

有几种方法可以解决您的问题。第一条规则是保持简单。

评估函数

这是最快的处理方式,你只需传递函数并执行它。

await page.evaluate(() => 
  var myFunc = function(element)  element.innerHTML = "baz" ;
  var foo = document.querySelector('.bar');
  myFunc(foo);
  return true;
);

预先暴露函数

您可以使用 page.evaluate 或 page.addScriptTag 预先公开该函数

// add it manually and expose to window
await page.evaluate(() => 
  window.myFunc = function(element)  element.innerHTML = "baz" ;
);

// add some scripts
await page.addScriptTag(path: "myFunc.js");

// Now I can evaluate as many times as I want
await page.evaluate(() => 
  var foo = document.querySelector('.bar');
  myFunc(foo);
  return true;
);

使用元素句柄

page.$(选择器)

您可以将元素句柄传递给 .evaluate 并根据需要进行更改。

const bodyHandle = await page.$('body');
const html = await page.evaluate(body => body.innerHTML, bodyHandle);

page.$eval

您可以定位一个元素并根据需要进行更改。

const html = await page.$eval('.awesomeSelector', e => 
e.outerHTML = "whatever"
);

诀窍是read the docs 并保持简单。

【讨论】:

第一个解决方案有什么意义,你可以传递函数并执行它。?如果我们想在多个evaluate 调用中使用相同的myFunc 怎么办? 就像下面的答案所说,你不能像使用 page.evaluate 的变量那样将函数传递到页面中。例如:await page.evaluate(async func =&gt; /* ... */ , myFunc) 【参考方案3】:

带参数的传递函数

//手动添加并暴露到窗口

 await page.evaluate(() => 
      window.myFunc = function(element)  element.innerHTML = "baz" ;
    );

//然后调用上面声明的函数

 await page.evaluate((param) => 
         myFunc (param);
    , param);

【讨论】:

我不应该在page.evaluate(...) 中调用window.myFunc 而不是myFuncmyFunc 分配给window.myFunc 后如何进入全局命名空间? 你的答案与投票最多的答案有何不同?【参考方案4】:

抛出错误是因为你执行了func();func 不是函数。我更新我的答案以回答您更新的问题:

选项 1:在页面上下文中执行您的函数:

var myFunc = function(element)  element.innerHTML = "baz" ;
await page.evaluate(func => 
  var foo = document.querySelector('.bar');
  myFunc(foo);
  return true;
);

选项 2:将元素句柄作为参数传递

const myFunc = (element) =>  
    innerHTML = "baz";
    return true;

const barHandle = await page.$('.bar');
const result = await page.evaluate(myFunc, barHandle);
await barHandle.dispose();

`

【讨论】:

不是真的,我想在 .evaluate 函数中评估 myFunc。这样我就可以在浏览器上下文中做一些事情,并使用它在浏览器上下文中调用 myFunc: 之前能够做其他事情并将其传递给函数:await page.evaluate(func => /* 在这里做事情 / func( / 使用这些事情在这里 */ ); return true; , myFunc); 所以你可以这样做:` var myFunc = function(element) element.innerHTML = "baz" ;等待 page.evaluate(() => var foo = document.querySelector('.bar'); myFunc(foo); return true; ); ` 这是错误的。您不能将测试中的全局范围函数传递到 page.evaluate 中。【参考方案5】:
//  External function to run inside evaluate context
function getData() 
        return document.querySelector('title').textContent;
    

function mainFunction(url, extractFunction)
    let browser = await puppeteer.launch();
    let page = await browser.newPage();

    await page.goto(url);

    let externalFunction = Object.assign(extractFunction);

    let res = await this.page.evaluate(externalFunction)

    console.log(res);

    

// call it here
mainFunction('www.google.com',getData);

【讨论】:

【参考方案6】:

创建了一个包装 page.evaluate 的辅助函数:

const evaluate = (page, ...params) => browserFn => 
    const fnIndexes = [];
    params = params.map((param, i) => 
        if (typeof param === "function") 
            fnIndexes.push(i);
            return param.toString();
        
        return param;
    );
    return page.evaluate(
        (fnIndexes, browserFnStr, ...params) => 
            for (let i = 0; i < fnIndexes.length; i++) 
                params[fnIndexes[i]] = new Function(
                    " return (" + params[fnIndexes[i]] + ").apply(null, arguments)"
                );
            
            browserFn = new Function(
                " return (" + browserFnStr + ").apply(null, arguments)"
            );
            return browserFn(...params);
        ,
        fnIndexes,
        browserFn.toString(),
        ...params
    );
;

export default evaluate;

获取所有参数并将函数转换为字符串。 然后在浏览器上下文中重新创建函数。 见https://github.com/puppeteer/puppeteer/issues/1474

你可以像这样使用这个函数:

const featuredItems = await evaluate(page, _getTile, selector)((get, s) => 
    const items = Array.from(document.querySelectorAll(s));
    return items.map(node => get(node));
);

【讨论】:

你在这里用这些代码做什么?怎么用? 这让我可以为浏览器上下文创建模块化函数,将它们导入我的脚本并将它们传递给评估。 IE。 _getTile 可以访问文档,从它自己的 javascript 文件中导入,然后传递给评估方法。通过对函数进行字符串化然后在浏览器上下文中重新创建,您可以将函数传递给 Puppeteer .evaluate() 方法,如问题中所述。【参考方案7】:
function stringifyWithFunc(obj) 
  const str = JSON.stringify(obj, function(key, val) 
      if (typeof val === "function") 
        return val + "";
        return val;
      );
    return str;
  

  function parseWithFunction(str) 
    const obj = JSON.parse(str, function(key, val) 
      if (typeof val === 'string' && val.includes("function")) 
        return eval(`($val)`);
      
      return val;
    );
    return obj;
  

  function testFunc() 
    console.log(123);
  ;

  const params = 
    testFunc,
    a: 1,
    b: null
  

  await page.exposeFunction("parseWithFunction", parseWithFunction);

  await pageFrame.$eval(".category-content", (elem, objStr) => 
      const params = parseWithFunction(objStr);
      params.testFunc()
    ,
    stringifyWithFunc(params)
  );

【讨论】:

以上是关于如何在 Puppeteers .evaluate() 方法中传递函数?的主要内容,如果未能解决你的问题,请参考以下文章

如何解决在electron里无法使用puppeteer的evaluate函数

Puppeteer 记录在 page.evaluate

在phantom.js中同步page.evaluate

如何在使用 fit_generator 和 evaluate_generator 训练我的网络时绘制 AUC 和 ROC?

从 Puppeteer 中的 page.evaluate 获取元素?

在 Application.Evaluate 中使用多个 UDF 函数