
1. 前言
從之前的文章 從零到一:用Golang編寫機(jī)器人 ,我們已經(jīng)可以編寫一個屬于自己的小機(jī)器人了。
而本文將講解自己的機(jī)器人Samaritan找電影技能的實(shí)現(xiàn),算是拋磚引玉吧。
本文技術(shù)僅供交流學(xué)習(xí),請尊重影視版權(quán)。
2. 明確需求與前期準(zhǔn)備
當(dāng)我們想下載電影時:
- 輸入電影名稱
- 找到相關(guān)頁面
- 找到下載資源超鏈接
- 復(fù)制鏈接地址用于最終的下載
而交給機(jī)器人做的話:
- 識別用戶的輸入
- 找到資源鏈接并格式化
- 輸出格式化之后的結(jié)果
其中第1步和第3步是不是似曾相識?其實(shí)這正是之前文章實(shí)現(xiàn)的一個對話過程,只不過我們不再是讓機(jī)器人“自由發(fā)揮”,而是告訴機(jī)器人該回復(fù)什么內(nèi)容。
所以我們還需要做的,僅是教會機(jī)器人怎么從網(wǎng)絡(luò)中搜索信息,以及哪些是我們所需要的信息。最好的辦法便是“身教”,讓機(jī)器人學(xué)習(xí)并模仿我們完成整個過程的所有動作。
3. 獲取并解析資源
此處以電影“星球大戰(zhàn)7”為例,資源站點(diǎn)選擇龍部落,目標(biāo)是找到可用下載鏈接。
以下操作,實(shí)為我們用瀏覽器找到最終鏈接的操作記錄
3.1 搜索“星球大戰(zhàn)7”

而對于機(jī)器人,便是請求http://www.lbldy.com/search/星球大戰(zhàn)7,獲取頁面返回:
movie:= "星球大戰(zhàn)7"
resp, _ := http.Get("http://www.lbldy.com/search/" + movie)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
這里暫時忽略錯誤處理,此時 body的值便是我們剛才在瀏覽器內(nèi)看到的頁面的源碼了,通過瀏覽器審查元素同樣可以看到:

3.2 找到第一個結(jié)果鏈接
右鍵復(fù)制鏈接地址可知為:http://www.lbldy.com/movie/64115.html
唯一的變量便是64115這個數(shù)字,這正是網(wǎng)頁源碼中出現(xiàn)的數(shù)字
<div> class="postlist" id="post-64115"
大膽猜測,只需要提取出id="post-64115"中的數(shù)字即可,此時比較簡單的做法便是利用正則:
re, _ := regexp.Compile("<div class=\"postlist\" id=\"post-(.*?)\">")
firstId := re.FindSubmatch(body) //find first match case
3.3 進(jìn)入資源下載頁
此時瀏覽器部分顯示內(nèi)容為:

審查元素:

可以看到下載地址已經(jīng)看到了,接下來要做的就是讓機(jī)器人從中提取所有相關(guān)鏈接了。
上一步我們已經(jīng)找到電影id,讓機(jī)器人同樣訪問此頁面:
resp, _ = http.Get("http://www.lbldy.com/movie/" + id + ".html")
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(io.Reader(resp.Body))
if err != nil {
return
}
雖然依舊可以用正則來搜索下載鏈接,但此時可用goquery庫來處理較為復(fù)雜的html頁面。
doc.Find("p").Each(func(i int, selection *goquery.Selection) {
name := selection.Find("a").Text()
link, _ := selection.Find("a").Attr("href")
if strings.HasPrefix(link, "ed2k") || strings.HasPrefix(link, "magnet") || strings.HasPrefix(link, "thunder") {
m := Media{
Name: name,
Link: link,
}
ms = append(ms, m)
}
})
goquery通過對html標(biāo)簽的解析,為我們找到了所有的下載結(jié)果列表。
3.4 復(fù)制下載鏈接
機(jī)器人將找到的結(jié)果通過channel返回給用戶:
if len(ms) == 0 {
results <- fmt.Sprintf("No results for *%s* from LBL", movie)
return
} else {
ret := "Results from LBL:\n\n"
for i, m := range ms {
ret += fmt.Sprintf("*%s*\n```%s```\n\n", m.Name, m.Link)
//when results are too large, we split it.
if i%4 == 0 && i < len(ms)-1 && i > 0 {
results <- ret
ret = fmt.Sprintf("*LBL Part %d*\n\n", i/4+1)
}
}
results <- ret
}
此時我們可以從機(jī)器人處獲得回復(fù):

4. 從更多資源站點(diǎn)獲取
通常我們會通過多個的資源站點(diǎn)搜索同一資源,Samaritan在搜索電影時,除了龍部落,還會從字幕組獲取。
字幕組的資源搜索流程和龍部落差不多,只不過涉及到登錄,所以在獲取資源前需讓機(jī)器人先登錄,并攜帶cookie訪問:
//zmz.tv needs to login before downloading
var zmzClient http.Client
func loginZMZ() {
gCookieJar, _ := cookiejar.New(nil)
zmzURL := "http://www.zimuzu.tv/User/Login/ajaxLogin"
zmzClient = http.Client{
Jar: gCookieJar,
}
zmzClient.PostForm(zmzURL, url.Values{"account": {"username"}, "password": {"password"}, "remember": {"0"}})
}
通過cookiejar登錄,zmzClient在后續(xù)訪問時便可攜帶用戶cookie,得以訪問需登錄的頁面。
同樣的電影,從字幕組獲取的資源:

5. 更快地返回結(jié)果
當(dāng)我們有A, B, C..若干個資源站點(diǎn)時,寫出的代碼很可能是這樣
func DownloadMovie(){
retA := getResourceFromA()
retB := getResourceFromB()
retC := getResourceFromC()
...
return retA + retB + retC
}
而理想情況下,我們希望并發(fā)地進(jìn)行資源獲取,只要一有結(jié)果,立馬返回給用戶。
利用Golang的CSP并發(fā)模型,用goroutine不難寫出并發(fā)的版本:
func DownloadMovie(results chan<- string){
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
results <- getResourceFromA()
}()
go func() {
defer wg.Done()
results <- getResourceFromB()
}()
go func() {
defer wg.Done()
results <- getResourceFromC()
}()
wg.Wait()
close(results)
}
而調(diào)用者只需不斷從channel獲取:
func(){
results:= make(chan string)
go DownloadMovie(results)
for {
msg, ok := <-results //retrive result from channel
if !ok {
return
}
reply(msg)
}
}
這樣,用戶就可以第一時間收到回復(fù)了。這便是goroutine與channel配合的精妙之處了。
6. 總結(jié)
通過上一篇文章,我們搭建了一個可以對話的小機(jī)器人,而本文講解了機(jī)器人常見的一個技能:爬取資源(爬蟲)。
經(jīng)過已有的知識儲備,然后通過分析,明確了我們的目標(biāo)。無非是接受用戶輸入->找到資源->輸出給用戶。
然后以找電影資源為例,讓機(jī)器人一步步地模擬用戶操作,最終找到了資源鏈接。
可我們并未滿足于此,提出了兩個優(yōu)化點(diǎn),功能性需求上,我們從更多的站點(diǎn)獲取到了資源; 非功能需求上,我們通過Golang的并發(fā)特性使得結(jié)果返回更快。
源碼參考
Have Fun!