我是靠谱客的博主 苗条汉堡,这篇文章主要介绍bilibili弹幕转ass程序制作思路及过程,现在分享给大家,希望可以做个参考。

b站的弹幕,线下播放还是挺麻烦的,专用的弹幕播放器对其他格式的视频支持不好。我也试着弄个弹幕转字幕的小程序出来。

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

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

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

复制代码
1
2
3
4
5
<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里面了,所以我使用正则表达式来提取的。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
以上表第一条弹幕为例。很明显的,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是字体颜色。

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

正则表达式:

复制代码
1
<dsp="([d.]+),([145]),(d+),(d+),d+,d+,w+,d+">([^<>]+?)</d>


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

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

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

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

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

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

go源代码如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// 将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(`<dsp="([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}%sn", 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}%sn", 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}%sn", 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。欢迎各位评论,倒不如各位多多评论啊。

最后

以上就是苗条汉堡最近收集整理的关于bilibili弹幕转ass程序制作思路及过程的全部内容,更多相关bilibili弹幕转ass程序制作思路及过程内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(149)

评论列表共有 0 条评论

立即
投稿
返回
顶部