Pholcus爬虫:爬取慕课网内容


  • 利用纯Go语言编写的开源的支持分布式的高并发、重量级爬虫软件Pholcus(幽灵蛛)爬取慕课网课程、章节等并入库MongoDB。

写在前面

个人非常热爱慕课网,在慕课网学习两年多了,收获很大,编写此爬虫目的纯粹为了学习Pholcus, 爬虫也并未爬取慕课网视频地址(其实慕课的视频地址不是那么容易就能爬到的,好像是分割成了1M左右的小视频),没有其他任何商业用途。同时也希望网友能爱护慕课网如果要爬,请轻爬!

爬取说明

  1. 在图1页面爬取课程名称、课程简介等,然后点击进入在图2页面爬取学习人数、课程章节等内容。最后如图3写入数据库。
  2. 爬取过程中,图1中爬取的内容并不直接如果,而是通过struct request中的temp字段以及SetTemp()GetTemp()讲图1中内容与图2中的内容整合后写入数据库。
  3. 因为慕课网课程内容层级结构类似于JSON,所以数据库考虑使用MongoDB,实现图3效果。

Paste_Image.png

Paste_Image.png

Paste_Image.png

Spider源码

package imooc

import (
    "strconv"
    "strings"

    "encoding/json"
    //"io/ioutil"
    "log"
    "net/http"
    "regexp"

    "fmt"
    "github.com/henrylee2cn/pholcus/app/downloader/request"
    . "github.com/henrylee2cn/pholcus/app/spider"
    "github.com/henrylee2cn/pholcus/common/goquery"
)

func init() {
    Imooc.Register()
}

func cleanHTML(s string) string {
    s = strings.Replace(s, " ", "", -1)
    s = strings.Replace(s, "\n", "", -1)
    s = strings.Replace(s, "\t", "", -1)
    return s
}

type Data struct {
    id      string `json:"id"`
    Numbers string `json:"numbers"`
}

type AjaxCourseMembers struct {
    result int    `json:"result"`
    Data   []Data `json:"data"`
    msg    string `json:"msg"`
}

var chapterin = make(map[string]string)
var Score = make(map[string]string)
var chapterout = make(map[string]interface{})
var chapterall = make(map[string]interface{})

var Imooc = &Spider{
    Name:         "Imooc",
    Description:  "慕课网课程,[Auto Page][imooc.com]",
    EnableCookie: false,
    RuleTree: &RuleTree{
        Root: func(ctx *Context) {
            ctx.AddQueue(&request.Request{Url: "http://www.imooc.com/course/list", Rule: "首页请求"})
        },
        Trunk: map[string]*Rule{
            "首页请求": {
                ParseFunc: func(ctx *Context) {
                    tmpMp, _ := ctx.GetDom().Find("div.page").Find("a").Eq(8).Attr("href")
                    maxPage, _ := strconv.Atoi(strings.TrimLeft(tmpMp, "/course/list?page="))
                    ctx.Aid(map[string]interface{}{"loop": [2]int{1, maxPage}, "Rule": "所有课程"}, "所有课程")
                },
            },

            "所有课程": {
                AidFunc: func(ctx *Context, aid map[string]interface{}) interface{} {
                    for loop := aid["loop"].([2]int); loop[0] < loop[1]; loop[0]++ {
                        ctx.AddQueue(&request.Request{
                            Url:  "http://www.imooc.com/course/list?page=" + strconv.Itoa(loop[0]),
                            Rule: aid["Rule"].(string),
                        })
                    }
                    return nil
                },

                ParseFunc: func(ctx *Context) {
                    query := ctx.GetDom().Find(".index-card-container")
                    query.Each(func(i int, goq *goquery.Selection) {
                        CourseTitle := goq.Find(".course-card-name").Text()
                        TechStack := goq.Find(".course-card-top span").Text()
                        Introduction := goq.Find("p").Text()
                        Attr, ok := goq.Find(".course-card").Attr("href")
                        CourseUrlNumber := strings.Join(regexp.MustCompile("[0-9]").FindAllString(Attr, -1), "")
                        url := "http://www.imooc.com/course/AjaxCourseMembers?ids=" + CourseUrlNumber
                        resp, err := http.Get(url)

                        /***

                        if err != nil {
                            log.Println("ERROR:", err)
                            return
                        }
                        doc1, _ := ioutil.ReadAll(resp.Body)
                        ajaxCourseMembers := &AjaxCourseMembers{}
                        if err:= json.Unmarshal([]byte(string(doc1)), &ajaxCourseMembers);err!=nil{
                            log.Println("ERROR:", err)
                            return
                        }

                        ***/
                        
                        //myjson, _ := ioutil.ReadAll(resp.Body)
                        //fmt.Println(string(myjson))  //resp的body内容OK

                        if err != nil {
                            log.Println("ERROR:", err)
                        }
                        defer resp.Body.Close()
                        ajaxCourseMembers := &AjaxCourseMembers{}
                        if err := json.NewDecoder(resp.Body).Decode(&ajaxCourseMembers); err != nil {
                            log.Println("ERROR:", err)
                        }
                        //LearnerNumber:=ajaxCourseMembers.data[0].Numbers   //此处,这么写更加方便。
                        LearnerNumber := ajaxCourseMembers.getnumbers()

                        if ok == true {
                            ctx.AddQueue(&request.Request{
                                Url:  "http://www.imooc.com" + Attr,
                                Rule: "课程详细信息",
                                Temp: map[string]interface{}{
                                    "CourseTitle":   CourseTitle,
                                    "TechStack":     TechStack,
                                    "Introduction":  Introduction,
                                    "LearnerNumber": LearnerNumber,
                                },
                            })
                        }

                    })

                },
            },
            "课程详细信息": {

                ItemFields: []string{
                    "课程名称",
                    "课程分类",
                    "课程简介",
                    "学习人数",
                    "课程介绍",
                    "课程路径",
                    "难度级别",
                    "课程时长",
                    "评分",
                    "章节",
                },
                ParseFunc: func(ctx *Context) {
                    dom := ctx.GetDom()
                    query := dom.Find(".mod-chapters > div")
                    Summary := cleanHTML(dom.Find("div.course-brief").Text())
                    CoursePath := cleanHTML(dom.Find(".course-infos").Find(".path").Text())
                    Difficulty := dom.Find(".course-infos").Find("div.static-item").Eq(1).Find(".meta-value").Text()
                    Duration := dom.Find(".course-infos").Find("div.static-item").Eq(2).Find(".meta-value").Text()
                    scoretmp := dom.Find(".course-infos").Find(".score-btn")
                    vScore0 := scoretmp.Find("span").Eq(0).Text()
                    vScore1 := scoretmp.Find("span").Eq(1).Text()
                    vScore2 := scoretmp.Find("span").Eq(2).Text()
                    vScore3 := scoretmp.Find("span").Eq(3).Text()
                    vScore4 := scoretmp.Find("span").Eq(4).Text()
                    vScore5 := scoretmp.Find("span").Eq(5).Text()
                    vScore6 := scoretmp.Find("span").Eq(6).Text()
                    vScore7 := scoretmp.Find("span").Eq(7).Text()
                    vScore8 := scoretmp.Find("span").Eq(8).Text()
                    Score[vScore0] = vScore1
                    Score["评价数"] = vScore2
                    Score[vScore4] = vScore3
                    Score[vScore6] = vScore5
                    Score[vScore8] = vScore7
                    query.Each(func(i int, goq *goquery.Selection) {
                        ChapterH1 := cleanHTML(goq.Find("strong").After("i").Text())
                        ctx.SetTemp("ChapterH1", ChapterH1)
                        Chapter2_html := goq.Find("ul.video>li")
                        Chapter2_html.Each(func(_ int, goq1 *goquery.Selection) {
                            Chapter2_url, _ := goq1.Find("a").Attr("href")
                            Chapter2 := cleanHTML(goq1.Find("a").After("button").Text())
                            chapterin[Chapter2] = "www.imooc.com" + cleanHTML(Chapter2_url)
                            ctx.SetTemp("JsonChapterH1", chapterin)
                        })
                        chapterout[ctx.GetTemp("ChapterH1", "").(string)] = ctx.GetTemp("JsonChapterH1", "")
                        chapterall[ctx.GetTemp("CourseTitle", "").(string)] = chapterout
                        chapterin = make(map[string]string)

                    })
                    chapterout = make(map[string]interface{})
                    ctx.Output(map[int]interface{}{
                        0: ctx.GetTemp("CourseTitle", "").(string),
                        1: ctx.GetTemp("TechStack", "").(string),
                        2: ctx.GetTemp("Introduction", "").(string),
                        3: ctx.GetTemp("LearnerNumber", "").(string),
                        4: Summary,
                        5: CoursePath,
                        6: Difficulty,
                        7: Duration,
                        8: Score,
                        9: chapterall[ctx.GetTemp("CourseTitle", "").(string)],
                    })
                    chapterall = make(map[string]interface{})
                    Score = make(map[string]string)
                },
            },
        },
    },
}

func (s *AjaxCourseMembers) getnumbers() string {
    return s.Data[0].Numbers
}

源码说明

  1. 如上面爬取说明->2中说明,代码第115行的temp字段临时存放页面1爬取到的字段,并传递到下一个Rule(这是跨Rule字段传递);代172、174等行中的SetTemp()GetTemp()在同一请求下存取以及获取临时字段(这是同Rule字段传递)。
  2. 如图4,页面2中的人数字段是通过Ajax动态加载的,所以需要通过相关接口单独获取JSON格式的人数字段。如图5,在代码第82、83行通过http.Get()返回Response。
  3. 在代码100~107行用json.NewDecoder(resp.Body).Decode解析JSON。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容