bilibili弹幕转ass

抓取xml文件的工作就不多说了,很简单的事,只要在播放页面看看源文件就能确定xml文件的地址进行抓取了。

本文主要是讲述xml内的弹幕转字幕的过程。

除去xml文件开头结尾的一些七七八八的东西,弹幕主体是这样的:

<d p="51.593,5,25,16711680,1408852480,0,7fa769b4,576008622">怒求 up 自己配音!</d>
<d p="10.286,1,25,16777215,1408852600,0,a3af4d0d,576011065">颜艺?</d>
<d p="12.65,1,25,16777215,1408852761,0,24570b5a,576014281">我的女神!</d>
<d p="19.033,1,25,16777215,1408852789,0,cb20d1c7,576014847">前!!!</d>
<d p="66.991,1,25,16777215,1408852886,0,a78e484d,576016806">已撸</d>

如果它把弹幕的各种属性分开表示,我就用encoding/xml包来解码,但是丫把弹幕的属性都放在p里面了,所以我使用正则表达式来提取的。

以上表第一条弹幕为例。很明显的,p属性开始的浮点数,与播放时一比对,就能知道,表示的是弹幕应该出现的播放时间。

随后的1和25先不管;

16777215,目测应该是颜色(因为该值表示为十六进制是FFFFFF);

1408852480,在弹幕中是递增的,感觉应该是个unix时间,用这个数(d),求:d/86400/365.2425+1970,结果约为2014.6。看来确实是unix时间。估计是创建弹幕的时间。

0,不知道,抓取了很多视频的弹幕,这个位置都是0,暂且不管它。

7fa769b4,估计是创建者的ID,因为同一xml文件会出现多次,而且看起来是十六进制数,恰好有些hash函数就是返回4字节整数。

576008622,也是递增的,不用猜也知道,这个肯定就是弹幕的ID了。

事后再核对一下,果然,1代表弹幕的类型(从右向左移动啊,出现在下方或者上方啊……),25是字体大小,16777125是字体颜色。

所以,我们就只要捕获每条弹幕的时间、类型、大小、颜色、文本就行了。

正则表达式:

<d\sp="([\d\.]+),([145]),(\d+),(\d+),\d+,\d+,\w+,\d+">([^<>]+?)</d>
捕获弹幕很简单,关键是排布弹幕为字幕的算法。

关于这个算法我就很坑爹的弄了个乱七八糟的算法,采用的是固定移动速度,最小重叠的排布原则。

对游动弹幕,会倾向于选择下面一行的位置,如果会重叠,则选择更下一行(最低行会循环到最上面一行),如果没有不重叠的行,会选择重叠文本最少的行。

对上现隐/下现隐的固定弹幕,会选择最接近上方/下方,且不重叠的行;如果没有不重叠的行,则选择重叠时间最短的行,居中放置字幕。

默认字体微软雅黑,默认大小25,默认白色黑边;默认占满整个屏幕,共计12行;默认屏幕大小640x360。

这么弄,主要是为了让ass字幕的效果更接近原始弹幕的效果。

高级弹幕真的超出我的能力范围了,全部忽略掉。

go源代码如下:

// 将bilibili的xml弹幕文件转换为ass字幕文件。
// xml文件中,弹幕的格式如下:
// <d p="32.066,1,25,16777215,1409046965,0,017d3f58,579516441">地板好评</d>
// p的属性为时间、弹幕类型、字体大小、字体颜色、创建时间、?、创建者ID、弹幕ID。
// p的属性中,后4项对ass字幕无用,舍弃。被<d>和</d>包围的是弹幕文本。
// 只处理右往左、上现隐、下现隐三种类型的普通弹幕。
package main

import (
	"fmt"
	"io"
	"io/ioutil"
	"math"
	"os"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

// ass文件的头部
const header = `[Script Info]
ScriptType: v4.00+
Collisions: Normal
playResX: 640
playResY: 360

[V4+ Styles]
Format: Name, Fontname, Fontsize, primaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default, Microsoft YaHei, 28, &H00FFFFFF, &H00FFFFFF, &H00000000, &H00000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, 1, 0, 2, 10, 10, 10, 0

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
`

// 正则匹配获取弹幕原始信息
var line = regexp.MustCompile(`<d\sp="([\d\.]+),([145]),(\d+),(\d+),\d+,\d+,\w+,\d+">([^<>]+?)</d>`)

// 用来保管弹幕的信息
type Danmu struct {
	text  string
	time  float64
	kind  byte
	size  int
	color int
}

// 使[]Danmu实现sort.Interface接口,以便排序
type Danmus []Danmu

func (d Danmus) Len() int {
	return len(d)
}
func (d Danmus) Less(i, j int) bool {
	return d[i].time < d[j].time
}
func (d Danmus) Swap(i, j int) {
	d[i], d[j] = d[j], d[i]
}

// 将正则匹配到的数据填写入Danmu类型里
func fill(d *Danmu, s [][]byte) {
	d.time, _ = strconv.ParseFloat(string(s[1]), 64)
	d.kind = s[2][0] - '0'
	d.size, _ = strconv.Atoi(string(s[3]))
	bgr, _ := strconv.Atoi(string(s[4]))
	d.color = ((bgr >> 16) & 255) | (bgr & (255 << 8)) | ((bgr & 255) << 16)
	d.text = string(s[5])
}

// 返回文本的长度,假设ascii字符都是0.5个字长,其余都是1个字长
func length(s string) float64 {
	l := 0.0
	for _, r := range s {
		if r < 127 {
			l += 0.5
		} else {
			l += 1
		}
	}
	return l
}

// 生成时间点的ass格式表示:`0:00:00.00`
func timespot(f float64) string {
	h, f := math.Modf(f / 3600)
	m, f := math.Modf(f * 60)
	return fmt.Sprintf("%d:%02d:%05.2f", int(h), int(m), f*60)
}

// 读取文件并获取其中的弹幕
func open(name string) ([]Danmu, error) {
	data, err := ioutil.ReadFile(name)
	if err != nil {
		return nil, err
	}
	dan := line.FindAllSubmatch(data, -1)
	ans := make([]Danmu, len(dan))
	for i := len(dan) - 1; i >= 0; i-- {
		fill(&ans[i], dan[i])
	}
	return ans, nil
}

// 将弹幕排布并写入w,采用的简单的固定移速、最小重叠排布算法
func save(w io.Writer, dans []Danmu) {
	p1 := make([]float64, 36)
	p2 := make([]float64, 36)
	p3 := make([]float64, 36)
	t := 0
	max := func(x []float64) float64 {
		i := x[0]
		for _, j := range x[1:] {
			if i < j {
				i = j
			}
		}
		return i
	}
	set := func(x []float64, f float64) {
		for i, _ := range x {
			x[i] = f
		}
	}
	find := func(p []float64, f float64, i, d int) int {
		i = (i/d + 1) * d % 36
		m, k := f+10000, 0
		for j := 0; j < 36; j += d {
			t := (i + j) % 36
			if n := max(p[t : t+d]); n <= f {
				k = t
				break
			} else if m > n {
				k = t
				m = n
			}
		}
		return k
	}
	for _, dan := range dans {
		s, l := "", length(dan.text)
		if l == 0 {
			continue
		}
		switch {
		case dan.size < 25:
			dan.size, l, s = 2, l*18, "\\fs18"
		case dan.size == 25:
			dan.size, l = 3, l*28
		case dan.size > 25:
			dan.size, l, s = 4, l*38, "\\fs38"
		}
		if dan.color != 0x00FFFFFF {
			s += fmt.Sprintf("\\c&H%06X", dan.color)
		}
		switch dan.kind {
		case 1: // 右往左
			t := find(p1, dan.time, t, dan.size)
			set(p1[t:t+dan.size], dan.time+8)
			h := (t+dan.size)*10 - 1
			s += fmt.Sprintf("\\move(%d,%d,%d,%d)", 640+int(l/2), h, -int(l/2), h)
			fmt.Fprintf(w, "Dialogue: 1,%s,%s,Default,,0000,0000,0000,,{%s}%s\n",
				timespot(dan.time+0),
				timespot(dan.time+8), s, dan.text)
		case 4: // 下现隐
			j := find(p2, dan.time, 35, dan.size)
			set(p2[j:j+dan.size], dan.time+4)
			s += fmt.Sprintf("\\pos(%d,%d)", 320, (36-j)*10-1)
			fmt.Fprintf(w, "Dialogue: 2,%s,%s,Default,,0000,0000,0000,,{%s}%s\n",
				timespot(dan.time+0),
				timespot(dan.time+4), s, dan.text)
		case 5: // 上现隐
			j := find(p3, dan.time, 35, dan.size)
			set(p3[j:j+dan.size], dan.time+4)
			s += fmt.Sprintf("\\pos(%d,%d)", 320, (j+dan.size)*10-1)
			fmt.Fprintf(w, "Dialogue: 3,%s,%s,Default,,0000,0000,0000,,{%s}%s\n",
				timespot(dan.time+0),
				timespot(dan.time+4), s, dan.text)
		}
	}
}

// 主函数,实现了命令行
func main() {
	if len(os.Args) <= 1 {
		os.Exit(0)
	}
	for _, name := range os.Args[1:] {
		dans, err := open(name)
		if err != nil {
			os.Exit(1)
		}
		if n := strings.LastIndex(name, "."); n != -1 {
			name = name[:n]
		}
		name += ".ass"
		file, err := os.Create(name)
		if err != nil {
			os.Exit(2)
		}
		file.WriteString(header)
		sort.Sort(Danmus(dans))
		save(file, dans)
		file.Close()
	}
}



2014.9.2 9:30am更新:对字体排布进行了修正。

2014.9.2 9:50am更新:算法修改为固定出现时间,最小重叠排布,最终版本。

over。欢迎各位评论,倒不如各位多多评论啊。

转载于:https://my.oschina.net/liudiwu/blog/308924

Danmaku2ASS 用来将来自 Niconico/Acfun/Bilibili 的评论ASS 格式,这样就可以在任意支持 ASS 字母的媒体播放器中使用弹幕。使用方法:usage: danmaku2ass.py [-h] [-f FORMAT] [-o OUTPUT] -s WIDTHxHEIGHT [-fn FONT]                       [-fs SIZE] [-a ALPHA] [-dm SECONDS] [-ds SECONDS]                       [-fl FILTER] [-p HEIGHT] [-r]                       FILE [FILE ...] positional arguments:   FILE                  Comment file to be processed optional arguments:   -h, --help            show this help message and exit   -f FORMAT, --format FORMAT                         Format of input file (autodetect|Bilibili|Tudou2|MioMi                         o|Acfun|Niconico|Tudou) [default: autodetect]   -o OUTPUT, --output OUTPUT                         Output file   -s WIDTHxHEIGHT, --size WIDTHxHEIGHT                         Stage size in pixels   -fn FONT, --font FONT                         Specify font face [default: sans-serif]   -fs SIZE, --fontsize SIZE                         Default font size [default: 25]   -a ALPHA, --alpha ALPHA                         Text opacity   -dm SECONDS, --duration-marquee SECONDS                         Duration of scrolling comment display [default: 5]   -ds SECONDS, --duration-still SECONDS                         Duration of still comment display [default: 5]   -fl FILTER, --filter FILTER                         Regular expression to filter comments   -p HEIGHT, --protect HEIGHT                         Reserve blank on the bottom of the stage   -r, --reduce          Reduce the amount of comments if stage is full 标签:Danmaku2ASS
AcFun.cn是国内一家仿niconico的视频网站,以其弹幕闻名。但网站并没有提供下载视频及弹幕的方法,通过浏览器以及其它的嗅探器可以得到下载地址,但是十分繁琐,且下载下来在本地很难播放,虽然有一些网友做了基于flash的本地播放器,但其效率和方便性皆不尽人意。 你还在为不能下载保存AcFun网站上的视频和弹幕苦恼吗?你还在为不能本地播放弹幕伤心吗?你还在为神字幕被破坏不能再次欣赏到而后悔吗?赶快试试AcFun视频字幕下载换器,它是一款由网友自发编写的绿色无公害的免费软件。 功能特点: 根据提供的acfun视频网页地址,自动分析视频地址及字幕地址,并将其下载到指定位置 分析acfun弹幕格式,并将其换为视觉效果基本相同的ssa字幕文件,可以由vobsub播放器插件加载 可以仅换本地以前保存下来的acfun格式的字弹幕文件 所有处理过程清晰明了,有进度条显示 可以仅下载字幕,不下载视频 可以仅换某些模式类别的弹幕,忽略掉没用的评论 具有下载列表,在下载过程中可以随时添加新任务 可以一次换本地保存的多个文件 v2.004 - 20100125 增加修复flv本地视频功能,可以消除由于加载h263头导致不能播放的问题,也可以去除加入在视频前面的前置黑屏 增加下载时自动修复视频功能 增加修复选项设置,与下载时是否自动修复选项设置  修改了程序窗口标题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值