欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

记录一次完整的爬虫流程

程序员文章站 2022-06-17 10:25:38
...

一般来说,平台上爬取内容的方案有两种:
一种是通过web,即借助基于puppeteer的爬虫方案
另一种是通过app,即借助基于uiautomator2的UI自动化方案

本次介绍web爬取的方案,这里需要简单学习下puppeteer框架(相关资料https://github.com/GoogleChrome/puppeteer)
下面介绍具体操作流程:
(1)如何用代码定位网页上的一个元素?
Chrome的开发者工具可以很好地帮助我们完成这个工作,以百度应用商店为例:
记录一次完整的爬虫流程
单击一下,会发现在开发者工具的Elements这个tab中有一个元素被高亮了,这就是待定位的元素的HTML代码,右键点击该元素,按照下图选择Copy selector,即可得到该元素在页面中的代码定位
记录一次完整的爬虫流程
至此,拥有了开发者工具帮我们确定的元素的selector,我们就可以对其进行一系列操作了,比如输入、点击等操作
(2)环境的初始化流程
我们需要创建一个浏览器示例,同时还需要让浏览器生成一个新页面,这样才能进行我们之后的操作,大致代码如下:

const browser = await puppeteer.launch({
    args: ["--proxy-server='direct://'", '--proxy-bypass-list=*']
});
const page = await browser.newPage();

有了这个page变量,我们才方便进行接下来的操作

(3)如何正确地进行输入?
从第(1)步中我们可以获得一个元素的selector,但是如果想要在一个元素中输入内容的话,首先需要保证它是一个,其次需要通过如下代码selector转换成元素后再进行输入(上图中Copy下来的selector是#searchText)

const searchInputSel = '#searchText';
let inputEle = await page.$(searchInputSel);
await inputEle.type('第五人格');

(4)如何正确进行点击?
这里不仅包含如何执行点击操作,还包含如何监控点击后的页面变化。
首先是执行点击操作,只要保证元素时可以点击的就行,代码很简单,如下:

const searchEnterSel = '#btnSearch';
await page.click(searchEnterSel);

可以看出只需要selector就行。这个selector实际上是上面那个输入框右边的搜索按钮。

这类搜索按钮点击后通常有3个结果:1.本tab转到一个新地址,并载入搜索结果;2.弹出一个新tab,里面是搜索结果;3.本tab的hash变化(即#符号后面的内容变化),并载入搜索结果

这3种结果的处理方式不尽相同,这里分别举例:
1)转到新地址
直接贴示例代码:

const [response] = await Promise.all([
    page.waitForNavigation({timeout:0, waitUntil:'networkidle0'}),
    page.click(searchEnterSel),
]);

这是官方文档的建议写法,用文字解释的话大致是:等到点击以及页面跳转全部完成
2)弹出新tab
这里的示例代码源于*上的一个回答,如下:

const homeTarget = page.target();
await page.click(searchEnterSel);
const newTarget = await browser.waitForTarget(target => target.opener() == homeTarget);
const searchResultPage = await newTarget.page();
await searchResultPage.reload({timeout:0, waitUntil:'networkidle0'});

其中,最后一个reload可以换成一些waitFor之类的操作,总之就是等到页面中有我们想要的元素。
这个过程用文字解释的话大致是:点击后等到开启者为本页面的弹出页面载入完成
3)本tab的hash变化
需要明确的一点是,hash变化并不是navigation,因此不能用waitForNavigation这个函数,需要改用如下方法:

const [response] = await Promise.all([
    page.waitForSelector(wantedSelector),
    page.click(searchEnterSel),
]);

重点在于找到wantedSelector,这个selector的要求是:它出现即代表搜索过程结束(或者大致结束,只需等待一些贴图的载入)

(5)如何提取元素的文字信息?
我们获取搜索结果后,必须要判定搜索到的东西是否为想要的。目前各种应用商店都是模糊查询,搜“第一人格”的搜索结果第一位大多数时候同样为“第五人格”,因此,需要提取文字信息来做确认。
目前碰到两种情况:1)文字被诸如的标签包起来;2)文字为标签的一个属性值
针对这两种情况,有着不同的解决方案,如下:
1.文字被标签包起来
这里直接贴示例代码

const nameSel = '...';
let nameEle = await page.$(nameSel);
if (nameEle) {
    let nameText = await (await nameEle.getProperty('innerText')).jsonValue();
}

这里需要注意的是两种await
2.文字作为标签的一个属性
比如,vivo应用商店的搜索结果中,应用名就作为标签的data-name属性存在,这里示例代码如下:

const nameSel = '...';
let nameText = await page.evaluate(`document.querySelector("${nameSel}").getAttribute("data-name")`);

至于爬虫脚本的统一格式(包含输入和输出,可能还包含和一些组件的交互),稍后我会制定

(6)如何在编写过程中确认脚本的正确性(即如何调试)?
虽然基于puppeteer的脚本在了解一些Node.js之后编写起来比较容易,但是每个人都很难保证脚本的每一步都完全按照所想的去执行。偏偏puppeteer默认情况下是*面的,那么到底怎么在脚本编写的过程中确认自己的编写是正确的呢?这里给出两种方案:
1.“print”大法
这里不仅仅是单纯的print出语句(Node.js中需要用console.log或者其他方式进行打印),还包含在想要确认的位置生成当前页面的截图。生成当前页面的截图的方法如下:

await page.screenshot({path: 'shot.png'});

2.编写过程中开启界面
虽然puppeteer默认情况下是*面的浏览器,但是你仍然可以通过一些配置让它启动一个有界面的浏览器,这样你就能看到每一步的操作了。我们要针对浏览器实例做出一些修改,关键部分如下:

const browser = await puppeteer.launch({headless: false});

// browser.close();

当然,这里并不是说launch函数里面不能传其他参数了,只是说必须将headless的值设置成false,这样才能打开一个有界面的浏览器。而注释掉close函数则可以保证在代码运行结束后浏览器仍然保留,方便你确认页面
下面是具体代码 实现需求:在应用商店获取第五人格游戏的相关数据

let appName = opts['appName'];
        const browser = await puppeteer.launch({
            args: ["--proxy-server='direct://'", '--proxy-bypass-list=*']
        });
        const page = await browser.newPage();
        page.setViewport({
            width: 1920,
            height: 1080
        });
        function defer(exitCode) {
            browser.close();
            return exitCode;
        }
        const url = 'https://sj.qq.com/myapp/';
        await page.goto(url, waitOptions);
        const searchInputSel = '#J_MainInput';
        const searchEnterSel = '#J_SearchBtn';

        let inputEle = await page.$(searchInputSel);
        await inputEle.type(appName);
        const [response] = await Promise.all([
            page.waitForNavigation(waitOptions),
            page.click(searchEnterSel),
        ]);
        const bestMatchSel='body > div.search-default-container.J_Mod';
        let bestMatchEle = await page.$(bestMatchSel);
        if (!bestMatchEle) {
            console.log(`没找到包含${appName}相关结果`);
            return defer(-1);
        }
        const matchedNameSel = '#J_SearchDefaultListBox > li:nth-child(1) > div.search-boutique-data > div.data-box > div.name-line > div.name > a';
        await checknameinfo(page,matchedNameSel);
        const [detailResponse] = await Promise.all([
            page.waitForSelector(matchedNameSel),
            page.click(matchedNameSel),
        ]);
        const homeTarget = page.target();
        const newTarget = await browser.waitForTarget(target => target.opener() == homeTarget);
        const searchResultPage = await newTarget.page();
        await searchResultPage.reload(waitOptions);
        const packageName='#J_DetDataContainer > div > div.det-ins-container.J_Mod > div.det-ins-btn-box > a.det-ins-btn';
        const versionCode = '#J_DetDataContainer > div > div.det-othinfo-container.J_Mod > div:nth-child(2)';
        const publishDate = '#J_ApkPublishTime';
        const packageSize='#J_DetDataContainer > div > div.det-ins-container.J_Mod > div.det-ins-data > div.det-insnum-line > div.det-size';
        const screen='#picInImgBoxImg0'+','+'#picInImgBoxImg1'+','+'#picInImgBoxImg2'+','+'#picInImgBoxImg3';
        const icon='#J_DetDataContainer > div > div.det-ins-container.J_Mod > div.det-icon > img';
        const intro='#J_DetAppDataInfo > div:nth-child(1)';
        await outputInfo(searchResultPage,packageName,'应用的包名');
        await outputInfo(searchResultPage,packageSize,'软件大小');
        await outputInfoUrl(searchResultPage,icon,'应用图标的url');
        await outputInfoUrl(searchResultPage,screen,'应用的截图');
        await outputInfo(searchResultPage,publishDate,'更新日期');
        await outputInfo(searchResultPage,versionCode,'应用的版本号');
        await outputInfo(searchResultPage,intro,'应用的简介');
        browser.close();

node运行脚本:记录一次完整的爬虫流程

相关标签: 爬虫 js web