puppeteer 爬取网易云网站问题
发布于 6 年前 作者 zhongshiji 4768 次浏览 来自 问答

转自 @huyiyang 的代码: 获取网易云的歌词和评论

const fs = require('fs');
const puppeteer = require('puppeteer');

(async () => {
  const browser = await (puppeteer.launch({ executablePath: '/Users/huqiyang/Documents/project/z/chromium/Chromium.app/Contents/MacOS/Chromium', headless: false }));
  const page = await browser.newPage();
  // 进入页面
  await page.goto('https://music.163.com/#');

  // 点击搜索框拟人输入 鬼才会想起
  const musicName = '鬼才会想';
  await page.type('.txt.j-flag', musicName, {delay: 0});

  // 回车
  await page.keyboard.press('Enter');

  // 获取歌曲列表的 iframe
  await page.waitFor(2000);
  let iframe = await page.frames().find(f => f.name() === 'contentFrame');
  const SONG_LS_SELECTOR = await iframe.$('.srchsongst');

  // 获取歌曲 鬼才会想起 的地址
  const selectedSongHref = await iframe.evaluate(e => {
    const songList = Array.from(e.childNodes);
    const idx = songList.findIndex(v => v.childNodes[1].innerText.replace(/\s/g, '') === '鬼才会想起');
    return songList[idx].childNodes[1].firstChild.firstChild.firstChild.href;
  }, SONG_LS_SELECTOR);

  // 进入歌曲页面
  await page.goto(selectedSongHref);

  // 获取歌曲页面嵌套的 iframe
  await page.waitFor(2000);
  iframe = await page.frames().find(f => f.name() === 'contentFrame');

  // 点击 展开按钮
  const unfoldButton = await iframe.$('#flag_ctrl');
  await unfoldButton.click();

  // 获取歌词
  const LYRIC_SELECTOR = await iframe.$('#lyric-content');
  const lyricCtn = await iframe.evaluate(e => {
    return e.innerText;
  }, LYRIC_SELECTOR);

  console.log(lyricCtn);

  // 截图
  await page.screenshot({
    path: '歌曲.png',
    fullPage: true,
  });

  // 写入文件
  let writerStream = fs.createWriteStream('歌词.txt');
  writerStream.write(lyricCtn, 'UTF8');
  writerStream.end();

  // 获取评论数量
  const commentCount = await iframe.$eval('.sub.s-fc3', e => e.innerText);
  console.log(commentCount);

  // 获取评论
  const commentList = await iframe.$$eval('.itm', elements => {
    const ctn = elements.map(v => {
      return v.innerText.replace(/\s/g, '');
    });
    return ctn;
  });
  console.log(commentList);
})();

在18-21行,运行到:

  // 获取歌曲列表的 iframe
  await page.waitFor(2000);
  let iframe = await page.frames().find(f => f.name() === 'contentFrame');
  const SONG_LS_SELECTOR = await iframe.$('.srchsongst');
  console.log('SONG_LS_SELECTOR') // null

输出 SONG_LS_SELECTORnull, 导致往后运行不下去,请问是什么原因?

12 回复

@所有人

试试不用它的api, 自己在dom里找iframe呢?

const SONG_LS_SELECTOR = await page.evaluate(() => {
  let iframe = document.querySelector('iframe#g_iframe');
  if(!iframe) return null;
  return iframe.contentWindow.document.querySelector('.srchsongst');
})

@icheer 这样也是null呢。 而且我这样:

  // 获取歌曲列表的 iframe
  await page.waitFor(2000);
  let iframe = await page.frames().find(f => f.name() === 'contentFrame');
  // const SONG_LS_SELECTOR = await iframe.$('.srchsongst');
  const SONG_LS_SELECTOR = await page.evaluate(() => {
    let iframe = document.querySelector('iframe#g_iframe');
    console.log('这里');   //  没有输出'这里'
    if(!iframe) return null;
    return iframe.contentWindow.document.querySelector('.srchsongst');
  });
  console.log(SONG_LS_SELECTOR); // null
  // =======================================================================

只是输出了一个null。

在page.evaluate() 内部的console.log, 是会打印到chromium浏览器的控制台上, 而不会打印到你的终端或powershell里

另外querySelector(‘iframe#g_iframe’) 这里写的#g_iframe是我观察网易云音乐里有这样一个iframe的id, 你还需要验证一下, 你要抓的是不是这个id

iframe 还没加载完吧。加个 on(‘load’) 或者 waitForSelector 呢?

楼上说的对, 另外, 才发现你demo中这一行:

await page.goto('https://music.163.com/#');

不等页面加载完就去操作dom, 输入歌名了…

需要改为:

await page.goto('https://music.163.com/#', { waitUntil: 'networkidle2' });

的确是因为iframe还没有加载完,原代码只是单纯

await page.waitFor(2000);

网速的关系。谢谢两位大佬 @icheer @zhuweiyou 另外请问下怎么 **加个 on(‘load’) ** 呢?在puppeteer的文档并没有看见这个东西。

@zhongshiji

await page.goto('https://music.163.com/#', { waitUntil: 'networkidle2' });

这里的waitUntil: ‘networkidle2’ 就是做这件事的, 一直等…直到页面不再有网络流量为止

如果一定要写onload, 可以在page.evaluate()里面写… 我觉得可以把evaluate当成是一场梦, 你不能传入任何参数(非得传的话, 可以通过goto, 在url里用hash传递一些信息到梦里), 只能用return传出你想要的信息, 梦醒后(evaluate执行完了), 你只记得梦中return出的信息

@icheer page.evaluate()真是个神奇的方法… 我觉得:

await page.waitFor(2000);

这太蠢了,也容易出错,于是我想用Frame.waitForSelector()方法去等待,所以我这样写:

  // await page.waitFor(2000);
  await page.waitForSelector('#g_iframe.srchsongst', {
    visible: true
  });

但是它报了超时的错,设了 timeout: 0 的话就会一直等待下去。 请问用Frame.waitForSelector或者page.waitForSelector去 等待iframe里某个选择器加载完毕 的正确姿势是什么呢?

@zhongshiji 文档有啊,可以直接 .on(‘load’)

@zhuweiyou 好的,谢谢,查到了。

回到顶部