在 ⬆️上节,我们知道了, 仅仅只是知道-哪个网站, 然后把用户输入-url-
给到 对应的extractors
然后继续往下看 是 抖音部分
-
Download >> 这
-
urlSave >> 三
-
request-Request >> 个 其实并不独属于douyin, 是通用下载
annie/extractors/douyin.go
package extractors
import (
"encoding/json"
"github.com/iawia002/annie/downloader"
"github.com/iawia002/annie/request"
"github.com/iawia002/annie/utils"
)
// json 数据
type douyinVideoURLData struct {
URLList []string `json:"url_list"`
}
type douyinVideoData struct {
PlayAddr douyinVideoURLData `json:"play_addr"`
RealPlayAddr string `json:"real_play_addr"`
}
type douyinData struct {
Video douyinVideoData `json:"video"`
Desc string `json:"desc"`
}
// 对应下来-就是 这样的数据结构
// {
// desc:"",
// video:{
// play_addr:{
// url_list:"",
// }
// real_play_addr:"",
// }
// }
// Douyin download function
func Douyin(url string) downloader.VideoData {
html := request.Get(url) // 从url-获取完整的 <html>..html内容..</html>
vData := utils.MatchOneOf(html, `var data = \[(.*?)\];`)[1] // 从 html 找出 匹配
// 所谓的 命令行选项
// annie url
// 就能下载所想要的视频或其他,都是对网页内容的解析 获取真实的下载地址
// 从而进行下载
var dataDict douyinData
json.Unmarshal([]byte(vData), &dataDict) // 解析从 html:string -> json 获得 的匹配项
size := request.Size(dataDict.Video.RealPlayAddr, url)
// 从 要下载的 网址 head.Content-Length 知道下载的 文件大小
urlData := downloader.URLData{
URL: dataDict.Video.RealPlayAddr,
Size: size,
Ext: "mp4",
} // 下载器 定义的 数据块
data := downloader.VideoData{
Site: "抖音 douyin.com",
Title: utils.FileName(dataDict.Desc),
Type: "video",
URLs: []downloader.URLData{urlData}, // 有时候 是 一串视频流
Size: size,
} // 下载器 定义的 视频数据
data.Download(url) // 开始下载
return data
}
json.Unmarshal([]byte(vData), &dataDict)
可以试试
go run main.go json
./examples/t4-json.go
解析从 string -> json 获得 的匹配项
有关go对json的使用
类型和结构定义,是 go 的特性,这些特性对于-程序的稳定和递进有影响
但,要适度。一般的类型也就那几个,string
, number
之类
其中json
之类的内置解析,go 会教给你一些招式,让你可以快速击倒 -json🐶
try
go run main.go json
annie/downloader/downloader.go
代码 121-139
func (data VideoData) Download(refer string) {
if data.Size == 0 {
data.calculateTotalSize()
}
data.printInfo()
if config.InfoOnly {
return
}
// pb 是 进度条库
bar := pb.New64(data.Size).SetUnits(pb.U_BYTES).SetRefreshRate(time.Millisecond * 10)
bar.ShowSpeed = true
bar.ShowFinalTime = true
bar.SetMaxWidth(1000)
bar.Start()
if len(data.URLs) == 1 { // 本次的例子只有一个视频流
// only one fragment
data.urlSave(data.URLs[0], refer, data.Title, bar)
bar.Finish()
}
// 。。
}
pb
- 可以看看 》》github source
作为下载状态的进度条
- urlSave
urlSave( 真实视频网址, 用户输入网址, 视频名, 进度条实例 )
annie/downloader/downloader.go
代码 67-19
// urlSave save url file
func (data VideoData) urlSave(
urlData URLData, refer, fileName string, bar *pb.ProgressBar,
) {
filePath := utils.FilePath(fileName, urlData.Ext, false) // 组合-本地下载路径-文件名
fileSize := utils.FileSize(filePath) // 文件大小
// TODO: Live video URLs will not return the size // 直播不会返回大小
if fileSize == urlData.Size { // 如果相等 自然下载完
fmt.Printf("%s: file already exists, skipping\n", filePath)
bar.Add64(fileSize)
return
}
tempFilePath := filePath + ".download"
tempFileSize := utils.FileSize(tempFilePath)
headers := map[string]string{
"Referer": refer, // 用户输入网址
}
var file *os.File
if tempFileSize > 0 { // 还是
//状态-显示
// range start from zero
headers["Range"] = fmt.Sprintf("bytes=%d-", tempFileSize)
file, _ = os.OpenFile(tempFilePath, os.O_APPEND|os.O_WRONLY, 0644)
bar.Add64(tempFileSize)
} else {
// 新建文件
file, _ = os.Create(tempFilePath)
}
// close and rename temp file at the end of this function
// must be done here to avoid the following request error to cause the file can't close properly
defer func() {
// 在结束本函数 时 defer 后面的函数 「注意⚠️是函数运行 不仅仅是定义/声明」 都会运行,所以一般用来关闭 文件 数据库 连接 的关闭工作
file.Close()
// must close the file before rename or it will cause `The process cannot access the file because it is being used by another process.` error.
err := os.Rename(tempFilePath, filePath)
if err != nil {
log.Fatal(err)
}
}() // <--- 运行
res := request.Request("GET", urlData.URL, nil, headers)
if res.StatusCode >= 400 {
// color 是 颜色库 帮-显示信息-加颜色
red := color.New(color.FgRed)
log.Print(urlData.URL)
log.Fatal(red.Sprintf("HTTP error: %d", res.StatusCode))
}
defer res.Body.Close()
writer := io.MultiWriter(file, bar)
// go语言中 - 同时输出到文件和控制台(命令行)
// 请注意,io.Copy从输入读取32kb(最大值)并将它们写入输出,然后重复。 也就是说-一步到位,不用管了
_, copyErr := io.Copy(writer, res.Body) // res.Body 即是视频流本身 复制给文件 和 进度条
if copyErr != nil { // 错误
log.Fatal(fmt.Sprintf("Error while downloading: %s, %s", urlData.URL, copyErr))
}
request.Request("GET", urlData.URL, nil, headers)
重中之重, 在这步之后我们就拿到-真实数据和状态了
Request( 网页请求方式, 真实网址, io.Reader ?? ,请求头)
io.MultiWriter(file, bar)
-io.Copy(writer, res.Body)
你可以试试
go run main.go pb
查看相关代码
color
颜色库
annie/request/request.go
代码 24-107
// Request base request
func Request(
method, url string, body io.Reader, headers map[string]string,
) *http.Response {
transport := &http.Transport{
DisableCompression: true,
TLSHandshakeTimeout: 10 * time.Second,
}
if config.Proxy != "" {
// 添加代理
var httpProxy, err = netURL.Parse(config.Proxy)
if err != nil {
panic(err)
}
transport.Proxy = http.ProxyURL(httpProxy)
}
if config.Socks5Proxy != "" {
// socks-代理
dialer, err := proxy.SOCKS5(
"tcp",
config.Socks5Proxy,
nil,
&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
},
)
if err != nil {
panic(err)
}
transport.Dial = dialer.Dial
}
// 请求客户端 - 使用 Client.Do请求
client := &http.Client{
Timeout: time.Second * 100,
Transport: transport,
}
// 定义好- 网址的请求信息🆕
req, err := http.NewRequest(method, url, body)
if err != nil {
log.Print(url)
panic(err)
}
for k, v := range config.FakeHeaders {
req.Header.Set(k, v)
}
req.Header.Set("Referer", url)
if config.Cookie != "" {
var cookie string
if _, fileErr := os.Stat(config.Cookie); fileErr == nil {
// Cookie is a file
data, _ := ioutil.ReadFile(config.Cookie)
cookie = string(data)
} else {
// Just strings
cookie = config.Cookie
}
req.Header.Set("Cookie", cookie)
}
for k, v := range headers {
req.Header.Set(k, v)
}
if config.Refer != "" {
req.Header.Set("Referer", config.Refer)
}
// ———————— 定义完成✅
// 使用 Client.Do请求
res, err := client.Do(req)
if err != nil {
log.Print(url)
panic(err)
}
// 调试时-状态显示
if config.Debug {
blue := color.New(color.FgBlue)
fmt.Println()
blue.Printf("URL: ")
fmt.Printf("%s\n", url)
blue.Printf("Method: ")
fmt.Printf("%s\n", method)
blue.Printf("Headers: ")
pretty.Printf("%# v\n", req.Header)
blue.Printf("Status Code: ")
if res.StatusCode >= 400 {
color.Red("%d", res.StatusCode)
} else {
color.Green("%d", res.StatusCode)
}
}
// 返回请求结果
return res
// _, copyErr := io.Copy(writer, res.Body) // res.Body 即是视频流本身 复制给文件 和 进度条
// 上小节的
一般来说,测试最后先写在实现前,这样会有方向一点
annie/extractors/douyin_test.go
package extractors
import (
"testing"
"github.com/iawia002/annie/config"
"github.com/iawia002/annie/test"
)
func TestDouyin(t *testing.T) {
config.InfoOnly = true
tests := []struct {
name string
args test.Args
}{
{
name: "normal test",
args: test.Args{
URL: "https://www.douyin.com/share/video/6509219899754155272",
Title: "好冷 逢考必过",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := Douyin(tt.args.URL) // Douyin 返回的 是定义的视频数据块
test.Check(t, tt.args, data) // 对比 tt.args 和 data 对象 每一个是否相同
})
}
}