美文网首页
使用 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