记录一次完整的爬虫流程
一般来说,平台上爬取内容的方案有两种:
一种是通过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运行脚本:
下一篇: 记一次坑爹的爬虫经历