数据地址

百度:https://yuedu.baidu.com/partner/view/ptpress2013?fr=pidx
豆瓣:https://book.douban.com/latest

两个地址的区别

豆瓣简单但是不能请求数据过快会封ip,页面简单
百度页面有点复杂,公共类名多,且需要编码转换,而且有些地方显示的信息与页面有差距

技术点

axios —请求数据
cheerio — 转化为html
jquery — 选择元素
iconv-ilte — 编码转换
Promise — promise的使用

难点说的百度并不是豆瓣

数据请求的难点:

请求过来的数据中中文都是乱码。这是因为字符集的原因

解决办法

网页字符集怎吗看
image.png
如图百度的网页使用的是 gb2312字符集 我要让response的响应数据格式为 arraybuffer 然后使用插件 iconv-ilte 这是字符编码转换插件
代码

  1. const html = await axios
  2. .get(url, { responseType: 'arraybuffer' })
  3. .then((res) => {
  4. const str = iconv.decode(Buffer.from(res.data), 'gb2312');
  5. return iconv.encode(str, 'utf8').toString('utf8');
  6. });

cheerio与jquery 的难点

cheerio生成的HTML结构需要使用jq选择,但是cheerio不老实,自己又封装啦其他方法,例如获取元素上的属性 jq的是 attr 而cheerio中的是 attribs ,还有是不是想一下这是cheerio中的元素,还是用jq选择的元素

Promise.all的使用

之前我从诶使用过 promise.all() 这个api,了解是了解但是并没有用于项目中,我使用这个看最后是否把所有书籍信息拿回来,但是又要使用延迟,防止被封ip地址
解决方案

  1. /**
  2. * 获取每一个书籍的信息
  3. */
  4. async function fetchALL() {
  5. const list = await getLinks(); // 获取每本的url 存放与数组中
  6. const proms =[];
  7. // 遍历数组 拿取书籍详细信息
  8. for (let i = 0; i < list.length; i++) {
  9. proms.push(new Promise((res,rej) => {
  10. // 防止封ip 使用随机毫秒数
  11. setTimeout( () => {
  12. // 拿取当前书籍的详细信息
  13. const data = getBookInfo(list[i]);
  14. // 是否真的拿到
  15. if(data){
  16. res(data)
  17. } else {
  18. rej(data)
  19. }
  20. }, randomTimeout); // 随机毫秒数
  21. }))
  22. }
  23. // 全拿到才会返回数据,不然返回错误
  24. return Promise.all(proms)
  25. }

jq选择元素 元素与节点

由于百度使用公共类名过多会导致选错元素,以及有些元素没有类名。
有时候通过获取元素是获取不到想要的信息,需要元素+ 节点获取想要的信息,对节点操控很少,今天遇见啦。

  1. const $ = cheerio.load(html); // 生成html结构
  2. const imgurl = $('img.doc-info-img').attr('src'); // 获取书籍图片地址
  3. const name = $('.content-block h1.book-title').attr('title');// 获取书籍名
  4. const author = $('a.doc-info-field-val.doc-info-author-link').text(); // 获取作者
  5. const list = $('.book-information-tip'); // 获取书籍信息
  6. // 查找文本为出版时间的元素
  7. const publishSpan = list.filter((i, ele) => {
  8. return $(ele).text().includes('出版时间:');
  9. });
  10. // 获取书籍出版时间 时间是在出版时间后面的文本节点
  11. const publishDate = publishSpan[0] ? publishSpan[0].nextSibling.nodeValue.trim() : ' ';

完整代码-百度

  1. const axios = require('axios');
  2. const cheerio = require('cheerio');
  3. const Book = require('../models/Book')
  4. /**
  5. * 网页编码转换
  6. */
  7. const iconv = require('iconv-lite');
  8. /**
  9. * 获取网页HTML
  10. */
  11. async function getBooksHTML() {
  12. const res = await axios.get(
  13. 'https://yuedu.baidu.com/partner/view/ptpress2013?fr=pidx'
  14. );
  15. return res.data;
  16. }
  17. /**
  18. * 获取图书链接
  19. */
  20. async function getLinks() {
  21. const html = await getBooksHTML();
  22. const $ = cheerio.load(html);
  23. const list = $('.booklist-inner .book a.img-block');
  24. const listArr = list
  25. .map((i, ele) => {
  26. const url = 'https://yuedu.baidu.com' + ele.attribs['href'];
  27. return url;
  28. })
  29. .get();
  30. return listArr;
  31. }
  32. /**
  33. * 获取图书的详情
  34. */
  35. async function getBookInfo(url) {
  36. const html = await axios
  37. .get(url, { responseType: 'arraybuffer' })
  38. .then((res) => {
  39. const str = iconv.decode(Buffer.from(res.data), 'gb2312');
  40. return iconv.encode(str, 'utf8').toString('utf8');
  41. });
  42. const $ = cheerio.load(html); // 生成html结构
  43. const imgurl = $('img.doc-info-img').attr('src'); // 获取书籍图片地址
  44. const name = $('.content-block h1.book-title').attr('title');// 获取书籍名
  45. const author = $('a.doc-info-field-val.doc-info-author-link').text(); // 获取作者
  46. const list = $('.book-information-tip'); // 获取书籍信息
  47. // 查找文本为出版时间的元素
  48. const publishSpan = list.filter((i, ele) => {
  49. return $(ele).text().includes('出版时间:');
  50. });
  51. // 获取书籍出版时间 时间是在出版时间后面的文本节点
  52. const publishDate = publishSpan[0] ? publishSpan[0].nextSibling.nodeValue.trim() : ' ';
  53. return {
  54. name,
  55. imgurl,
  56. publishDate,
  57. author,
  58. };
  59. }
  60. /**
  61. * 获取每一个书籍的信息
  62. */
  63. async function fetchALL() {
  64. const list = await getLinks(); // 获取每本的url 存放与数组中
  65. const proms =[];
  66. // 遍历数组 拿取书籍详细信息
  67. for (let i = 0; i < list.length; i++) {
  68. proms.push(new Promise((res,rej) => {
  69. // 防止封ip 使用随机毫秒数
  70. setTimeout( () => {
  71. // 拿取当前书籍的详细信息
  72. const data = getBookInfo(list[i]);
  73. // 是否真的拿到
  74. if(data){
  75. res(data)
  76. } else {
  77. rej(data)
  78. }
  79. }, randomTimeout);
  80. }))
  81. }
  82. // 全拿到才会返回数据,不然返回错误
  83. return Promise.all(proms)
  84. }
  85. /**
  86. * 随机时间
  87. */
  88. function randomTimeout() {
  89. return Math.floor(Math.random() * 10000);
  90. }
  91. /**
  92. * 保存到数据库
  93. */
  94. async function saveToDB() {
  95. const res = await fetchALL();
  96. Book.bulkCreate(res);
  97. console.log('数据保存成功')
  98. }
  99. saveToDB();

待完善代码-豆瓣

由于豆瓣未做间隔请求数据被封ip,就只能搁置啦

  1. const axios = require('axios');
  2. const cheerio = require('cheerio');
  3. /**
  4. * 获取豆瓣读书页面的html
  5. */
  6. async function getBooksHTML() {
  7. const res = await axios.get('https://book.douban.com/latest');
  8. return res.data;
  9. }
  10. /**
  11. * 获取书籍的详情链接
  12. */
  13. async function getBooksLinks() {
  14. const html = await getBooksHTML();
  15. console.log(html, 'html')
  16. const $ = cheerio.load(html);
  17. const len = $('#content .grid-12-12 li a.cover');
  18. const linkArr = len
  19. .map((i, ele) => {
  20. const href = ele.attribs['href'];
  21. return href;
  22. })
  23. .get();
  24. console.log(linkArr, 'linkArr');
  25. return linkArr
  26. }
  27. /**
  28. * 获取书籍详情页面的数据
  29. */
  30. async function getBookInfo(url) {
  31. const res = await axios(url);
  32. const $ = cheerio.load(res.data);
  33. const name = $('h1').text().trim();
  34. const imgurl = $('#mainpic .nbg img').attr('src');
  35. const pl = $('#info span.pl')
  36. const authorSpan = pl.filter((i, ele) => {
  37. return $(ele).text().includes('作者')
  38. })
  39. const author = $(authorSpan).next().text().trim()
  40. const publishSpan = pl.filter((i, ele) => {
  41. return $(ele).text().includes('出版年')
  42. })
  43. const pluginsDate = publishSpan[0] ? publishSpan[0].nextSibling.nodeValue.trim() : ' ';
  44. return {
  45. name,
  46. imgurl,
  47. pluginsDate,
  48. author
  49. }
  50. }
  51. /**
  52. * 获取每一本书的信息
  53. */
  54. async function fetchALL () {
  55. const links = await getBooksLinks()
  56. const booksInfo = links.map( (ele) => {
  57. // 由于此处未做间隔请求数据被封ip啦
  58. return getBookInfo(ele)
  59. });
  60. return Promise.all(booksInfo)
  61. }
  62. /**
  63. * 将书籍信息写入到数据可
  64. */
  65. async function saveToDB() {
  66. const data = await fetchALL();
  67. console.log(data, 'data')
  68. }
  69. saveToDB()