Golangでスクレイピング(Rubyの225倍速)
Go速い。並列処理速い。
rubyの実装だと15分かかっていた処理が4.5秒へ。ヤバすぎるぜGo!!
もう、本当にrubyは書きやすいシェルスクリプトぐらいの立ち位置で良いんじゃないかな。
テストデータ作る処理とかだけで。
他はPythonとかGoで書くと良いとおもいましたよ。
スクレイピングはいかに効率を上げるかが重要ですが、Goの並行処理の仕組みは使いやすさも含めてすごい良いです。
ともかく速さは命。
package main /* 実装方針 とりまDB登録とかしちゃうのはやめておいて、 logger作るのはやめておいて JSONから設定読み込んで出力するとか goqueryで一回叩いてみるとか、その辺を2017/06/10はするところまで チャンネルとかデータ構造とかは2017/06/11から少しずつやっていけばよろし。 DB以外の部分はとりあえず出来た。 めっちゃ速いwwww rubyだと900秒 goだと4.5秒 */ import ( "encoding/json" f "fmt" "github.com/PuerkitoBio/goquery" "net/url" "os" "os/exec" "path/filepath" "regexp" "strings" "sync" ) const StartURL = "http://youtubeanisoku1.blog106.fc2.com/" const Configfile = "config.json" const ( ANISOKUTOP int = iota KOUSINPAGE KOBETUPAGE HIMADOSEARCH HIMADOVIDEO ) type Config struct { DonloadDir string `json:"downloaddir"` DBFILE string `json:"dbfile"` TITLEREGEXP string `json:"title_regexp"` } var cfg Config type JOB struct { JOBKIND int URL string TITLE string EPISODE string } func (job *JOB) Dispacher() { switch job.JOBKIND { case ANISOKUTOP: wg.Add(1) go job.AnisokuTop() case KOUSINPAGE: wg.Add(1) go job.KousinPage() case KOBETUPAGE: wg.Add(1) go job.KobetuPage() case HIMADOSEARCH: wg.Add(1) go job.HimadoSearch() case HIMADOVIDEO: wg.Add(1) go job.HimadoVideo() default: } } // トップページのスクレイピング func (job *JOB) AnisokuTop() { defer wg.Done() doc, err := goquery.NewDocument(job.URL) if err != nil { f.Println("url scraping fail:", job.URL) return } doc.Find(".Top_info div ul li a").Each(func(_ int, s *goquery.Selection) { title, _ := s.Attr("title") if strings.Contains(title, "更新状況") { u, _ := s.Attr("href") JobCh <- &JOB{KOUSINPAGE, u, "", ""} } }) } // 更新ページのスクレイピング func (job *JOB) KousinPage() { defer wg.Done() doc, err := goquery.NewDocument(job.URL) if err != nil { f.Println("kousinpage error", job.URL) return } doc.Find("ul[type='square'] li a").Each(func(_ int, s *goquery.Selection) { href, _ := s.Attr("href") if href == "" { return } title, _ := s.Attr("title") if title != "" { return } JobCh <- &JOB{KOBETUPAGE, href, "", ""} }) } // 個別ページのスクレイピング func (job *JOB) KobetuPage() { defer wg.Done() doc, err := goquery.NewDocument(job.URL) if err != nil { f.Println("kobetupage error", job.URL) return } var title string doc.Find("title").Each(func(_ int, s *goquery.Selection) { title = s.Text() title = cleanupValue(title) }) if !TitleRegexp.MatchString(title) { return } _, ok := TitleMap[title] if ok { return } else { TitleMap[title] = true } //f.Println("DO:", title) found := false doc.Find("a").Each(func(_ int, s *goquery.Selection) { if found { return } href, _ := s.Attr("href") if !strings.Contains(href, "himado.in") { return } //一つ見つかればOK if !found { JobCh <- &JOB{HIMADOSEARCH, href, title, ""} found = true } }) } // ひまわり検索ページ func (job *JOB) HimadoSearch() { defer wg.Done() doc, err := goquery.NewDocument(job.URL) if err != nil { f.Println("himadosearch error", job.URL) return } count := 0 doc.Find(".thumbtitle a[rel='nofollow']").Each(func(_ int, s *goquery.Selection) { if count > 2 { return } href, _ := s.Attr("href") if href == "" { return } href = "http://himado.in" + href // 再取得制御 _, exist := PageMap[href] if exist { return } PageMap[href] = true episode, _ := s.Attr("title") if episode == "" { return } episode = cleanupValue(episode) JobCh <- &JOB{HIMADOVIDEO, href, job.TITLE, episode} count++ }) } // ひまわりビデオページ func (job *JOB) HimadoVideo() { defer wg.Done() doc, err := goquery.NewDocument(job.URL) if err != nil { f.Println("himadoVideo error", job.URL) return } mediaUrl := "" doc.Find("script").Each(func(_ int, s *goquery.Selection) { text := s.Contents().Text() if text == "" { return } texta := strings.Split(text, "\n") for _, l := range texta { if strings.Contains(l, "var movie_url") { l = strings.TrimSpace(l) l = strings.Replace(l, "var movie_url = '", "", -1) l = strings.Replace(l, "';", "", -1) u, err := url.PathUnescape(l) if err == nil { mediaUrl = u } break } } }) if mediaUrl == "" { return } fp := makeFilePath(job.TITLE, job.EPISODE) if !FileIsExists(fp) { return } job.DownloadVideo(mediaUrl) } // ビデオダウンロード func (job *JOB) DownloadVideo(url string) { err := os.MkdirAll(makeFileDirPath(job.TITLE), 0777) if err != nil { f.Println("ディレクトリ作成失敗") return } fp := makeFilePath(job.TITLE, job.EPISODE) cmd := "curl -# -L " + url + " | ffmpeg -threads 4 -y -i - -vcodec copy -acodec copy '" + fp + "' &" f.Println(cmd) exec.Command("sh", "-c", cmd).Start() } // ディレクトリを確認 func makeFileDirPath(title string) string { return filepath.Join(cfg.DonloadDir, title) } // ファイルパスを作成 func makeFilePath(title string, episode string) string { return filepath.Join(cfg.DonloadDir, title, episode+".mp4") } // ファイル存在確認 func FileIsExists(filename string) bool { _, err := os.Stat(filename) return err != nil } // 値をきれいにする func cleanupValue(s string) string { s = strings.Replace(s, "★ You Tube アニ速 ★", "", -1) s = strings.Replace(s, ":", ":", -1) s = strings.Replace(s, "第", "", -1) s = strings.Replace(s, "話", ":", -1) s = strings.Replace(s, ".", "", -1) s = strings.Replace(s, " ", "", -1) s = strings.Replace(s, " ", "", -1) s = strings.Replace(s, "#", "", -1) s = strings.Replace(s, "(", "", -1) s = strings.Replace(s, ")", "", -1) s = strings.Replace(s, "/", "", -1) s = strings.Replace(s, "(", "", -1) s = strings.Replace(s, ")", "", -1) s = strings.Replace(s, "+", "+", -1) s = strings.Replace(s, "[720p]", "", -1) s = strings.Replace(s, "高画質", "", -1) s = strings.Replace(s, "QQ", "", -1) s = strings.Replace(s, "?", "?", -1) s = strings.Replace(s, "[", "", -1) s = strings.Replace(s, "]", "", -1) return s } // 確認済み管理マップ var TitleMap map[string]bool = make(map[string]bool) var PageMap map[string]bool = make(map[string]bool) var TitleRegexp *regexp.Regexp // JOBチャネル var JobCh chan *JOB = make(chan *JOB) // WaitGroup var wg sync.WaitGroup = sync.WaitGroup{} // コンフィグを読み出す func loadConfig() (*Config, error) { fh, err := os.Open(Configfile) if err != nil { return nil, err } defer fh.Close() err = json.NewDecoder(fh).Decode(&cfg) TitleRegexp = regexp.MustCompile(cfg.TITLEREGEXP) return &cfg, err } // JOBチャネルのレシーバー func receiver(ch chan *JOB) { for { job := <-ch job.Dispacher() } } // 本体 func Run() int { // コンフィグ読み出し cfg, err := loadConfig() if err != nil { f.Println("config load error", err) return 1 } f.Println(cfg) go receiver(JobCh) // 初期キック JobCh <- &JOB{ANISOKUTOP, StartURL, "", ""} wg.Wait() return 0 } func main() { retcode := Run() os.Exit(retcode) }