美文网首页
使用 Node.js 爬取 GitChat 全站课程

使用 Node.js 爬取 GitChat 全站课程

作者: alphabet_007 | 来源:发表于2019-06-04 17:18 被阅读0次

    GitChat 课程页面分析

    课程列表页

    GitChat 课程页面链接为:https://gitbook.cn/gitchat/columns,第一次加载完成加载了30条数据,之后通过滚动条滚动到页面底部懒加载的方式,调用相关方法,获取之后的数据。

    1559571985328-4c14f0a9-c4ab-40fd-94f4-fc665736d1f1.png

    查看浏览器控制台得知,每次请求数据调用的接口为:https://gitbook.cn/gitchat/columns?page=n&searchKey=&tag=,其中 n 为当前页面的页数, searchKey 为查询的关键词、 tag 为查询的标签,因为这里要爬取所有的课程数据,所以这里不需要 searchKeytag 参数。

    当请求 https://gitbook.cn/gitchat/columns?page=1 时,也就是获取第一页数据,和页面初次加载(访问 https://gitbook.cn/gitchat/columns)获取到的数据是一样的,所以通过 https://gitbook.cn/gitchat/columns?page=n 不断请求接口,就可以获取到所有的课程列表数据(课程链接)

    GitChat 源码中调用该接口的具体方法为:

        function loadMoreItems() {
            currentPage += 1;
            $.ajax({
                url: "/gitchat/columns?page=" + currentPage + '&searchKey=&tag=',
                type: "GET",
                contentType: "application/json; charset=utf-8",
                success: function(data, status) {
                    if (status == 'success' && data.code == 0) {
                        $('#colum-items #list_view').append(data.data);
                        console.log(data)
                        if (data.isEnd) {
                            if ('特价' == '') {
                                currentPage--;
                                $.ajax({
                                    url: "/gitchat/columns?page=" + currentPage + '&searchKey=',
                                    type: "GET",
                                    contentType: "application/json; charset=utf-8",
                                    success: function(data, status) {
                                        if (status == 'success' && data.code == 0) {
                                            $('#colum-items #list_view').append(data.data);
                                            if (data.isEnd) {
                                                scene.destroy(true);
                                                $("#loader").removeClass("active");
                                                $("#loader").hide();
                                                $("#noMoreToLoad").show();
                                            } else {
                                                scene.update();
                                                $("#loader").removeClass("active");
                                            }
                                        }
                                    }
                                })
                            } else {
                                scene.destroy(true);
                                $("#loader").removeClass("active");
                                $("#loader").hide();
                                $("#noMoreToLoad").show();
                            }
    
                        } else {
                            scene.update();
                            $("#loader").removeClass("active");
                        }
                        $('.banner').lazyload({
                            threshold: 100,
                            effect: "fadeIn"
                        })
                    }
                }
            });
        }
    

    response.isEndtrue 时,表示结束了,这是最后一页数据了,这时就不再需要调用接口去请求了。

    课程详情页

    从课程列表页点击任意一条课程链接,会进入课程详情页,如:https://gitbook.cn/gitchat/column/5c0e149eedba1b683458fd5f,当前我的是超级会员,进入课程详情页后,页面右上角显示两个按钮:会员免费学和免费试读,点击会员免费学后触发方法来订阅该课程:

    function submitVIPColomnOrder(o, r) {
        var i = $("#payColmunBtn")
          , a = i.html();
        i.addClass("link-not-active"),
        i.html("预订中...");
        var e = encodeURI(window.location.href);
        $.ajax({
            url: "/m/mazi/vip/order/column",
            type: "POST",
            data: JSON.stringify({
                columnId: o,
                requestUrl: e
            }),
            dataType: "json",
            contentType: "application/json; charset=utf-8",
            success: function(e, t) {
                if ("success" == t && 0 == e.code && "success" == e.message)
                    window.location.href = "/m/mazi/comp/column?columnId=" + o + "&refresh=true&newBuyer=true";
                else if (-1 == e.code && "reader exist" == e.message)
                    window.location.href = "/m/mazi/comp/column?columnId=" + o + "&refresh=true&newBuyer=true";
                else if (-1 == e.code && "no quota" == e.message)
                    if (r) {
                        var n = $("#payMethodModal");
                        $("#showVIPBuyTextTip").text("精英会员:尊敬的精英会员,您本月的达人课兑换名额已用完,请下个月再来或付费购买。"),
                        n.modal("show"),
                        i.removeClass("link-not-active"),
                        i.html(a)
                    } else {
                        n = $("#showVIPBuyModal");
                        $("#showVIPBuyText").text("精英会员:尊敬的精英会员,您本月的达人课兑换名额已用完,请下个月再来或付费购买。"),
                        n.modal("show"),
                        i.removeClass("link-not-active"),
                        i.html(a)
                    }
                else
                    -1 == e.code && e.redirectUrl && "" != e.redirectUrl ? (i.removeClass("link-not-active"),
                    i.html(a),
                    window.location.href = e.redirectUrl + "?err=" + e.message) : (alert("请检查会员是否到期,如果有问题,联系客服。"),
                    window.location.href = location.href)
            }
        })
    }
    
    submitVIPColomnOrder('59e467846958416d85ae933e',true);
    

    只有订阅后才有权限阅读,订阅后点击右侧该课程的主题列表,可以查看该课程下的每一篇主题文章,如:https://gitbook.cn/gitchat/column/5ce4ff9a308dd66813d92799/topic/5ce50135308dd66813d927a6,有了最终具体一篇文章的最终链接了,就可以获取到文章内容了。

    爬取过程

    工具下载和使用

    本次爬取数据使用的 Node 模块有:request,cheerio,fs

    其中 request 用来根据 url 发起 GET 或 POST 请求,这次爬取的数据属于需要登录才能查看到的内容,所以请求时需要携带 Cookie 来模拟登录状态,使用 User-Agent 模拟浏览器操作,它们都可以放在 request 方法的 options.headers

    cheerio 用来操作获取到的数据,使用方法类似 jQuery

    fs 是 Node 自带模块,可以将数据写入保存到本地

    安装 request、cheerio:

    npm install --save request cheerio
    

    此时安装后的版本分别是:

      "dependencies": {
        "cheerio": "^1.0.0-rc.2",
        "request": "^2.88.0"
      }
    

    引入资源

    var request = require("request");
    var cheerio = require("cheerio");
    var fs = require("fs");
    

    GitChat 网站查看控制台,获取到 Cookie 和 User-Agent 数据保存下来:

    var cookieStr = "cookieStr";
    var userAgentStr = "userAgentStr";
    var headers = {
        "Cookie": cookieStr,
        "User-Agent": userAgentStr
    }
    

    获取所有课程的 url

    不断请求接口,获取课程列表的数据,当 response.isEndtrue 时,停止继续请求,然后一次性订购所有课程

    function getAllCourseUrl(){
        var requestUrl = "https://gitbook.cn/gitchat/columns?page=" + currentPage;
        var options = {
            url: requestUrl,
            headers: headers,
        }
        request(options, function (error, response, body) {
            if (error === null && response.statusCode == 200) {
                var resObj=JSON.parse(body);
                console.log("resObj", resObj);
                var columnList = resObj.columnList;
                columnList.forEach(function(item){
                    allCourseIds.push(item._id);
                    allTopicN+=item.columnTopics.length;
                });
                if (resObj.code === 0 && resObj.isEnd !== true){
                    currentPage++;
                    getAllCourseUrl();
                }
                else{
                    allCourseIds.forEach(function(item){
                        submitVIPColomnOrder(item, true);
                    });
                }
            } else {
                console.log("getAllCourseUrl 失败", "\n", currentPage);
            }
        });    
    }
    getAllCourseUrl();
    

    订阅所有课程

    重复调用 submitVIPColomnOrder 方法,传入课程 id,订阅所有课程

    function submitVIPColomnOrder(courseId, bol) {
        request.post(
            {
                headers: headers,
                url: 'https://gitbook.cn/m/mazi/vip/order/column',
                form: { columnId: courseId, requestUrl: bol },
            },
            function (err, httpResponse, body) {
                var resObj = {};
                if (err === null && body) {
                    resObj = JSON.parse(body);
                    if (resObj.code === 0 && resObj.message === "success") {
                        successBugCourseN++;
                        if (successBugCourseN === allCourseIds.length) {
                            allCourseIds.forEach(function (item) {
                                getTopicAll(courseBaseUrl + item);
                            });
                        }
                    }
                }
                else {
                    
                }
            }
        );
    }
    

    获取某一课程中的全部文章 id

    function getTopicAll(courseUrl){
        var options = {
            url: courseUrl,
            headers: headers,
        }
        request(options, function (error, response, body) {
            if (error === null && response.statusCode == 200) {
                var $ = cheerio.load(body);
                var courseTitle = $(".catalog_items_head .lesson_intro").text();
                var topic = $(".catalog_view .catalog_items_item");
                topic.each(function(index){
                    var courseNum=index+1;
                    var topicTitle = $(this).children(".catalog_item_txt").attr("title");
                    if ($(this).attr("onclick")){
                        var topicId = $(this).attr("onclick").substring(14, 38);
                        getTopicOne(courseUrl, courseTitle, courseNum, topicTitle, topicId);
                    }
                });
            } else {
                
            }
        });
    };
    

    根据文章 id 获取文章内容并保存

    function saveTopicOne(courseTitle, courseNum, topicTitle, pageCont){
        var dir = "./courses/";
        var fileName = courseTitle + "-" + courseNum + "-" + topicTitle + ".html";
        fileName = fileName.replace(/\s+/g, "");
        fileName = fileName.replace(/[<>:"/|?*]+/g, "");
        var path = dir + fileName;
        fs.writeFile(path, pageCont, function (error) {
            if (error) {
                console.log("error", error);
            } else {
                successSavedN++;
            }
        });    
    
    }
    function getTopicOne(courseUrl, courseTitle, courseNum, topicTitle, topicId){
        var options = {
            // https://gitbook.cn/gitchat/column/5c3168154fcd483b02710425/topic/5c317c264fcd483b02710541
            url: courseUrl + "/topic/" + topicId,
            headers: headers,
        }
        request(options, function (error, response, body) {
            if (error === null && response.statusCode == 200) {
                var $ = cheerio.load(body);
                var column_topic_view = $(".column_topic_view").html();
                var pageCont =''+
                    '<!DOCTYPE html>'+
                    '<html lang="en">'+
                    '<head>'+
                        '<meta charset="UTF-8">'+
                        '<meta name="viewport" content="width=device-width, initial-scale=1.0">'+
                        '<meta http-equiv="X-UA-Compatible" content="ie=edge">'+
                        '<title>Document</title>'+
                        '<link rel="stylesheet" href="../courseDetail.css">'+
                    '</head>'+
                    '<body>'+
                        '<div className="column_topic_view">'+
                            column_topic_view+
                        '</div>'+
                    '</body>'+
                    '</html>'+            
                '';
                saveTopicOne(courseTitle, courseNum, topicTitle, pageCont)
            } else {
            }
        }); 
    }
    

    我事先创建了 courses 目录用来保存文章 html 文件,并把需要的样式保存到 courseDetail.css 中,放到文章的上级目录中

    代码中是要获取课程的所有文章内容,因为有些课程并没有完全更新完,课程中后面的文章状态是写作中,这样的文章获取到的内容就是空的,只有一个文章标题

    执行结果

    1559587440829-f49c8bf1-aaf9-4052-8f66-72933e5ccfab.gif

    全部代码

    var request = require("request");
    var cheerio = require("cheerio");
    var fs = require("fs");
    
    var cookieStr = "cookieStr";
    var userAgentStr = "userAgentStr";
    var headers = {
        "Cookie": cookieStr,
        "User-Agent": userAgentStr,
    }
    
    var courseBaseUrl ="https://gitbook.cn/gitchat/column/";
    // 当前页数
    var currentPage = 1;
    // 所有课程 id,如:[5a0aaf9e61c244061266375f]
    // https://gitbook.cn/gitchat/column/5a0aaf9e61c244061266375f
    var allCourseIds = [];
    // 所有主题个数
    var allTopicN=0;
    // 成功购买的课程数
    var successBugCourseN = 0;
    // 成功保存文件个数
    var successSavedN=0;
    
    // 保存该课程的一个主题
    function saveTopicOne(courseTitle, courseNum, topicTitle, pageCont){
        var dir = "./courses/";
        var fileName = courseTitle + "-" + courseNum + "-" + topicTitle + ".html";
        fileName = fileName.replace(/\s+/g, "");
        fileName = fileName.replace(/[<>:"/|?*]+/g, "");
    
        var path = dir + fileName;
    
        fs.writeFile(path, pageCont, function (error) {
            if (error) {
                console.log("保存失败", "\n", path);
                console.log("error", error);
            } else {
                successSavedN++;
                console.log("保存成功", successSavedN, "/", allTopicN, path);
            }
        });    
    
    }
    
    // 获取课程一个主题的内容
    function getTopicOne(courseUrl, courseTitle, courseNum, topicTitle, topicId){
        var options = {
            url: courseUrl + "/topic/" + topicId,
            headers: headers,
        }
        request(options, function (error, response, body) {
            if (error === null && response.statusCode == 200) {
                var $ = cheerio.load(body);
                var column_topic_view = $(".column_topic_view").html();
                var pageCont =''+
                    '<!DOCTYPE html>'+
                    '<html lang="en">'+
                    '<head>'+
                        '<meta charset="UTF-8">'+
                        '<meta name="viewport" content="width=device-width, initial-scale=1.0">'+
                        '<meta http-equiv="X-UA-Compatible" content="ie=edge">'+
                        '<title>Document</title>'+
                        '<link rel="stylesheet" href="../courseDetail.css">'+
                    '</head>'+
                    '<body>'+
                        '<div className="column_topic_view">'+
                            column_topic_view+
                        '</div>'+
                    '</body>'+
                    '</html>'+            
                '';
                saveTopicOne(courseTitle, courseNum, topicTitle, pageCont)
    
                
            } else {
                console.log("获取主题失败", "\n", courseTitle, topicTitle);
            }
        }); 
    }
    
    // 根据课程url,获取课程所有主题的内容
    function getTopicAll(courseUrl){
        var options = {
            url: courseUrl,
            headers: headers,
        }
        request(options, function (error, response, body) {
            if (error === null && response.statusCode == 200) {
                var $ = cheerio.load(body);
                // 课程名字
                var courseTitle = $(".catalog_items_head .lesson_intro").text();
                // 所有主题
                var topic = $(".catalog_view .catalog_items_item");
                // 主题id
                topic.each(function(index){
                    var courseNum=index+1;
                    // 主题名字
                    var topicTitle = $(this).children(".catalog_item_txt").attr("title");
                    if ($(this).attr("onclick")){
                        var topicId = $(this).attr("onclick").substring(14, 38);
                        getTopicOne(courseUrl, courseTitle, courseNum, topicTitle, topicId);
                    }
                });
            } else {
                console.log("get topic all 失败", "\n", courseUrl);
            }
        });
    };
    // getTopicAll("https://gitbook.cn/gitchat/column/5ce4ff9a308dd66813d92799");
    
    // 购买某一门课程
    function submitVIPColomnOrder(courseId, bol) {
        request.post(
            {
                headers: headers,
                url: 'https://gitbook.cn/m/mazi/vip/order/column',
                form: { columnId: courseId, requestUrl: bol },
            },
            function (err, httpResponse, body) {
                var resObj = {};
                if (err === null && body) {
                    resObj = JSON.parse(body);
                    if (resObj.code === 0 && resObj.message === "success") {
                        successBugCourseN++;
                        console.clear();
                        console.log("成功购买的课程 success BugCourse N", successBugCourseN);
                        if (successBugCourseN === allCourseIds.length) {
                            console.log("已成功购买全部课程");
                            allCourseIds.forEach(function (item) {
                                getTopicAll(courseBaseUrl + item);
                            });
                        }
                    }
                }
                else {
                    console.log("submit VIP ColomnOrder 失败", "\n", err);
                }
            }
        );
    }
    
    // 获取所有课程的 url,如:https://gitbook.cn/gitchat/column/5a0aaf9e61c244061266375f
    function getAllCourseUrl(){
        var requestUrl = "https://gitbook.cn/gitchat/columns?page=" + currentPage;
        var options = {
            url: requestUrl,
            headers: headers,
        }
        request(options, function (error, response, body) {
            if (error === null && response.statusCode == 200) {
                var resObj=JSON.parse(body);
                var columnList = resObj.columnList;
                columnList.forEach(function(item){
                    allCourseIds.push(item._id);
                    allTopicN+=item.columnTopics.length;
                });
                console.clear();
                console.log("获取到的所有课程url总数", allCourseIds.length);
                if (resObj.code === 0 && resObj.isEnd !== true){
                    currentPage++;
                    getAllCourseUrl();
                }
                else{
                    console.clear();
                    console.log("获取到的所有课程url总数", allCourseIds.length);     
                    console.log("所有主题数", allTopicN);
                    allCourseIds.forEach(function(item){
                        submitVIPColomnOrder(item, true);
                    });
                }
            } else {
                console.log("getAllCourseUrl 失败", "\n", currentPage);
            }
        });    
    }
    
    getAllCourseUrl();
    

    相关文章

      网友评论

          本文标题:使用 Node.js 爬取 GitChat 全站课程

          本文链接:https://www.haomeiwen.com/subject/ojdrxctx.html