node爬虫入门 本项目源代码地址
批量爬取网站图片 实现步骤
发送http
请求获取整个网页内容
通过cheerio
库对网页内容进行分析
提取img
标签的src
属性
使用download
库对图片进行批量下载
发送http请求
爬取目标网址 http://www.itheima.com/teacher.html#ajavaee
使用 http 模块请求网站地址
发送请求并获取详情获取网站源代码1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let http = require ('http' )let request = http.request('http://www.itheima.com/teacher.html#ajavaee' , res => { let chunsk = [] res.on('data' , c => chunsk.push(c)) res.on('end' ,()=> { console .log(Buffer.concat(chunsk).toString('utf-8' )); }) }) request.end()
cheerio库简介 cheerio 可以在服务端像使用 jQuery 一样来解析 html,使用它可以非常简单的帮助我们解析通过 http 获取到html 字符串
官网文档
安装
1 npm install cheerio --save
实例demo
1 2 3 4 5 6 7 8 const cheerio = require ('cheerio' );const $ = cheerio.load('<h2 class="title">Hello world</h2>' );$('h2.title' ).text('Hello there!' ); $('h2' ).addClass('welcome' ); $.html();
使用cheerio解析html,获取图片中的src属性值 首先要将网址的域名作为一个常量抽离出来
1 2 const HOST = 'http://www.itheima.com/' let cheerio = require ('cheerio' )
然后将获取到的网站代码用cheerio的load方法解析获取一个 $, 使用 $ 获取图片
1 2 3 4 5 6 7 8 9 10 11 12 13 res.on('end' , () => { const HTMLStr = Buffer.concat(chunsk).toString('utf-8' ) const $ = cheerio.load(HTMLStr); let imgs = Array .prototype.map.call($('.maincon .clears li .main_pic img' ), item => { return HOST + $(item).attr('src' ) }) console .log(imgs); })
使用download批量下载图片 官方文档
安装
1 npm install download --save
官方实例demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const fs = require ('fs' );const download = require ('download' ); (async () => { await download('http://unicorn.com/foo.jpg' , 'dist' ); fs.writeFileSync('dist/foo.jpg' , await download('http://unicorn.com/foo.jpg' )); download('unicorn.com/foo.jpg' ).pipe(fs.createWriteStream('dist/foo.jpg' )); await Promise .all([ 'unicorn.com/foo.jpg' , 'cats.com/dancing.gif' ].map(url => download(url, 'dist' ))); })();
使用 Promise.all 批量下载我们获取到的图片
注意:请求的路径中不能包含中文,如果有中文需要用一个全局方法 encodeURL 来对路径进行 base64 转义
1 2 3 4 5 6 7 8 9 10 let download = require ('download' )let imgs = Array .prototype.map.call($('.maincon .clears li .main_pic img' ), item => { return encodeURI (HOST + $(item).attr('src' )) }) Promise .all(imgs.map(url => download(url, 'dist' ))).then(()=> { console .log('图片获取成功' ); })
encodeURL 方法 encodeURI() 函数可把字符串作为 URI 进行编码
1 console .log(encodeURI ('http://百度.com' ));
使用http发送post请求时有时候需要携带header请求头才可以正常请求
http请求的第二个参数可以设置请求方式和请求头
- method 设置请求方法,不设置默认是 get
- headers 是一个对象,里面设置请求的头信息,需要用引号括起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 let http = require ('https' )let request = http.request('https://weibo.com/ajax/statuses/hot_band' , { method: "get" , headers: { "authority" : "weibo.com" , "method" : "GET" , "path" : "/ajax/statuses/hot_band" , "scheme" : "https" , "accept" : "application/json, text/plain, */*" , "accept-encoding" : "gzip, deflate, br" , "accept-language" : "zh-CN,zh;q=0.9" , "cookie" : "SUBP=0033WrSXqPxfM725Ws9jqgMF55529P9D9WWjZa3-O7Xh0ilHzUjr5bGu5JpX5KMhUgL.Fo-fS05XSKq7Sh52dJLoIpxqgCH8SC-RxF-4SEH8SCHFSE-4x5tt; SINAGLOBAL=8655939215617.323.1616200012427; UM_distinctid=178632914c4a0e-0c5c35f55fe3f8-3c634203-100200-178632914c58b9; ALF=1650518966; SSOLoginState=1618982966; SCF=AmdR3wyLr9BHJPHz43JVNdyeUIoju-GE2ECLznLbRe_e7p1bSHxE0b_cwSvRBWwBZoKQE3xnKfVDzl6BqldWTOI.; SUB=_2A25Ne8hnDeRhGeNL7FIV9SjMzzyIHXVu8L6vrDV8PUNbmtANLWTmkW9NSPtXpCiVfjEDU4GIDRCh1uRuK0D2il7X; wvr=6; wb_view_log_5570456040=1366*7681; _s_tentry=login.sina.com.cn; Apache=345608834203.7789.1618982971783; UOR=login.sina.com.cn,service.weibo.com,www.baidu.com; ULV=1618982971818:3:1:1:345608834203.7789.1618982971783:1616571340254; webim_unReadCount=%7B%22time%22%3A1618982990208%2C%22dm_pub_total%22%3A5%2C%22chat_group_client%22%3A0%2C%22chat_group_notice%22%3A0%2C%22allcountNum%22%3A79%2C%22msgbox%22%3A0%7D; XSRF-TOKEN=D6EqZZsYP2IFHxXs47IJQFlv; WBPSESS=chY5fvV0VYzufsU9gfOV8Hvaow8rhToTA9H35uW2CRhBnav2CB7tH6LS37WchRB6yzq7wkApy7JK43mOrl0mUIjfeWrk6RUnplJcpt-6DmKq8OqV1UGcwxm394UGfafg" , "referer" : "https://weibo.com/hot/search" , "sec-fetch-dest" : "empty" , "sec-fetch-mode" : "cors" , "sec-fetch-site" : "same-origin" , "user-agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36" , "x-requested-with" : "XMLHttpRequest" , "x-xsrf-token" : "D6EqZZsYP2IFHxXs47IJQFlv" } }, res => { let chunsk = [] res.on('data' , c => chunsk.push(c)) res.on('end' , () => { console .log(Buffer.concat(chunsk).toString('utf-8' )); }) }) request.end()
封装爬虫基础库 为了方便开发,考虑代码规范,建议使用 typescript 进行封装
准备环境
首先全局安装 typeScript
npm install typescript -g
使用 tsc
初始化项目
tsc --init
初始化完成后自动创建一个 tsconfig.json
文件,这个文件是 typescript
的配置文件
有了这个配置文件后可以吧我们编写的 ts
代码编译成 js
代码,但是只有这个文件还不够,需要进一步配置才可以
ts 的配置如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "compilerOptions" : { "target" : "es2015" , "module" : "commonjs" , "outDir" : "./bin" , "rootDir" : "./src" , "strict" : true , "esModuleInterop" : true , "skipLibCheck" : true , "forceConsistentCasingInFileNames" : true }, "include" : [ "src/**/*" ], "exclude" : [ "node_modules" , "**/*.spec.ts" ] }
hello ts 在 src
目录下新建 hellots.ts
1 console .log('hello ts' );
然后在项目根目录执行 tsc
命令开始编译,编译成功后会自动生成 bin
文件夹,bin
文件夹下有一个 hellots.js
文件,文件内容就是编译后的结果
1 2 "use strict" ;console .log('hello ts' );
创建一个祖宗类的基本功能 实现一个爬虫祖宗类
这个类接收一个对象,里面包含url,method,header
子类继承这个祖宗类,传入url自动开爬
1 2 3 4 5 class Spider { constructor (options ) { } }
设置接口 在 src 目录下新建 interfaceOptions/spiderOptions.ts
文件,这个文件用来设置这个祖宗类的参数字段类型和是否必填
1 2 3 4 5 6 export default interface spiderOptions { url: String , method?: String , Header?: Object }
导入接口并实现基础请求功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const http = require ('https' )import spiderOptions from './interfaceOptions/spiderOptions' class Spider { options: spiderOptions constructor (options: spiderOptions = { url: '' , method: 'get' } ) { this .options = options this .start() } start ( ) { let requset = http.request(this .options.url, (res: any ) => { let chunks: any = [] res.on('data' , (c: any ) => chunks.push(c)) res.on('end' , () => { console .log(Buffer.concat(chunks).toString('utf-8' )); }) }) requset.end() } } export default Spider
ts环境下使用require时的坑 在使用 require 导入 https 时会有一个报错,这是因为我们是在 ts 的开发环境下编码,需要在开发环境依赖中添加@types/node
包,打开命令提示工具执行以下这个命令即可
1 npm i --save-dev @types/node
新建测试文件查看功能是否正常 新建 src/test.ts
文件,导入 Spider
类,传入我们要爬取的 url 地址,查看功能是否正常
1 2 3 4 import Spider from './Spider' new Spider({ url: 'https://www.jianshu.com/p/c8b86b09daf0' })
切记编写好 ts 文件后我们不能直接运行,需要把ts文件编译成 js 文件才可以执行,在项目根目录下执行 tsc 开始编译代码,然后在 bin 文件夹下自动生成对应的 js 文件
然后在 bin 目录下执行 node test.js
,可以看到页面代码可以成功获取到
selenium基础使用 selenium简介 Selenium 是一个 Web 的自动化测试工具,类型像我们玩游戏用的按键精灵,它支持所 有主流的浏览器(包括 PhantomJS 这些无界面的浏览器)。 Selenium 可以根据我们的指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。
selenium-webdriver下载 首先在 npm 官网搜索 selenium
https://www.npmjs.com/search?q=selenium
选择第二个点击进来,这里我们选择 Chrome 浏览器
点击 chromedriver(.ext)会进入到一个好多版本的下载页面,此时不要着急,我们先查看我们浏览器的版本,去下载对应的版本。 这时打开 Chrome 浏览器的设置页面,查看我们浏览器的版本号
然后在刚才打开的页面选择对应的版本,这里我的浏览器版本是 90.0.4430.85,那么我们要找的版本是至少精确到 4430 的版本
找到之后点击进来选择对应的平台压缩包进行下载
选择下载目录是我们项目的根目录
下载好之后解压出来可以看到一个 exe 文件,这个文件我们要通过写代码的方式来运行它
使用selenium实现自动打开百度并搜索 首先根据官方介绍 安装依赖包
1 npm install selenium-webdriver
新建 hello-selenium.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const { Builder, By, Key, until } = require ('selenium-webdriver' ); (async function example ( ) { let driver = await new Builder().forBrowser('chrome' ).build(); try { await driver.get('https://www.baidu.com' ); await driver.findElement(By.id('kw' )).sendKeys('哔哩哔哩' , Key.RETURN); await driver.wait(until.titleIs('哔哩哔哩_百度搜索' ), 1000 ); } finally { } })();
selenium API https://www.selenium.dev/selenium/docs/api/javascript/index.html
设置无头浏览器 无头浏览器表示不打开新窗口,在后台默默爬取
1 2 3 4 5 6 7 let chrome = require ('selenium-webdriver/chrome' );let {Builder,By} = require ('selenium-webdriver' );let driver = new Builder() .forBrowser('chrome' ) .setChromeOptions(new chrome.Options().headless()) .build();
关于 chrome 浏览器的更多设置,请查看 API 文档
https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Options.html
自动打开浏览器输入关键字进行查询 下面代码实现了自动打开拉钩官网,选择上海站并搜索前端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const { Builder, By, Key, until } = require ('selenium-webdriver' ); (async function example ( ) { let driver = await new Builder().forBrowser('chrome' ).build(); await driver.get('https://www.lagou.com/' ); await driver.findElement(By.css("div#changeCityBox ul.clearfix li:nth-child(2)" )).click() await driver.findElement(By.id('search_input' )).sendKeys('前端' , Key.RETURN); })();
获取拉钩网站的第一页职位信息并保存到数组中 使用 driver.findElements
可以获取到批量元素
然后循环每个职位元素,分析每个信息的结构,获取元素中的文字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 await driver.findElement(By.id('search_input' )).sendKeys('前端' , Key.RETURN);let items = await driver.findElements(By.css("div#s_position_list>ul.item_con_list li" ))let jobList = []for (let i = 0 ; i < items.length; i++) { let item = items[i] let jobName = await item.findElement(By.css(".position>.p_top>a>h3" )).getText() let jobAddress = await item.findElement(By.css(".position>.p_top>a>span>em" )).getText() let positionLink = await item.findElement(By.css(".position>.p_top>a" )).getAttribute('href' ) let formatTime = await item.findElement(By.css(".position>.p_top>.format-time" )).getText() let money = await item.findElement(By.css(".position>.p_bot>.li_b_l>.money" )).getText() let requirement = await item.findElement(By.css(".position>.p_bot>.li_b_l" )).getText() requirement = requirement.replace(money, "" ) let jobContent = await item.findElement(By.css("div.list_item_bot>.li_b_l" )).getText() let jobwelfare = await item.findElement(By.css("div.list_item_bot>.li_b_r" )).getText() let companyname = await item.findElement(By.css("div.company>div.company_name>a" )).getText() let companylink = await item.findElement(By.css("div.company>div.company_name>a" )).getAttribute('href' ) let industry = await item.findElement(By.css("div.company>.industry" )).getText() jobList.push({ jobName, jobAddress, positionLink, formatTime, money, requirement, jobContent, jobwelfare, companyname, companylink, industry }) } console .log(jobList);
运行结果
分页爬取信息思路
首先将爬取网站信息的方法提取出来
声明一个当前页码和最大页码
首先先爬取第一页的数据,如果当前页爬取成功后,让当前页码 ++
如果当前页码小于等于最大页码,则说明没有爬完,应该点击下一页,递归调用爬取方法
常见错误分析 这个错误是由于页面数据还有加载出来,我们的爬虫就去根据我们编写的 css 结构去获取数据而导致的
1 StaleElementReferenceError: stale element reference: element is not attached to the page document
解决这种错误我们采用一个 while 循环来重复调用爬取接口,在每次循环后我们定义一个 netError 变量,默认是一个 true,表示我们这次循环没有错误发生, 然后用一个 try catch 来包裹住爬取的方法,如果发生错误会进入到 catch 中,此时设置 notError 为 false,然后在 finally 方法中去判断这个变量,finally 方法是除了 try catch 之外的另外一个方法,这个方法无论对错都会执行,所以在 finally 方法中判断 notError 是否为 true ,如果为 true 表示本次请求没有错误,则 break 退出 while 循环,否则进行 while 循环判断页面数据是否加载成功
分页爬取拉钩网站数据的完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 const fs = require ('fs' )const { Builder, By, Key, until } = require ('selenium-webdriver' ); let jobList = []let currentTag = 1 ;let maxTag;let driver = new Builder().forBrowser('chrome' ).build();(async function example ( ) { await driver.get('https://www.lagou.com/' ); await driver.findElement(By.css("div#changeCityBox ul.clearfix li:nth-child(2)" )).click() await driver.findElement(By.id('search_input' )).sendKeys('前端' , Key.RETURN); maxTag = await driver.findElement(By.css("div.page-number>.totalNum" )).getText() start() })(); async function start ( ) { console .log(`------当前正在获取第${currentTag} 页的数据,共${maxTag} 页------` ); while (true ) { let netError = true try { let items = await driver.findElements(By.css("div#s_position_list>ul.item_con_list li" )) for (let i = 0 ; i < items.length; i++) { let item = items[i] let jobName = await item.findElement(By.css(".position>.p_top>a>h3" )).getText() let jobAddress = await item.findElement(By.css(".position>.p_top>a>span>em" )).getText() let positionLink = await item.findElement(By.css(".position>.p_top>a" )).getAttribute('href' ) let formatTime = await item.findElement(By.css(".position>.p_top>.format-time" )).getText() let money = await item.findElement(By.css(".position>.p_bot>.li_b_l>.money" )).getText() let requirement = await item.findElement(By.css(".position>.p_bot>.li_b_l" )).getText() requirement = requirement.replace(money, "" ) let jobContent = await item.findElement(By.css("div.list_item_bot>.li_b_l" )).getText() let jobwelfare = await item.findElement(By.css("div.list_item_bot>.li_b_r" )).getText() let companyname = await item.findElement(By.css("div.company>div.company_name>a" )).getText() let companylink = await item.findElement(By.css("div.company>div.company_name>a" )).getAttribute('href' ) let industry = await item.findElement(By.css("div.company>.industry" )).getText() jobList.push({ jobName, jobAddress, positionLink, formatTime, money, requirement, jobContent, jobwelfare, companyname, companylink, industry }) } currentTag++ if (currentTag <= 3 ) { await driver.findElement(By.css(".item_con_pager>.pager_container>.pager_next" )).click() await start() } else { fs.writeFile('./bin/joblist.txt' , JSON .stringify(jobList), (err, data ) => { console .log('数据获取完毕' ); }) } } catch (error) { if (error) netError = false } finally { if (netError) break ; } } }
将数据保存到 xlsx 文件中 node-xlsx node-xlsx
可以将一个二维数组转成 buffer 类型数据的第三方依赖包,将生成的buffer数据写入到 xlsx 文件中即可获得一个有数据的 xlsx 文件
安装 1 npm install node-xlsx --save
使用 注意:写入的数据必须是一个二维数组,二维数组里面的第一个元素为 xlsx 文件的表头
1 2 3 4 5 import xlsx from 'node-xlsx' ;const data = [[1 , 2 , 3 ], [true , false , null , 'sheetjs' ], ['foo' , 'bar' , new Date ('2014-02-19T14:30Z' ), '0.3' ], ['baz' , null , 'qux' ]];var buffer = xlsx.build([{name : "mySheetName" , data : data}]);
设置列宽 列宽 10 表示一列可以展示 5 个汉字,10个英文字母,超出的部分将会被遮挡住
1 2 3 4 5 6 7 import xlsx from 'node-xlsx' ;const data = [[1 , 2 , 3 ], [true , false , null , 'sheetjs' ], ['foo' , 'bar' , new Date ('2014-02-19T14:30Z' ), '0.3' ], ['baz' , null , 'qux' ]]const options = {'!cols' : [{ wch : 6 }, { wch : 7 }, { wch : 10 }, { wch : 20 } ]};var buffer = xlsx.build([{name : "mySheetName" , data : data}], options);
将 buffer 写到 xlsx 文件中 1 2 3 4 5 6 7 8 9 10 11 12 13 var xlsx = require ('node-xlsx' ).default;var fs = require ('fs' )const data = [[1 , 2 , 3 ], [true , false , null , 'sheetjs' ], ['foo' , 'bar' , new Date ('2014-02-19T14:30Z' ), '0.3' ], ['baz' , null , 'qux' ]]const options = {'!cols' : [{ wch : 6 }, { wch : 7 }, { wch : 10 }, { wch : 20 } ]};var buffer = xlsx.build([{name : "mySheetName" , data : data}], options); fs.writeFile('./bin/test.xlsx' ,buffer,(err,res )=> { if (err) return console .log('写入失败' ); console .log('写入成功' ); })
打开 test.xlsx 文件查看文件内容
改造爬取拉钩网的案例 写一个公共的生成 xlsx 的方法,这个方法接收我们爬取到的数据,然后将数据处理后存入 xlsx 文件中,方便我们查看
提取公共的生成 xlsx 方法 新建 utli/node-create-excel.js
文件,导出一个方法,这个方法接收一个一维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 const xlsx = require ('node-xlsx' ).default;const fs = require ('fs' )const path = require ('path' )module .exports = function (joblist ) { let twoDList = []; joblist.forEach(item => twoDList.push(Object .values(item))) let xlsxTitle = [ '工作名称' , '工作地址' , '工作详情' , '发布时间' , '薪资范围' , '基本要求' , '行业范围' , '福利待遇' , '公司名称' , '公司介绍' , '公司规模' ] twoDList.unshift(xlsxTitle) let excelTitleWidht = { '!cols' : [{ wch: 16 }, { wch: 16 }, { wch: 16 }, { wch: 16 }, { wch: 16 }, { wch: 16 }, { wch: 16 }, { wch: 16 }, { wch: 16 }, { wch: 16 }, { wch: 16 }] }; let excelBuffer = xlsx.build([{ name: "mySheetName" , data: twoDList }], excelTitleWidht); console .log(twoDList); fs.writeFile(path.join(__dirname, "../bin/joblist.xlsx" ), excelBuffer, (err, res ) => { if (err) return console .log(err); console .log('文件写入成功' ) }) }
爬取到数据后调用方法生成xlsx 在代码顶部导入方法
1 2 const writeExcel = require ('./util/node-create-excel' )
修改原有代码,将原本写到 txt 文件的方法替换成调用生成 xlsx 方法。这里为了方便测试,只让爬虫获取第一页的数据,可根据实例需求获取前几页的数据或者所有页数据
1 2 3 4 5 6 7 8 9 10 11 12 13 if (currentTag <= 1 ) { await driver.findElement(By.css(".item_con_pager>.pager_container>.pager_next" )).click() await start() } else { writeExcel(jobList) }
执行代码查看运行效果
打开文件查看获取到的数据
完。