当我们需要大量获取网上的信息时,手动一个个获取必然会浪费大量的时间和精力,这时我们可以按照一定的规则编写程序,帮助我们收集信息,这就是爬虫。
网上的爬虫工具不可胜数,对于前端开发者来说Selenium是个不错的选择:支持多种编程语言(C#
、Javascript
、Java
、Python
、Ruby
),并且是直接在浏览器中运行,就跟真实的用户一样。
安装起来十分简单:
npm install selenium-webdriver
下载和自己浏览器版本对应的组件,并放到项目根目录下:
编写爬虫程序需要有四个步骤:
- 明确目标
- 业务分析
- 收集信息
- 储存数据
一、明确目标
以爬虫的“Hello World”,豆瓣Top250为例,抓取每部电影的名称、年份、评分、演员等信息。
二、业务分析
250部电影共分为10页显示,每页显示25部电影,参数start
决定从第几部开始显示。我们需要先获取到每部电影的内容页面链接,然后访问该页面获取里面的信息,根据该需求我们不难写出下面两个函数:
/** * 在电影列表页面获取每部电影内容页面链接 * @returns String[] 链接数组 */ const getMovieLinks = () => Array.from(document.querySelectorAll('.grid_view .info a')).map(link => link.href)
const getData = () => { const $rating = document.querySelector('#interest_sectl') return { 'name': document.querySelector('[property="v:itemreviewed"]').innerText, 'year': document.querySelector('.year').innerText.substr(1, 4), 'rating': { 'average': $rating.querySelector('[property="v:average"]').innerText, 'count': $rating.querySelector('[property="v:votes"]').innerText, 'weights': Array.from($rating.querySelectorAll('.rating_per')).map((rating) => rating.innerText) }, 'director': document.querySelector('#info span:first-child .attrs').innerText, 'leading': Array.from(document.querySelectorAll('#celebrities .celebrity a.name')).map((leading) => leading.innerText), 'categories': document.querySelector('#info [property="v:genre"]').innerText, } }
有了这两个核心函数,剩下的工作就只剩下往对应的页面里执行函数了。
三、收集信息
const { Builder } = require('selenium-webdriver') const driver = new Builder().forBrowser('chrome').build() const getMovieLinks = // 上面定义的函数 const getData = // 上面定义的函数 ;(async () => { /** * 访问电影列表页面,在里面执行getMovieLinks函数获取每部电影内容页面的链接 */ const links = []; for (let i = 0; i < 250; i += 25) { await driver.get(`https://movie.douban.com/top250?start=${i}`) links.push(...await driver.executeScript(getMovieLinks)) console.log(`getting links ${i}`) } /** * 分别访问上面获取到的链接,执行getData获取电影信息 */ const movies = []; for (let i = 0, len = links.length; i < len; i ++) { await driver.get(links[i]) const movie = await driver.executeScript(getData) movie.rank = i + 1 movies.push(movie) console.log(movie) } // 退出浏览器 await driver.quit() })()
理论上来说,上面的代码已经没什么问题了,应该能收集到所有电影的信息,尝试执行:
每次执行都会在不同的页面出现报错,当把getData
函数放到该页面的控制台执行时却又不会出现,因此该错误可能是因为页面未加载就执行代码引起的,所以可以稍微升级一下getData
函数:
const getMovieData = (callback) => { let finished = false const getData = () => { if (finished) return const $rating = document.querySelector('#interest_sectl') // onload和setTimeout是异步执行的,要用callback替换return实现返回 callback({ 'name': document.querySelector('[property="v:itemreviewed"]').innerText, 'year': document.querySelector('.year').innerText.substr(1, 4), 'rating': { 'average': $rating.querySelector('[property="v:average"]').innerText, 'count': $rating.querySelector('[property="v:votes"]').innerText, 'weights': Array.from($rating.querySelectorAll('.rating_per')).map((rating) => rating.innerText) }, 'director': document.querySelector('#info span:first-child .attrs').innerText, 'leading': Array.from(document.querySelectorAll('#celebrities .celebrity a.name')).map((leading) => leading.innerText), 'categories': document.querySelector('#info [property="v:genre"]').innerText, }) finished = true } // 处理页面未加载完成就执行getData引起的报错 try { getData() } catch (e) { onload = getData } // 极小几率会出现上面代码不执行的情况,延迟两秒执行保底 setTimeout(getData, 2000) }
前面的代码也需要升级为:
;(async () => { // ... const movies = []; for (let i = 0, len = links.length; i < len; i ++) { // ... const movie = await driver.executeAsyncScript(getMovieData) // ... } // ... })()
再重新执行,问题已经得到解决。
四、储存数据
movies
数组中已经包含了所有电影的信息,可以根据自身需求进行储存,这里演示另存为json
文件的做法:
const { Builder } = require('selenium-webdriver') const fs = require('fs') // ... ;(async ()=> { // ... fs.writeFileSync('top250.json', JSON.stringify(movies)) await driver.quit() })()
最后
当然真正的爬虫需求当然不会像本文一样简单,一般公司级的网站都有做反爬虫处理,反反爬虫才是爬虫岗位中最棘手的工作。