豆瓣搜索 `__DATA__` 对象破解

Rhilip 2021-09-12 AM 8402℃ 4条

这些年我一直跟着维护Pt-Gen以及PTPP、豆瓣搜索大师等公开项目。然而19年起,豆瓣逐渐关闭公开API (豆瓣疑下线所有公开 API),从现有豆瓣APP的 frodo 接口请求相关数据,所需要构造的参数过于麻烦。

所以在这段时间,我们一直使用 https://movie.douban.com/j/subject_suggest 接口来实现搜索功能作为过渡。这个接口构造简单,而且返回的数据直接为JSON格式,但同样存在返回的数据较为简单的问题,例如:

[
  {
    "episode": "",
    "img": "https://img2.doubanio.com/view/photo/s_ratio_poster/public/p480747492.webp",
    "title": "肖申克的救赎",
    "url": "https://movie.douban.com/subject/1292052/?suggest=%E8%82%96%E7%94%B3%E5%85%8B",
    "type": "movie",
    "year": "1994",
    "sub_title": "The Shawshank Redemption",
    "id": "1292052"
  },
  {
    "episode": "",
    "img": "https://img1.doubanio.com/f/movie/b6dc761f5e4cf04032faa969826986efbecd54bb/pics/movie/movie_default_small.png",
    "title": "肖申克的救赎:主演访谈",
    "url": "https://movie.douban.com/subject/35278770/?suggest=%E8%82%96%E7%94%B3%E5%85%8B",
    "type": "movie",
    "year": "2004",
    "sub_title": "The Shawshank Redemption: Cast Interviews",
    "id": "35278770"
  }
]

只返回了img, title, url, type, id(虽然基本够用....)。这主要是因为该接口只是用来搜索建议的,而真正搜索界面的结果隐藏在了subject_search页面的 __DATA__ 对象中。所以我们需要用另外一种方法来破解该对象。

我最早找到的文章来自掘金的这篇 豆瓣读书搜索页的window.__DATA__的解密 ,然而作者的思路是直接扒取豆瓣自带的JS文件(这当然没有问题)。然而作者讲的挺玄乎的: 总之很复杂,这个只能意会不能言传,篇幅有限,也不可能全部一个一个扣出来并和你说怎么改。

正当我以为没有办法,只能复制这位作者成果的时候(毕竟顶层函数有了,而且我也是在 JavaScript 环境中使用,不需要在Python中调用),我检索到了另外一个项目 dli98/Spider,文中作者详细介绍了 window.__data__ 参数破解过程是使用 base64 + xxHash + rc4 + bplist 一整套的思路,并指出 bplist 处理流程有暗改的情况。

但这位作者的结果其实并不对,少了最后一步的映射调整 (见 怎么获取豆瓣评分和投票数的顺序 · Issue #2 · dli98/Spider),所以得到的结果并不对。

所以就干脆直接写个 npm 库算了,之后工程中也能使用。于是你现在可以使用 npm i douban-search-crack来进行安装,其实例代码如下:

import axios from 'axios';
import decryptDoubanData, { extractDataFromPage } from 'douban-search-crack';

const { data: doubanSearchPage } = await axios.get('https://search.douban.com/movie/subject_search', {
  params: {
    search_text: '肖申克的救赎',
    cat: 1002
  },
  responseType: 'text'
})

const encryptDoubanData = extractDataFromPage(doubanSearchPage)
console.log(decryptDoubanData(encryptDoubanData))

函数主流程如下:

export default function decryptDoubanData(dataRawString: string): searchData {
    // 将网页字符串解析为base64
    const dataRaw = Buffer.from(dataRawString, 'base64')

    // 从base64中提取secKey,并整理生成需要解密的数据
    const i = 16
    const s = Math.max(Math.floor((dataRaw.length - 2 * 16) / 3), 0)
    const u = dataRaw.slice(s, s + i)

    const secKey = XXH.h64(u, 41405).toString(16)
    const encryptData = Buffer.concat([dataRaw.slice(0, s), dataRaw.slice(s + i)])

    // rc4解密
    const cipher = new RC4(secKey);
    const decryptData = cipher.update(encryptData);

    // BpList解密
    const bpList = parseBPlist(decryptData)

    // 对BpList的结果进行修正并返回
    return fixture.parse(bpList)
}

其中 xxhash 部分直接用库;rc4 因为过于简单,所以方法也直接内置;bplist解密因为豆瓣的暗改,所以不能直接用库,所以也是将库文件修改后内置的(原来想学着用monkeypatch的,然而patch发现还不如直接内置)。

此外,bpList部分,对比库原实现,除调换了objType= 4,5,6的解析方式,还删除了 对库 bigInt 的依赖,以及对header的检查。

而 fixture 模块,主要因为 BpList 返回的值主要为一个list,里面的Object字段基本如下:

{k: [ 40, 27, 35, 50, 52, 44 ] }
{k: [ '', [] ] , z: [ 44, 55 ] }
{k: [ '', [] , {j: 55} ], z: [ 44, 66, 88 ]}

所以我们需要进行修正,其中各数字均表示其在原list中的位置。通过对比 sergiojune 扒出来的代码,形成了矫正的步骤,具体可见 utils/fixture.ts 文件。

那么,自此,你就可以使用 douban-search-crack来在node以及browser环境中来破解豆瓣搜索的DATA数据。与直接扒网页js相比,如果你在node环境中,那么总文件大小 仅 16.0 KB (16,452 字节)。而如果你要在Browser环境中使用,那么也可以直接使用打包好的 dist/bundle.js 文件,其大小为 47.0 KB (48,147 字节),与直接扒拉js的实现大小基本相同。

标签: none

非特殊说明,本博所有文章均为博主原创。

评论啦~



已有 4 条评论


  1. 小李子
    小李子

    大神微信群拉我个 我微信 aboutsiyu88

    回复 2021-09-16 19:48
    1. Rhilip
      Rhilip 博主

      不好意思,我从来不在微信上加陌生人

      回复 2021-09-16 21:59
  2. 无得
    无得

    大神,请问怎么自己添加一些资源网站到豆瓣搜索大师呢

    回复 2021-11-23 18:54
    1. Rhilip
      Rhilip 博主

      不属于本文讨论范围,请自己查看“豆瓣搜索大师”脚本注释

      回复 2021-11-24 10:25