Quét sạch dữ liệu của insecam.org bằng Google AppScript
google appscript
2
White

Anh NC viết ngày 30/05/2019

insecam.org là website lưu trữ dữ liệu về các IPCam bị public (The world biggest directory of online surveillance security cameras.)

Để bản đồ hóa dữ liệu IPCam trên toàn thế giới, mình tiến hành xây dựng bot cào dữ liệu.

alt text

Demo : http://findcam.glitch.me/

Kế hoạch

alt text

Dự định xây dựng bot lấy từ ngoài vào trong, tức là theo chuyên mục rồi đi sâu vào từng đường dẫn IPCam chi tiết.

Khi chạy qua từng trang chuyên mục, bot sẽ:

  • Lưu chỉ mục các đường dẫn bài viết chi tiết ( tạm đặt tên là IPCamView) vào table IPCam
  • Lưu các chỉ mục chuyên mục vào table Links
  • Lưu các chỉ mục trang (page) vào table Links
  • Update lại thông tin cho các chỉ mục IPCamView

Các chỉ mục đề được gắn cờ để kiểm tra việc đã được bot chạy qua hay chưa.

  • Nhũng chỉ mục đã được chạy gắn cờ isRun1
  • Những chỉ mục chưa được chạy gắn cờ isRun0

Các function sẽ xây dựng một cách độc lập: đọc dữ liệu lên để fetchData theo cờ isRun.

Việc sử dụng cronjob (trigger) sẽ gây nên bất đồng bộ dữ liệu (trùng dữ liệu). Tiến hành xây dựng một class DB để xử lý dữ liệu.

Thư viện sử dụng : Cheerio (1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0)

Tiến hành

1 . Class DB (Quản lý dữ liệu theo sheet : thêm, sửa, xóa )


function DB(sheetName) {
    this.db = SpreadsheetApp.getActiveSpreadsheet();

    if (sheetName)
        this.sheet = this.db.getSheetByName(sheetName);
}


DB.prototype = {

    set sheetName(sheetName) {
        this.sheet = this.db.getSheetByName(sheetName);
    },

    set header(headerContent) {
        if (this.sheet.getLastRow() == 0) {
            this.add(headerContent);
        }
    },

    getValueByIndex: function (columIndex, value) {
        var index = this.findByIndex(columIndex, value);
        if (index <= 0) return;
        var lastCol = this.sheet.getLastColumn();
        return this.sheet.getRange(index, 1, 1, lastCol).getValues()[0];
    },

    findByIndex: function (columIndex, value) {
        var lastRow = this.sheet.getLastRow(), index = -1;

        if (lastRow > 0) {
            this.sheet.getRange(1, columIndex, lastRow, 1)
                .getValues()
                .forEach(function (item, i) {
                    if (item == value && index == -1) index = i;
                });
        }

        return index + 1;
    },

    hasId: function (id) {
        return this.findId(id) > 0;
    },

    findId: function (id) {
        return this.findByIndex(1, id);
    },

    add: function (rowContents) {
        return this.sheet.appendRow(rowContents);
    },

    update: function (id, rowContents) {
        var index = this.findId(id);
        var lastColumn = this.sheet.getLastColumn();
        if (index > 0) {
            return this.sheet
                .getRange(index, 1, 1, lastColumn)
                .setValues([rowContents]);
        }
    }

}

2 . Helper

Array.prototype.fLabel = function (label) {
    var item = this.find(function (x) { return x.label == label });
    if (!item) return "";
    return item.value;
}

3 . Module IPCam

function IPCam() {
    this.BASE_URL = "https://www.insecam.org";
    this.listId = [];
    var db = new DB();
    db.sheetName = "IPCam";
    db.header = [
        "ID",
        "COUNTRY",
        "COUNTRYCODE",
        "COUNTRY",
        "REGION",
        "CITY",
        "LATITUDE",
        "LONGITUDE",
        "ZIP",
        "TIMEZONE",
        "MANUFACTURER",
        "TAGS",
        "SOURCE",
        "IS_RUN"
    ];
    db.sheetName = "Links";
    db.header = [
        "ID",
        "HREF",
        "TEXT",
        "IS_RUN"
    ];
    if (db.hasId(Utilities.base64Encode("/en/"))) return;
    db.add([
        Utilities.base64Encode("/en/"),
        "/en/",
        "Home page",
        0
    ]);
}

IPCam.prototype = {

    set html(html) {
        this.source = html;
    },

    fetch: function (url) {
        var content = UrlFetchApp.fetch(url);
        var html = content.getContentText();
        this.html = html;
        return Cheerio.load(html);
    },

    // Input: url ipcam
    fetchInDetail: function () {

        var db = new DB();
        db.sheetName = "IPCam";

        var row = db.getValueByIndex(14, 0);

        if (row) {
            var id = row[0];

            var $ = this.fetch("https://www.insecam.org/en/view/" + id + "/");

            var tags = this.source && this.source.match(/addtagset\("(\w+)"\)/gi)
            if (tags) {
                tags = tags.map(function (tag) {
                    tag = tag.match(/addtagset\("(\w+)"\)/);
                    return tag && tag[1];
                });
            }

            var fields = $(".camera-details__row").map(function (i, item) {
                var cell = $(this).find(".camera-details__cell");
                var label = cell.first().text().toUpperCase().replace(/\W+/gi, "");
                var value = cell.first().next().text().trim();
                return { label: label, value: value };
            }).get();

            fields.push({
                label: "TAGS",
                value: tags && tags.join(",") || ""
            });

            fields.push({
                label: "SOURCE",
                value: $("#image0").attr("src") || ""
            });

            db.update(id, [
                id,
                fields.fLabel("COUNTRY"),
                fields.fLabel("COUNTRYCODE"),
                fields.fLabel("COUNTRY"),
                fields.fLabel("REGION"),
                fields.fLabel("CITY"),
                fields.fLabel("LATITUDE"),
                fields.fLabel("LONGITUDE"),
                fields.fLabel("ZIP"),
                "'" + fields.fLabel("TIMEZONE"),
                fields.fLabel("MANUFACTURER"),
                fields.fLabel("TAGS"),
                fields.fLabel("SOURCE"),
                1
            ]);
        }
    },

    creatDetail: function (id) {
        var db = new DB();
        db.sheetName = "IPCam";
        if (db.hasId(id)) return;
        db.add([
            id,
            "", //fields.fLabel("COUNTRY"), 
            "", // fields.fLabel("COUNTRYCODE"), 
            "", // fields.fLabel("COUNTRY"), 
            "", // fields.fLabel("REGION"), 
            "", // fields.fLabel("CITY"), 
            "", // fields.fLabel("LATITUDE"), 
            "", // fields.fLabel("LONGITUDE"), 
            "", // fields.fLabel("ZIP"), 
            "", // fields.fLabel("TIMEZONE"),
            "", // fields.fLabel("MANUFACTURER"),
            "", // fields.fLabel("TAGS"),
            "", // fields.fLabel("SOURCE"),
            0
        ]);

    },

    // Input: page next, category url
    fetchInCategory: function () {

        var vm = this;

        var db = new DB();
        db.sheetName = "Links";

        var row = db.getValueByIndex(4, 0);

        if (row) {
            var idRow = row[0];
            row[3] = 1;
            db.update(idRow, row);
        }

        var slug = row && row[1];
        var url = this.BASE_URL + slug;

        var $ = this.fetch(url);

        // get link view
        $('[href*="/en/view/"]').map(function (i, item) {
            var href = $(item).attr("href"); //Logger.log("Href:" + href);
            var id = href.match(/(\d+)/)[1]; //Logger.log("Id:" + id);
            if (id) { 
                vm.creatDetail(id);
                vm.listId.push(id);
            }
        });

        // get other link
        $('[rel="index"][href^="/en/"]').map(function (i, item) {
            var href = $(item).attr("href");
            var id = Utilities.base64Encode(href);
            if (!db.hasId(id)) {
                var text = $(item).text().trim();
                db.add([id, href, text, 0]);
            };
        });

        // pagenavigator
        if (slug) {
            var f = vm.source.match(/pagenavigator\(("\?page=", ([\d]+), ([\d]+))\);/);
            var from = f && f.length > 3 && f[3], to = f && f.length > 2 && f[2];
            for (var i = from; i <= to; i++) {
                if (i) {
                    var href = slug + "?page=" + i;  // Logger.log("Href:" + href);
                    var id = Utilities.base64Encode(href);
                    if (!db.hasId(id)) {
                        var text = i;
                        db.add([id, href, text, 0]);
                    };
                }
            }
        }

    }
};

4 . WorkFlow (trigger)

// cào chỉ mục chuyên mục, đặt thời gian tái kích hoạt là 1 phút
function workflow() {
    var i = new IPCam();
    i.fetchInCategory();
}

// cào chỉ mục bài viết chi tiết, đặt thời gian tái kích hoạt là 1 phút
function detail() {
    var ip = new IPCam();
    for(var i = 0; i <= 6; i++){
       ip.fetchInDetail();
    }
}

Kết quả

alt text

Bot vẫn đang tiến hành cào dữ liệu.

https://docs.google.com/spreadsheets/d/1dABxJMpDCLSU-MpkQaR6wjXeWdIr3S8WqbRiTkWMjmo/edit?usp=sharing

(*) Bài viết hơi sơ sài, tỉnh ngủ sẽ update lại thêm cho đỡ nhạt hơn, mong quý bạn thông cảm.

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Anh NC

3 bài viết.
38 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
61 12
Nguồn gốc Từ việc thiếu thốn dữ liệu của các nhà số học xung quanh mình. Buộc tôi phải tìm cách giúp họ. Nhưng với đôi tay trắng này, giúp sao đây...
Anh NC viết 3 tháng trước
61 12
White
44 11
Hoài niệm Ngồi một góc nhỏ cafe, lại nhớ về một chút kỷ niệm gì của quá khứ. Một người bạn của quá khứ hay đôi khi là những tin nhắn sến sẩm với b...
Anh NC viết 3 tháng trước
44 11
Bài viết liên quan
White
61 12
Nguồn gốc Từ việc thiếu thốn dữ liệu của các nhà số học xung quanh mình. Buộc tôi phải tìm cách giúp họ. Nhưng với đôi tay trắng này, giúp sao đây...
Anh NC viết 3 tháng trước
61 12
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
3 bài viết.
38 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!