Greasy Fork is available in English.

下载 B 站弹幕 CSV

下载 B 站弹幕为 CSV 方便分析查找

'use scrict';

// ==UserScript==
// @name         下载 B 站弹幕 CSV
// @namespace    http://tampermonkey.net/
// @version      1.1.3
// @description  下载 B 站弹幕为 CSV 方便分析查找
// @author       malei0311
// @match        https://www.bilibili.com/bangumi/play/*
// @grant        none
// ==/UserScript==

/**
 * 在支持 es6 的浏览器中使用
 */

(function () {
  function formatDate (date, format) {
    const o = {
      'M+': date.getMonth() + 1, // 月份
      'd+': date.getDate(), // 日
      'H+': date.getHours(), // 小时
      'm+': date.getMinutes(), // 分
      's+': date.getSeconds(), // 秒
      'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
      S: date.getMilliseconds() // 毫秒
    }
    let fmt = format || 'yyyy-MM-dd HH:mm:ss'
    if (/(y+)/.test(fmt)) { 
      fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
    }
    for (let k in o) {
      if (new RegExp('(' + k + ')').test(fmt)) {
        fmt = fmt.replace(
          RegExp.$1,
          RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
        )
      }
    }
    return fmt
  }

  function parseXML (response = '') {
    try {
      response = response.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/g, '')
    } catch (e) {
      console.error('parse xml replace error', e)
      return
    }
    const xmlDoc = new window.DOMParser().parseFromString(response, 'text/xml')
    const danmuList = []
    const $d = xmlDoc.getElementsByTagName('d')
    for (let i = 0; i < $d.length; i++) {
      const $item = $d[i]
      const [stime, mode, size, color, date, sc, uid, dmid] = $item.getAttribute('p').split(',')
      const text = $item.textContent || $item.text || ''
      danmuList.push({
        stime, // 弹幕播放时间
        mode, // 弹幕模式 1..3 滚动弹幕 4底端弹幕 5顶端弹幕 6逆向弹幕 7精准定位 8高级弹幕 
        size, // 字号 12非常小,16特小,18小,25中,36大,45很大,64特别大
        color, // 弹幕颜色 10 进制
        date, // unix 时间戳
        sc, // 弹幕池 0普通池 1字幕池 2特殊池【目前特殊池为高级弹幕专用】
        uid, // 发送者 id,加密算法 hash crc32b
        dmid, // 弹幕在弹幕数据库中 row id
        text // 弹幕内容
      })
    }
    return danmuList
  }

  function loadXML (url, cb) {
    const xhr = new XMLHttpRequest()
    xhr.timeout = 10000
    xhr.addEventListener('load', () => {
      const ret = parseXML(xhr.response)
      if (ret) {
        cb (ret)
      }
    })
    xhr.addEventListener('error', () => {
      console.error('load xml error', xhr.status, xhr.statusText)
    })
    xhr.addEventListener('abort', () => {
      console.error('load xml abort', xhr.status, xhr.statusText)
    })
    xhr.addEventListener('timeout', () => {
      console.error('load xml timeout', xhr.status, xhr.statusText)
    })
    xhr.open('GET', url)
    xhr.withCredentials = true
    xhr.send()
  }

  function download (content, fileName, mimeType = 'text/csv;encoding:utf-8') {
    const a = document.createElement('a')
    content = `\uFEFF${content}`

    if (navigator.msSaveBlob) { // IE10
      navigator.msSaveBlob(new Blob([content], {
        type: mimeType
      }), fileName)
    } else if (URL && 'download' in a) {
      a.href = URL.createObjectURL(new Blob([content], {
        type: mimeType
      }))
      a.setAttribute('download', fileName)
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a)
    } else {
      location.href = 'data:application/octet-stream,' + encodeURIComponent(content)
    }
  }

  function load (url, fileName) {
    loadXML(url, (danmuList) => {
      let writeHead = false
      let content = ''
      danmuList.forEach((item) => {
        if (!writeHead) {
          writeHead = true
          content += Object.keys(item).join(',') + '\n'
        }
        const str = Object.keys(item).reduce((ret, key) => {
          if (key === 'date') {
            ret.push(`"${formatDate(new Date(item[key] * 1000))}"`)
          } else if (key === 'text') {
            ret.push(`"${item[key].replace(/"/g, '""')}"`)
          } else {
            ret.push(`"${item[key]}"`)
          }
          return ret
        }, []).join(',')
        content += str + '\n'
      })
      download(content, fileName)
    })
  }

  function inject () {
    const {
      h1Title = window.$('h1[title]').text() || document.title,
      epInfo = {}
    } = window.__INITIAL_STATE__

    const oid = epInfo.cid
    if (!oid) {
      return
    }

    let injected = false
    let timer
    (function tryInject () {
      const $btn = window.$('.up-load')
      if (!injected && $btn.length) {
        injected = true
        clearTimeout(timer)
        const $clone = $btn.clone()
        $clone.html('<a class="u-link" style="cursor: pointer;">下载弹幕</a>').on('click', () => {
          load(`https://api.bilibili.com/x/v1/dm/list.so?oid=${oid}`, `弹幕----${h1Title}.csv`)
        })
        $btn.before($clone)
      }

      timer = setTimeout(tryInject, 3000)
    })()
  }

  inject()
})()