一种通用的豆瓣片单数据爬虫方式

一种通用的豆瓣片单数据爬虫方式

前言

我是前端切图仔,女朋友是统计学专业的,毕业课题是电影票房和网络口碑相关的研究,需要电影票房及评分数据。我找了一下github上相关的数据爬虫仓库,大多以python爬虫为主,或是年代久远接口失效,难以达到要求,且本人py水平仅为helloworld级的,因此选择使用ts-node重新造个爬虫轮子

技术选型

数据获取:x-crawl

  • 简介: 一个灵活的 Node.js 多功能爬虫库。灵活的使用方式和众多的功能可以帮助您快速、安全、稳定地爬取页面、接口以及文件。
  • 文档: x-crawl
  • 选择理由: 相较于原生的fetch或者axios等直接请求方案而言,更具有爬虫扩展性而言,后续拓展时遭遇反爬策略等也能更好处理一点,防止重复造轮子

页面HTML处理:cherrio

  • 简介: 一个为服务端定制的,快速、灵活、实施的jQuery核心实现
  • 文档: cheerio
  • 选择理由: 能够以JQ的方式处理x-crawl爬取的页面HTML文档,符合前端操作习惯

爬取方式详解

基本思路

  1. 获取页面的HTML
  2. 解析HTML,从HTML中获取数据
  3. 整理数据,输出数据文件.csv

获取页面的HTML

在片单里打开F12,点击下一页可以看到请求情况如下

请求参数:

可以看到,豆瓣片单的请求是根据请求参数start顺序请求25项数据展示在页面上的。因此我们可以根据片单的想要爬取的开始页和结束页,以及片单对应的id,配置出一个pageUrlList

typescript 复制代码
 class DouListSpider {
   protected douListCode: number
   private pageList: string[]
   constructor(startPage: number, endPage: number, douListCode: number) {
     const length = startPage - endPage + 1
     this.pageList = Array.from({ length }).map((_, idx) => {
       const baseUrl = `https://www.douban.com/doulist/${douListCode}/`
       const curStartVal = (idx + startPage - 1) * 25
       const search = `?start=${curStartVal}&sort=seq&playable=0&sub_type=`
       return `${baseUrl}${search}`
     })
     this.douListCode = douListCode
   }
 }
 export default DouListSpider
 ​

为了防止被豆瓣拉黑名单,请求的时间间隔配置为3s,且配置随机的Header

arduino 复制代码
 export const staticHeaderList = [
   'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 OPR/26.0.1656.60', ...]
 function getRandomIntInclusive(min: number, max: number) {
   return Math.floor(Math.random() * (Math.floor(max) - Math.ceil(min) + 1)) + min
 }
 export function getRandomHeader() {
   const max = staticHeaderList.length - 1
   const idx = getRandomIntInclusive(0, max)
   return staticHeaderList[idx]
 }
 ​

使用x-crawl的crawlHTML方法爬取页面的DOM,最后返回HTML的列表

typescript 复制代码
 class DouListSpider {
   private pageList: string[]
   constructor(startPage: number, endPage: number, douListCode: number) {...}
   public async crawlHTMLInfos() {
     const urlList = this.pageList.map((url) => {
       const headerAgent = getRandomHeader()
       return {
         url,
         header: {
           'User-Agent': headerAgent,
         },
       }
     })
     const instance = xCrawl({
       mode: 'sync',
       intervalTime: { max: 3000, min: 1000 },
     })
     const resList = await instance.crawlHTML(urlList)
     return resList.map((res) => {
       const { data } = res ?? {}
       if (!data)
         throw new Error('no data')
       const { html: pageHtml } = data
       return pageHtml
     })
   }
 }
 export default DouListSpider  
 ​
 ​

页面DOM结构解析

首先看片单每个元素的一些关键信息。

可以看到有以下信息:序号、电影名、评分、评价人数、简介、评语(可能是票房,可能是其他信息,也可能没有),查看DOM结构,可以拿到这些元素对应的JSPath

dart 复制代码
 const orderId = document.querySelector("#item735897688 > div > div.hd > span")
 const name = document.querySelector("#item735897688 > div > div.bd.doulist-subject > div.title > a")
 const ratingCommentInfo = document.querySelector("#item735897688 > div > div.bd.doulist-subject > div.rating")
 const description = document.querySelector("#item735897688 > div > div.bd.doulist-subject > div.abstract")
 const extraInfo = document.querySelector("#item735897688 > div > div.ft > div.comment-item.content > blockquote")

根据JSPath,写出对应的获取元素的方法如下

typescript 复制代码
 class DouListSpider {
   // 获取评分
   private getRateValue = (element: Cheerio<any>) => {
     const rateValue = element.find('.rating').find('.rating_nums').text()
     return Number(rateValue)
   }
 ​
   // 获取评价人数
   private getRatePersonCount = (element: Cheerio<any>) => {
     const rateInfos = element.find('.rating').children().last().text()
     const regex = /((\d+)人评价)/
     const match = rateInfos.match(regex) ?? []
     return Number(match[1] ?? '0')
   }
 ​
   // 获取底部信息
   private getExtraFooterInfo = (element: Cheerio<any>) => {
     const extraInfo = element.find('.ft .content .comment').text()
     const boxInfo = /[\d{.|,}]+/.exec(extraInfo)?.[0] ?? '0'
     return boxInfo.split(',').join('')
   }
 ​
   // 获取电影名称
   private getMovieNames = (element: Cheerio<any>) => {
     const target = element.find('.title a')
     const { href = '' } = target.attr() ?? {}
     if (href)
       this.urlList.push(href)
     const movieCode = Number(/\d+/.exec(href)?.[0] || '0')
     return {
       movieName: target.text().trim(),
       movieCode,
     }
   }
   // 获取orderId  
   private getOrderId = (element: Cherrio<any>) => {
     return $(element).find('.mod .hd .pos').text().trim() ?? idx + 1
   }
 }
 export default DouListSpider
 ​

片单中的每一项在HTML中的结构完全相同

因此选择通过类似于document.querrySelectorAll('.doulist-item')这种方法获取到doulist-item元素列表。然后遍历列表对元素进行数据提取,最后存储到数据列表中。数据结构设计如下

typescript 复制代码
 export interface DataListType {
   orderId: number
   movieName: string
   rateValue: number
   ratePersonCount: number
   description: string
   boxOffice: number | string
   
   movieCode: number
 }

其中movieCode是电影的id,同步获取,方便后续的详细数据爬取拓展

typescript 复制代码
  class DouListSpider {
   async startCrawlPage() {
     const htmlList: string[] = await this.crawlHTMLInfos()
     htmlList.forEach((html) => {
       const $ = load(html)
       const dataItems = $('.article .doulist-item')
       if (dataItems.length === 0)
         console.log(`page get failed`)
       dataItems.each((idx, element) => {
         const orderId = $(element).find('.mod .hd .pos').text().trim() ?? idx + 1
         const movieSubject = $(element).find('.doulist-subject')
         const { movieCode, movieName } = this.getMovieNames(movieSubject)
         const rateValue = this.getRateValue(movieSubject)
         const ratePersonCount = this.getRatePersonCount(movieSubject)
         const description = this.operateDescription(movieSubject.find('.abstract').text())
         const boxOffice = this.getExtraFooterInfo($(element))
         const obj: DataListType = {
           orderId: Number(orderId),
           movieCode,
           movieName,
           rateValue: Number(rateValue),
           ratePersonCount,
           description,
           boxOffice,
         }
         this.dataList.push(obj)
       })
     })
   }
 }
 export default DouListSpider

数据的输出

本文采用的是拼接dataList,输出成.csv的方法。这个没什么好说的

typescript 复制代码
  class DouListSpider {
    public writeData = (fileName: string = '') => {
       const csvData = this.dataList.map(item => `${item.orderId},${item.movieCode},${item.movieName},${item.rateValue},${item.ratePersonCount},"${item.description}",${item.boxOffice}`)
       const csvString = `orderId,shouldCurId,movieName,rateValue(x/10),ratePersonCount(评价人数),description,boxOffice(票房,万美元)\n${csvData.join(
         '\n',
       )}`
       writeFileSync(`./data/data-${fileName}-${this.douListCode}.csv`, csvString, 'utf8')
     }
 }
 export default DouListSpider  

考虑后续进阶的话可以采取数据库存储的方式。

进阶爬取展望

影片详情爬取

在豆瓣列表中可以获取到movieCode, 访问https://movie.douban.com/subject/${movieCode}可以获取影片详情。进一步访问子路由则可以获取更多

例如

  • 影评: https://movie.douban.com/subject/${movieCode}/reviews

  • 短评: https://movie.douban.com/subject/${movieCode}/comments

  • 获奖: https://movie.douban.com/subject/${movieCode}/awards

    ...

数据库的构建(自身学习素材)

早年间豆瓣电影有自己的公开api,但是现在似乎都难以访问。

而在页面 https://movie.douban.com/explore下筛选电影时会访问接口 https://m.douban.com/rexxar/api/v2/movie/recommend这个接口是可以爬取的。因此可以考虑通过该接口爬取电影数据,构建本地数据库。

问题的点在于该接口最大返回为500,数据很难全覆盖,所以数据完整性扔需要想办法解决

(这部分正在着手写,也属于是我自己接触后端的学习素材之一)

仓库地址

douban-movie-datas-boxoffice

欢迎批评建议。

相关推荐
Domain-zhuo10 小时前
如何提高webpack的构建速度?
前端·webpack·前端框架·node.js·ecmascript
田猿笔记11 小时前
解决 Node.js 单线程限制的有效方法
node.js
蟾宫曲11 小时前
Node.js 工具:在 Windows 11 中配置 Node.js 的详细步骤
windows·npm·node.js·前端工具
web1350858863512 小时前
前端node.js
前端·node.js·vim
滚雪球~1 天前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语1 天前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
m0_748234521 天前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
丰云1 天前
一个简单封装的的nodejs缓存对象
缓存·node.js
泰伦闲鱼1 天前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
敲啊敲95271 天前
5.npm包
前端·npm·node.js