目录

---------

2024 年第一篇文章.

本文章的完整代码在 Plumbiu/twid 可以看到,项目已经实现通过命令行的方式爬取推特图片/视频了,这里介绍一下基本思路。

Puppeteer 是一个 Node 工具库,它提供了一套高阶 API 来通过 DevTools 协议控制 Chromium 或 Chrome —— 官方介绍。

简而言之,Puppeteer 提供了一套编程方法,模拟用户使用浏览器,例如打开某个网页,滑动页面等操作,也可以监听页面中的相应等,由于是直接操作浏览器,所以 Puppeteer 几乎可以爬取任意网站的内容。

如何爬取图片

图片/视频内容主要在 /user/media 页面,然而这个页面需要用户登录才可以,一般登录的信息都是在 Cookie 或者 Localstroge 里,而推特将登录信息放在了 cookie -> auth_token 字段上,如下图:

同时 puppeteer 也提供了设置 cookie 的方法:

Typscript
import { launch } from 'puppeteer'

// 启动浏览器
const browser = await launch()
// 打开一个页面
const page = await browser.newPage()
// 页面跳转到 baseUrl 地址
await page.goto(baseUrl)
// 设置页面的 cookie 值
await page.setCookie({
  name: 'auth_token',
  value: token,
})
// 设置好后,跳转到对应的 media 页面
await page.goto(baseUrl + '/media')

现在,我们能打开推特用户的媒体页面了,紧接着就是监听我们对图片的请求:

Typscript
// 定义一个 set 结构,用来存储图片地址
const images = new Set()
page.on('request', () => {
  // 请求的类型
  const reqType = req.resourceType()
  // 请求的地址
  const reqUrl = req.url()
  if (reqType === 'image') {
    // 如果请求类型为图片,将该地址添加到 images
    images.add(reqUrl)
  }
})

获取图片地址的方法有了,但是还有个问题,推特的图片是懒加载的,这意味着我们需要滚动,才能获取所有的图片,在翻阅 puppeteer 的 issue 时,找到了模拟浏览器滚动的方法:

Typscript
async function scrollToBottom(page: Page) {
  // page.evaluate 运行在浏览器环境中
  await page.evaluate(async () => {
    await new Promise<void>((resolve, _reject) => {
      // 浏览器总高度
      let totalHeight = 0
      // 每一次滚动的距离
      const distance = 100
      // 定时器,每 500ms 滚动 100px 像素
      const timer = setInterval(() => {
        const scrollHeight = document.body.scrollHeight
        window.scrollBy(0, distance)
        totalHeight += distance
        // 如果总高度和滚动高度一致,那么说明我们滚动到底部了
        if (totalHeight >= scrollHeight) {
          clearInterval(timer)
          resolve()
        }
      }, 500)
    })
  })
}

继续完善我们之前的代码,只需要加上一句话

Typscript
// 定义一个 set 结构,用来存储图片地址
const images = new Set()
page.on('request', () => {
  // 请求的类型
  const reqType = req.resourceType()
  // 请求的地址
  const reqUrl = req.url()
  if (reqType === 'image') {
    // 如果请求类型为图片,将该地址添加到 images
    images.add(reqUrl)
  }
})
// 添加这一句话,就可以模拟滚动了
await scrollToBottom()

另外在最后记得要把浏览器和页面关掉:

Typscript
await page.close()
await browser.close()

图片下载

知道了图片的地址,那么就很好下载了,这里使用的是 got,因为它比 axios 少了 60kb 大小,而且对于下载图片视频来说完全够用

Typscript
import { got } from 'got'

// images 是我们之前定义的 Set,这里转换为数组
Promise.all(
  [...images].map(async (url) => {
    const res = await got.get(url, {
      // 这里我们伪造一下请求头
      headers: {
        'User-Agent':
          'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36',
      },
      // 设置响应类型为 buffer
      responseType: 'buffer',
    })
    await fsp.writeFile(writePath, res.rawBody)
  }),
)

视频下载

视频下载关键在于推特会返回一个以 UserMedia 开头的 json 文件,json 文件里会有一个 video_info 字段,在其中可以找到视频的真实地址:

image.png

由于 json 文件是响应回来的,因此我们需要监听页面的响应事件,和之前监听请求时间类似:

Typscript
// 定义一个 Set,用于存储 videos 地址
const videos = new Set()
page.on('response', async (res) => {
  const url = res.url()
  // 如果地址里包含 UserMedia
  if (url.includes('UserMedia')) {
    // 将相应数据转换为字符串
    // 不转换为对象的原因是因为 json 字段嵌套的太深了
    // 我需要 xxx.yyy.zzz.ttt .... 嵌套几十层,这里我们选择正则匹配,具体可以看项目里的代码
    const requestSource = await res.text()
    // 处理函数
    resolveVideoInfo(requestSource, videos, user)
  }
})

resolveVideoInfo 函数地址 core/src.utils

下载视频

视频需要流来下载,这部分 got 做的十分简单:

Typscript
import { pipeline as streamPipeline } from 'node:stream/promises'
import fs from 'node:fs'
import { got } from 'got'

Promise.all(
  [...videos].map(async (url) => {
    await streamPipeline(
      got.stream(url, {
        ...USER_AGENT_HEADER,
      }),
      // outputDir 是输出文件
      fs.createWriteStream(outputDir),
    )
  }),
)