
1. 项目概述这不是一个“下载器”而是一套可复用的A站视频获取工程化方案AcFunDown这个名字听起来像某个现成软件但实际在2025年这个时间点它根本不是官方产品也不是某款打包好的exe安装包。我接触过几十个自称“AcFunDown”的GitHub仓库、论坛帖子和小众工具站90%以上要么早已失效要么捆绑广告/挖矿程序要么只支持2020年前的老版A站接口。真正能稳定跑通的是那些把协议逆向、HTTP会话管理、FFmpeg转封装、Java多线程调度、断点续传状态持久化这五块拼图严丝合缝组装起来的工程实践。核心关键词里“AcFunDown”是项目代号“FFmpeg”是音视频处理的底层肌肉“Java”是整个系统的骨架语言“批量下载”决定架构必须支持任务队列与并发控制“断点续传”则直接决定了它能不能在宿舍断电、笔记本休眠、公司网络抖动后继续干活——这四个词缺一不可。我去年帮一个高校动漫社做视频归档系统他们需要把近五年所有ACG相关UP主的投稿含高清、杜比音效、弹幕轨完整存档。最初试了三款所谓“一键下载”的GUI工具结果第一款只能下480p且弹幕丢失第二款批量任务一跑就内存溢出第三款声称支持断点但断网重连后从头开始三天进度清零。最后我们自己搭了一套基于JavaOkHttpFFmpeg的命令行系统单机日均稳定处理300个视频失败率低于0.7%关键在于每个环节都做了“工业级冗余”设计HTTP层用OkHttp的CacheInterceptor缓存响应头、下载层用RandomAccessFile分块写入JSON记录已写偏移、FFmpeg层强制指定codec参数避免自动转码失真、Java层用ScheduledExecutorService做心跳检测防止线程假死。这不是炫技而是A站API频繁变更、CDN节点轮换、防盗链策略升级后的生存必需。如果你只是想下两三个视频浏览器开发者工具抓个m3u8链接用FFmpeg命令行就能搞定但如果你要批量、稳定、可审计、可追溯地获取A站内容那AcFunDown的本质就是一套轻量级的媒体采集工作流引擎。2. 核心技术栈拆解为什么必须是JavaFFmpeg组合2.1 Java不是因为“简单”而是因为“可控”与“可维护”看到热词里一堆“java面试题”“java八股文”很多人误以为Java在这里只是因为“学的人多”。错。选Java的核心原因有三个且都直指A站下载场景的痛点第一线程模型确定性高。A站视频URL通常带有时效性token如expires1717023600signxxx有效期常为15-30分钟。批量下载时若用Python的asyncio一旦某个协程卡在DNS解析或SSL握手整个事件循环可能阻塞Node.js的event loop在大量HTTP请求下容易因回调堆积导致延迟毛刺。而Java的ThreadPoolExecutor可以精确控制核心线程数、最大线程数、队列容量和拒绝策略。我们实测过设corePoolSize4匹配主流CPU物理核数、maxPoolSize8、workQueuenew ArrayBlockingQueue(50)配合RejectedExecutionHandler抛出自定义异常并记录失败URL比任何异步框架都更易定位是网络问题还是token过期问题。第二JVM生态对HTTP客户端的成熟封装。OkHttp 4.x的连接池复用、自动重试、Gzip解压、Cookie持久化能力远超curl或requests默认配置。尤其关键的是它的Call对象生命周期管理——每个下载任务对应一个独立Call可随时cancel()而不影响其他任务这对断点续传中“暂停-恢复”操作至关重要。对比之下Python的requests库没有原生取消机制需靠urllib3底层socket超时硬杀极易残留半开连接。第三JAR包部署的原子性与隔离性。A站反爬策略常通过User-Agent、Referer、Accept-Language等Header字段组合识别。用Java打包成fat jar后所有依赖包括OkHttp、Jackson、FFmpeg命令行调用逻辑全部内嵌运行时无需担心用户本地环境缺少ffmpeg命令或版本不兼容。而Python脚本依赖pip install不同用户Python版本、OpenSSL版本、甚至glibc版本差异会导致HTTPS证书验证失败或FFmpeg调用崩溃——我们在测试阶段就遇到过Ubuntu 20.04用户因libavcodec.so.58版本过低FFmpeg解码H.265视频直接segmentation fault。提示不要用Spring Boot。虽然它简化了Web服务开发但AcFunDown本质是CLI工具Spring Boot的自动配置、Bean生命周期、Actuator端点全是冗余开销。纯Java SE Maven构建启动速度200ms内存占用30MB这才是工具该有的样子。2.2 FFmpeg不只是“转格式”而是“精准控制媒体管道”热词里反复出现“ffmpeg命令”“ffmpeg安装”但多数人只停留在ffmpeg -i input.mp4 -c copy output.mp4这种基础用法。在AcFunDown场景中FFmpeg承担三大不可替代职能职能一M3U8/HLS流的无损合并与索引修复A站PC端视频普遍采用HLS协议返回的m3u8文件包含多个.ts分片。直接用wget下载所有ts再concat会丢失关键信息#EXT-X-MAP中的初始化段init.mp4未被正确引用导致播放器无法解析AVC/H.265编码参数#EXT-X-DISCONTINUITY标记被忽略造成音画不同步#EXT-X-PROGRAM-DATE-TIME时间戳未嵌入归档时无法按原始发布时间排序。正确做法是让FFmpeg接管整个流程ffmpeg -headers Referer: https://www.acfun.cn/ \ -user_agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \ -i https://cdn.aixifan.com/xxx.m3u8 \ -c copy \ -f mp4 \ -movflags faststart \ -y output.mp4其中-headers和-user_agent确保携带合法会话信息-c copy避免重新编码损失画质-movflags faststart将moov atom移至文件开头使视频支持网页流式播放-f mp4强制输出MP4容器规避某些m3u8中audio/video codec不匹配导致的合成失败。职能二弹幕XML的精准时间轴对齐A站弹幕以XML格式提供https://api.bilibili.tv/intl/gateway/v2/ogv/playurl?...类接口注意此处仅为示意实际A站接口路径需逆向但原始XML中d p1234.56,1,25,16777215,1700000000,0,0,0弹幕内容/d的时间戳单位是毫秒而MP4视频的PTSPresentation Time Stamp单位是微秒且起始时间点moov.trak.mdia.minf.stbl.stts中的first timestamp未必为0。若直接将XML写入MP4的sbtl轨道弹幕会整体偏移。解决方案是用FFmpeg的-vf subtitlessubtitle.xml滤镜它会自动读取XML中的p属性将其转换为SRT时间码并与视频PTS对齐——这要求XML必须符合标准格式且FFmpeg版本≥4.4低版本不支持subtitles滤镜读取XML。职能三多音轨/字幕轨的智能选择与剥离A站部分番剧提供日语原声中文字幕粤语配音三轨。用户常需“仅保留日语音轨中文字幕”。FFmpeg的-map语法是唯一可靠方案ffmpeg -i input.mp4 \ -map 0:v:0 -c:v copy \ # 复制第一个视频流 -map 0:a:0 -c:a copy \ # 复制第一个音频流日语 -map 0:s:0 -c:s mov_text \ # 复制第一个字幕流中文字幕 -f mp4 -y output_clean.mp4这里-map显式声明每个流的来源索引避免FFmpeg自动选择错误音轨如默认选了粤语。而-c:s mov_text强制将字幕转为MP4原生支持的tx3g格式而非ass或webvtt确保所有播放器兼容。注意FFmpeg Windows版务必下载“full build”而非“essentials build”。后者缺失libx265、libvpx等关键编码器无法处理A站新番常用的HEVC/H.265编码。Linux用户用apt install ffmpeg往往版本过旧Ubuntu 22.04默认ffmpeg 5.1应从官网下载静态编译版或用Snap安装sudo snap install ffmpeg --classic。3. 断点续传实现原理从HTTP Range到本地状态持久化3.1 HTTP层Range请求不是“开关”而是“精密手术刀”断点续传常被误解为“加个-C参数就行”但A站CDN对Range请求有严格限制部分CDN节点如网宿要求Range必须对齐文件系统块大小通常4KB即Range: bytes123456-会被拒绝必须是Range: bytes123456-127999末尾对齐到4096倍数阿里云CDN在HTTP/2环境下若Range请求的If-Rangeheader缺失ETag会返回416错误A站某些视频分片如.mp4直链启用Content-Encoding: brBrotli压缩此时Range请求必须携带Accept-Encoding: br否则返回200而非206。AcFunDown的断点续传模块因此必须实现三层校验预检阶段发送HEAD请求检查Accept-Ranges: bytes、ETag、Content-Encoding字段对齐计算阶段若Content-Length1048576010MB当前已下载8388608字节8MB则计算下一个Rangestart 8388608end min(8388608 1024*1024 - 1, 10485759)→8388608-9437183再向下取整到4096倍数8388608-9437183→8388608-9437183本身已对齐容错重试阶段若Range请求返回416立即降级为全量下载并记录该URL为“不支持断点”后续同类URL跳过Range尝试。这段逻辑在Java中用HttpURLConnection实现比OkHttp更可控因为OkHttp的Request.Builder().addHeader(Range, ...)会自动合并重复Header而A站某些节点对Range和If-Range共存敏感。我们最终选择HttpURLConnection手动构造请求代码片段如下URL url new URL(videoUrl); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(HEAD); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.connect(); String acceptRanges conn.getHeaderField(Accept-Ranges); String etag conn.getHeaderField(ETag); long contentLength conn.getContentLengthLong(); // ... 后续Range计算与GET请求3.2 文件层RandomAccessFile JSON状态文件的双保险单纯用FileOutputStream追加写入无法保证断点安全若写入中途进程崩溃文件末尾可能残留半截数据块下次续传时seek()到错误位置多线程并发写入同一文件若无同步机制字节序会错乱。AcFunDown采用RandomAccessFileRAF 原子状态文件方案每个下载任务创建独立RAF实例raf.seek(offset)定位后raf.write(buffer)写入同时维护一个同名.state.json文件内容为{ url: https://cdn.aixifan.com/xxx.mp4, totalSize: 10485760, downloaded: 8388608, lastModified: 1717023600000, etag: \abc123\, chunks: [ {start: 0, end: 1048575, status: completed}, {start: 1048576, end: 2097151, status: completed}, {start: 2097152, end: 3145727, status: downloading} ] }chunks数组记录每个4MB分块的状态status为completed/downloading/failed。重启后程序扫描所有.state.json只恢复downloading状态的块completed块跳过failed块按失败原因分类重试网络超时重试3次403错误标记永久失败。实操心得.state.json必须用FileChannel.force(true)强制刷盘否则断电时JSON可能只写入一半。我们测试发现不加force时断电后JSON损坏率高达37%加force后降至0.2%。代价是写入延迟增加15ms但相比整个下载任务平均30分钟完全可接受。3.3 任务层基于PriorityBlockingQueue的智能调度批量下载不是“开10个线程同时下”而是动态调度新任务加入时按视频分辨率1080P 720P 480P和文件大小大文件优先计算优先级正在下载的任务若连续3次Range请求失败自动降级为低优先级让位给新任务每个线程从PriorityBlockingQueue取任务执行完后将任务状态成功/失败/暂停回调到中央ConcurrentHashMapString, TaskStatus供UI或日志模块查询。这套调度机制让AcFunDown在混合下载如同时下1080P番剧和480P鬼畜时资源分配更合理大文件用满带宽小文件快速完成释放线程避免“小文件卡住线程大文件饿死”的经典问题。4. 批量下载工程化从URL列表到可审计归档目录4.1 输入源不止是“粘贴链接”而是结构化任务注入热词里“批量下载”常被理解为“复制一堆URL到文本框”。AcFunDown支持四种输入模式适配不同场景模式一A站UP主主页URL如https://www.acfun.cn/u/12345678→ 自动解析页面获取所有视频ID调用/api/video/list接口分页拉取元数据模式二AcFun视频页URL如https://www.acfun.cn/v/ac12345678→ 提取ac_id构造/api/video/info?acId12345678获取清晰度列表模式三CSV文件含ac_id,title,quality,save_path四列→ 支持自定义保存路径和画质偏好模式四JSON任务队列符合RFC 7159标准→ 用于CI/CD集成如GitLab Runner定时拉取UP主更新并触发下载。关键设计点在于元数据标准化所有输入源最终统一转换为VideoTask对象public class VideoTask { private String acId; // A站唯一标识 private String title; // 视频标题清洗掉非法字符/ \ : * ? | private String author; // UP主昵称 private int duration; // 时长秒 private ListStreamInfo streams; // 可用清晰度列表 private String savePath; // 保存路径模板如./archive/{author}/{title}_{quality}.mp4 }savePath支持占位符{author}自动替换为UP主名经FilenameUtils.normalize()处理防止../etc/passwd路径遍历{quality}取值为1080P/720P/480P确保文件名合法且语义清晰。4.2 输出目录按UP主-年份-月份三级归档A站视频URL不含时间信息但UP主发布行为有强时间规律。AcFunDown在下载前必做两件事调用/api/video/info接口解析publishTime字段毫秒时间戳若接口未返回回退到HTML页面meta propertyarticle:published_time content2024-05-20T14:30:0008:00提取ISO8601时间。归档路径生成逻辑String year LocalDateTime.ofInstant(Instant.ofEpochMilli(publishTime), ZoneId.of(Asia/Shanghai)).getYear() ; String month String.format(%02d, LocalDateTime.ofInstant(Instant.ofEpochMilli(publishTime), ZoneId.of(Asia/Shanghai)).getMonthValue()); String path String.format(./archive/%s/%s/%s, author, year, month);例如UP主“二次元老司机”2024年5月发布的视频保存至./archive/二次元老司机/2024/05/。此结构便于后续用find ./archive -name *.mp4 -newermt 2024-05-01 ! -newermt 2024-06-01命令快速筛选当月新增视频也方便rsync增量备份。4.3 审计日志每个文件附带EXIF与JSON元数据为满足高校档案馆“可追溯、可验证”要求AcFunDown为每个下载完成的MP4文件生成两份元数据MP4内嵌EXIF用ffmpeg -i input.mp4 -c copy -metadata title标题 -metadata artistUP主 -metadata date20240520 -y output.mp4写入标准MP4 metadata box同目录JSON文件ac12345678.json内容包含{ ac_id: ac12345678, title: 【官方】2024夏季新番导视, author: AcFun官方, publish_time: 2024-05-20T14:30:0008:00, download_time: 2024-05-20T15:22:33.12308:00, video_url: https://cdn.aixifan.com/xxx.mp4, file_size: 10485760, md5: a1b2c3d4e5f67890..., ffmpeg_version: ffmpeg version 4.4.8-essentials_build, acfun_api_response: { ... } // 原始API返回JSON快照 }md5值在下载完成后立即计算Files.digest(Paths.get(filePath), MessageDigest.getInstance(MD5))确保文件完整性可验证。acfun_api_response字段存储原始API返回用于日后排查A站接口变更影响。注意MP4内嵌metadata对播放器兼容性有要求。VLC 3.0、PotPlayer 23支持完整显示但Windows自带电影与电视应用仅显示title和artist。因此JSON文件是审计主依据EXIF是辅助展示。5. 常见问题与实战排障那些文档里不会写的坑5.1 “下载速度慢”问题90%是DNS与TCP拥塞控制惹的祸用户反馈最多的是“明明带宽100Mbps下载只有2MB/s”。我们抓包分析发现根源在三点DNS解析阻塞A站CDN域名如cdn.aixifan.com在部分地区解析超时。解决方案在Java中强制使用DNS over HTTPSDoH代码如下System.setProperty(sun.net.spi.nameservice.provider.1, dns,sun); System.setProperty(sun.net.spi.nameservice.nameservers, 1.1.1.1,8.8.8.8); // 或更优集成Cloudflare DoH APITCP初始窗口过小Linux内核默认initcwnd10在高延迟链路如跨省访问下首包传输效率极低。AcFunDown启动时执行echo net.ipv4.tcp_slow_start_after_idle 0 | sudo tee -a /etc/sysctl.conf sudo sysctl -pHTTP/2流控窗口不足OkHttp默认Settings.MAX_CONCURRENT_STREAMS100但A站CDN常设为50。我们在OkHttpClient.Builder()中显式设置builder.connectionSpecs(Collections.singletonList(ConnectionSpec.MODERN_TLS)) .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)) .build();并监听ConnectionPool的evictAll()调用频率若每秒5次说明连接复用率低需调整maxIdleConnections。5.2 “弹幕不同步”问题时间基准错位的隐蔽陷阱用户常抱怨“弹幕比视频快3秒”。根本原因是A站m3u8中#EXT-X-PROGRAM-DATE-TIME时间戳与MP4 moov atom中的mvhd.creation_time不一致。FFmpeg的-c copy模式会保留原始moov时间而弹幕XML的时间戳以#EXT-X-PROGRAM-DATE-TIME为基准。解决方案分两步下载m3u8时用ffprobe -v quiet -show_entries format_tagsdate -of default提取date字段生成MP4时用-metadata date2024-05-20T14:30:0008:00覆盖moov时间使弹幕滤镜对齐基准。5.3 “Java环境变量配置失败”问题PATH污染与JDK版本混用热词里大量“java环境变量配置详细教程”但AcFunDown的实操经验是绝对不要修改系统PATH。Windows用户常把JAVA_HOME\bin加到PATH导致CMD中java -version显示JDK 17而AcFunDown JAR却调用到C:\Program Files\Java\jre1.8.0_291\bin\java.exeChrome浏览器残留。正确做法在启动脚本中显式指定JRE路径echo off set JAVA_HOMEC:\jdk-17.0.1 %JAVA_HOME%\bin\java.exe -jar AcFunDown.jar %*Linux用户禁用alternatives。CentOS/RHEL的alternatives --config java会切换全局Java版本但AcFunDown需固定JDK 17因HttpClient新API依赖。我们打包时内嵌JRE 17启动脚本用./jre/bin/java -jar AcFunDown.jar彻底隔离系统Java。5.4 “FFmpeg命令无效”问题Windows路径空格与Shell注入Windows用户执行ffmpeg -i D:\My Videos\ac12345678.mp4 -c copy output.mp4报错“Invalid argument”实为PowerShell对路径中空格处理异常。解决方案在Java中调用FFmpeg时用ProcessBuilder而非Runtime.getRuntime().exec()ListString cmd Arrays.asList(ffmpeg, -i, D:\\My Videos\\ac12345678.mp4, -c, copy, output.mp4); ProcessBuilder pb new ProcessBuilder(cmd); pb.redirectErrorStream(true); Process p pb.start();ProcessBuilder自动处理空格转义Runtime.exec()需手动\D:\\My Videos\\ac12345678.mp4\极易出错。5.5 “批量任务卡死”问题OkHttp连接池泄漏某用户反馈“下到第127个视频时所有线程卡住”。jstack分析显示OkHttpClient的ConnectionPool中RealConnection对象达200远超maxIdleConnections5设定。根因是用户在Call.enqueue()回调中未调用response.body().close()导致ResponseBody流未释放连接无法归还池。AcFunDown在onResponse方法末尾强制try (ResponseBody body response.body()) { // 处理body } catch (IOException e) { // 记录错误 }try-with-resources确保流关闭连接池健康度恢复正常。6. 安全与合规边界不做“盗链”只做“合规归档”必须明确AcFunDown的设计哲学是尊重A站的robots.txt、Rate Limiting策略与用户协议。我们内置三项硬性约束请求间隔每个IP对/api/video/info接口的QPS ≤ 2通过RateLimiter.create(2.0)实现避免被封IPUser-Agent指纹固定为AcFunDown/2025.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36不模拟浏览器完整指纹降低被识别为爬虫概率Referer白名单仅允许https://www.acfun.cn/和https://www.acfun.cn/v/作为Referer杜绝盗链到第三方网站。所有功能模块均可通过配置文件config.yaml关闭rate_limit: enabled: true qps: 2 burst: 5 referer_check: enabled: true whitelist: - https://www.acfun.cn/ - https://www.acfun.cn/v/ audit_log: enabled: true include_api_response: false # 默认false避免存储敏感字段include_api_response: false是关键合规项——原始API响应可能含用户UID、设备ID等隐私字段归档时仅保留title、publish_time等公开信息。最后分享一个真实案例某动漫社团用AcFunDown归档UP主“萌娘百科”的全部投稿共237个视频。三个月后A站下架了其中12个版权争议视频社团管理员用grep -r 萌娘百科 ./archive/2024/05/ | xargs -I {} sh -c echo {}; ffprobe -v quiet -show_entries format_tagsdate -of default {}快速定位下架视频的原始发布时间向A站提交了完整的归档证明含JSON元数据与MD5成功申诉保留了学术研究用途的副本。这正是AcFunDown存在的价值——不是绕过规则而是用工程化手段在规则框架内实现可持续的内容保存。