领英导出好友信息

基于领英好友列表界面,导出好友信息 excel

// ==UserScript==
// @name         领英导出好友信息
// @namespace    ༺黑白༻
// @version      1.3
// @description  基于领英好友列表界面,导出好友信息 excel
// @author       Paul
// @connect      *
// @match        *linkedin.com/search/results/people*
// @include      *linkedin.com/search/results/people*
// @match        *linkedin.com/mynetwork/invite-connect/connections/*
// @include      *linkedin.com/mynetwork/invite-connect/connections/*
// @match        *linkedin.com/in/*/detail/contact-info/*
// @include      *linkedin.com/in/*/detail/contact-info/*
// @require      https://lib.baomitu.com/vue/2.6.11/vue.js
// @require      https://lib.baomitu.com/element-ui/2.12.0/index.js
// @resource elementui https://lib.baomitu.com/element-ui/2.12.0/theme-chalk/index.css
// @require      https://cdn.staticfile.org/xlsx/0.15.1/xlsx.core.min.js
// @grant        GM_addStyle
// @grant        GM_openInTab
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_getResourceText
// @run-at       document-end
// @noframes     true
// @license      MIT
// ==/UserScript==
(function () {
    'use strict';

    class AntBase {
        constructor() {
            this.queueStorageName = 'local_name';
            this.ArrayPrototype = Array.prototype;
        }

        appendHtml(html, dom = document.body) {
            var temp = document.createElement('div');
            temp.innerHTML = html;
            var frag = document.createDocumentFragment();
            frag.appendChild(temp.firstElementChild);
            dom.appendChild(frag);
        }

        execByPromiseAsync(scope, fn) {
            var args = Array.prototype.slice.call(arguments);
            args.splice(0, 2)
            return new Promise((resolve, reject) => {
                args.unshift({
                    resolve: resolve,
                    reject: reject
                });
                fn.apply(scope, args);
            });
        }

        waitAsync(chkFn, ts = 1000) {
            var hasChkFn = typeof chkFn == 'function';
            var setTimeoutFn = hasChkFn ?
                async (dfd) => {
                    var chkResult = chkFn();
                    var resolve = chkResult == null ? false : typeof chkResult == 'object' ? chkResult.success : chkResult;
                    if (resolve) {
                        dfd.resolve(chkResult);
                    }
                    else setTimeout(setTimeoutFn, ts, dfd);
                }
                : (dfd) => {
                    setTimeout(() => dfd.resolve(), ts);
                }
            return this.execByPromiseAsync(this, setTimeoutFn);
        }

        sleepAsync(ts = 1000) {
            return this.waitAsync(null, ts);
        }

        getRandom(n, m) {
            return parseInt(Math.random() * (m - n + 1) + n);
        }

        log(msg) {
            console.log(msg);
        }

        appendURLParam(url, name, val) {
            if (typeof url != 'string' || url.length <= 0) return url;
            if (url.indexOf('?') == -1) {
                url += "?"
            } else {
                url += "&"
            }
            return url += `${name}=${val}`;
        }

        getURLParam(name) {
            var query = unsafeWindow.location.search.substring(1);
            var vars = query.split("&");
            for (var i = 0; i < vars.length; i++) {
                var pair = vars[i].split("=");
                if (pair[0] == name) { return pair[1]; }
            }
            return "";
        }

        appendURLStorageParam(url, val) {
            return this.appendURLParam(url, this.queueStorageName, val);
        }

        getURLStorageParam() {
            return this.getURLParam(this.queueStorageName);
        }

        fireKeyEvent(el, evtType, keyCode) {
            var evtObj;
            if (document.createEvent) {
                if (unsafeWindow.KeyEvent) {//firefox 浏览器下模拟事件
                    evtObj = document.createEvent('KeyEvents');
                    evtObj.initKeyEvent(evtType, true, true, unsafeWindow, true, false, false, false, keyCode, 0);
                } else {//chrome 浏览器下模拟事件
                    evtObj = document.createEvent('UIEvents');
                    evtObj.initUIEvent(evtType, true, true, unsafeWindow, 1);

                    delete evtObj.keyCode;
                    if (typeof evtObj.keyCode === "undefined") {//为了模拟keycode
                        Object.defineProperty(evtObj, "keyCode", { value: keyCode });
                    } else {
                        evtObj.key = String.fromCharCode(keyCode);
                    }

                    if (typeof evtObj.ctrlKey === 'undefined') {//为了模拟ctrl键
                        Object.defineProperty(evtObj, "ctrlKey", { value: true });
                    } else {
                        evtObj.ctrlKey = true;
                    }
                }
                el.dispatchEvent(evtObj);

            } else if (document.createEventObject) {//IE 浏览器下模拟事件
                evtObj = document.createEventObject();
                evtObj.keyCode = keyCode
                el.fireEvent('on' + evtType, evtObj);
            }
        }

        find(source, fn) {
            return this.ArrayPrototype.find.call(source, fn);
        }

        filter(source, fn) {
            return this.ArrayPrototype.filter.call(source, fn);
        }
    }

    class TMBase extends AntBase {
        constructor() {
            super();
            this.GM_getValue_old = GM_getValue;
            this.GM_setValue = GM_setValue;
            this.GM_deleteValue = GM_deleteValue;
            this.GM_addValueChangeListener = GM_addValueChangeListener;
            this.GM_openInTab = GM_openInTab;
            this.GM_removeValueChangeListener = GM_removeValueChangeListener;
            this.GM_addStyle = GM_addStyle;
            this.GM_getResourceText = GM_getResourceText;
        }

        GM_getValue(name, defaultValue = '') {
            return this.GM_getValue_old(name, defaultValue);
        }
    }

    class PersonListAnt extends TMBase {
        constructor() {
            super();

            this.App = null;

            this.GM_addStyle(this.GM_getResourceText("elementui"));
            // 加载 element 字体
            this.GM_addStyle('@font-face{font-family:element-icons;src:url(https://lib.baomitu.com/element-ui/2.12.0/theme-chalk/fonts/element-icons.woff) format("woff"),url(https://lib.baomitu.com/element-ui/2.12.0/theme-chalk/fonts/element-icons.ttf) format("truetype");font-weight:400;font-display:"auto";font-style:normal}');

            var id = `vue${Date.now()}`;

            this.GM_addStyle(`

.${id}-drawerswitch{
    position: fixed;
    bottom: 0;
    left: 0;
    height: 60px;
    width: 60px;
    z-index: 999;
    border-radius: 50%;
    background-color: #fff; }


`);

            // 创建Vue承载容器
            this._buildHtml(id);

            //  // 创建Vue
            this.App = this._buildVue();
            this.App.instance = this;
            this.App.$mount(`#${id}`);

        }

        _buildHtml(id) {
            this.appendHtml(`
                <div id="${id}">
                    <el-button class="${id}-drawerswitch" @click="drawer = true" ><i class="el-icon-thumb"></i></el-button>
                    <el-drawer
                      title="抓取用户信息"
                      :visible.sync="drawer"
                      :close-on-press-escape="false"
                      :before-close="closeDrawer"
                      direction="ltr"
                      size="20%"
                        >
                      <el-container>
                        <el-main>
                            <el-row>
                                 <el-col :span="8"><el-button type="primary" @click="refresh">刷新</el-button></el-col>
                            </el-row>
                            <el-row style="margin-top:10px;">
                                 <el-col :span="8"><el-button type="primary" @click="starting" :disabled="!canExecute" >开始抓取</el-button></el-col>
                                 <el-col :span="5">&nbsp;</el-col>
                                 <el-col :span="8"><el-button type="primary" @click="stop" :disabled="!this.isRunning" >停止</el-button></el-col>
                            </el-row>
                            <el-row style="margin-top:10px;">
                                <el-col :span="8">进度(共{{progressTotal}} 个):</el-col>
                                <el-col :span="14"><el-progress :text-inside="true" :stroke-width="20" :percentage="progress"></el-progress></el-col>
                            </el-row>
                            <el-row style="margin-top:10px;">
                                <el-col :span="24"><el-button type="primary" :disabled="!canExprot" @click="exportData" >导出</el-button></el-col>
                            </el-row>
                        </el-main>
                      </el-container>
                    </el-drawer>
                </div>
            `)
        }

        _buildVue() {
            return new Vue({
                data() {
                    this.currentDictionary = {};
                    this.userStop = false;
                    this.excelCfg = {
                        'firstName': 'firstName',
                        'lastName': 'lastName',
                        '历任公司': 'company',
                        '职位': 'headline',
                        '地址': 'locationName',
                        '邮箱': 'Email'
                    };

                    return {
                        drawer: false,
                        dataLinks: [],
                        isRunning: false,
                        progressTotal: 0,
                        progressIdx: 0,
                    };
                },
                computed: {
                    progress: function () {
                        if (this.progressTotal <= 0) return 0;
                        return Math.round((this.progressIdx / this.progressTotal) * 100);
                    },
                    canExecute() {
                        return !this.isRunning && this.progressIdx != this.progressTotal;
                    },
                    canExprot() {
                        return !this.isRunning && this.progressIdx > 0 ;
                    }
                },
                methods: {
                    refresh() {
                        var links = document.querySelectorAll('.mn-connection-card a.mn-connection-card__link');
                        links.forEach(item => {
                            var href = item.getAttribute('href');
                            if (!this.currentDictionary.hasOwnProperty(href)) {
                                this.currentDictionary[href] = null;
                            }
                            if (this.currentDictionary[href] === null) {
                                this.currentDictionary[href] = {};
                                this.dataLinks.push({ href });
                                this.progressTotal++;
                            }
                        });
                    },
                    closeDrawer(done) {
                        if (this.isRunning) return;
                        done();
                    },
                    // 停止
                    stop() {
                        this.userStop = true;
                    },
                    // 开始抓取
                    starting() {
                        if (this.dataLinks.length <= 0) {
                            this.$message.error('当前没有可执行的记录哦~~');
                            return;
                        }
                        this.isRunning = true;
                        this.userStop = false;
                        this.singleExecuteAsync();
                    },
                    async singleExecuteAsync() {
                        var obs = false;
                        if (!this.userStop) {
                            obs = this.dataLinks.shift();
                        }
                        if (!obs) {
                            this.isRunning = false;
                            this.$message({
                                message: this.userStop ? '用户停止' : '执行完成!',
                                type: 'success'
                            });
                            return;
                        }
                        var href = obs.href;
                        if (Object.keys(this.currentDictionary[href]).length == 0) {
                            this.currentDictionary[href] = await this.openLoadPageAsync(`${unsafeWindow.location.origin}${href}detail/contact-info/`);
                            this.progressIdx++;
                        }
                        setTimeout(this.singleExecuteAsync.bind(this), 100);
                    },
                    openLoadPageAsync(link) {
                        return this.instance.execByPromiseAsync(this, dfd => {
                            var index = 0;
                            var name = `${Date.now()}_${index}`,
                                listennerName = `listener_${name}`,
                                listennerTabName = `listener_tab_${name}`;

                            link = this.instance.appendURLParam(link, this.instance.queueStorageName, name);
                            this[listennerName] = this.instance.GM_addValueChangeListener(name, this._valueChangeListener.bind(this, dfd, listennerName, listennerTabName));
                            this[listennerTabName] = this.instance.GM_openInTab(link);
                        });
                    },
                    _valueChangeListener(dfd, listennerName, listennerTabName, name, old_v, new_v, remote) {
                        if (new_v && remote) {
                            this.instance.log(new_v);
                            this.instance.GM_deleteValue(name);
                            this.instance.GM_removeValueChangeListener(this[listennerName]);
                            delete this[listennerName];
                            if (this[listennerTabName]) this[listennerTabName].close();
                            delete this[listennerTabName];
                            dfd.resolve(JSON.parse(new_v));
                        }
                    },
                    //调整公司数据
                    adjustCompanyData: function (key,dataJson) {
                        var companyArray =[],values = [];
                        try{
                         companyArray =  JSON.parse(dataJson);
                        }catch(e){
                            console.log(key,'error');
                        }
                        companyArray.sort((item1, item2) => {
                            if (!item1.hasOwnProperty('dateRange')) return -1;
                            if (!item2.hasOwnProperty('dateRange')) return 1;

                            if (!item1.dateRange.hasOwnProperty('start')) return -1;
                            if (!item2.dateRange.hasOwnProperty('start')) return 1;

                            if (!item1.dateRange.start.hasOwnProperty('year')) return -1;
                            if (!item2.dateRange.start.hasOwnProperty('year')) return -1;

                            var styear = item1.dateRange.start.year - item2.dateRange.start.year;
                            if (styear != 0) return styear;

                            if (!item1.dateRange.start.hasOwnProperty('month')) return -1;
                            if (!item2.dateRange.start.hasOwnProperty('month')) return -1;

                            var stmonth = item1.dateRange.start.month - item2.dateRange.start.month;
                            if (stmonth != 0) return stmonth;

                            if (!item1.dateRange.hasOwnProperty('end')) return -1;
                            if (!item2.dateRange.hasOwnProperty('end')) return 1;

                            if (!item1.dateRange.end.hasOwnProperty('year')) return -1;
                            if (!item2.dateRange.end.hasOwnProperty('year')) return -1;

                            var etyear = item1.dateRange.end.year - item2.dateRange.end.year;
                            if (etyear != 0) return etyear;

                            if (!item1.dateRange.end.hasOwnProperty('month')) return -1;
                            if (!item2.dateRange.end.hasOwnProperty('month')) return -1;

                            var etmonth = item1.dateRange.end.month - item2.dateRange.end.month;
                            if (etmonth != 0) return etmonth;

                            return 0;
                        });
                        companyArray.forEach(function (item) {
                            var str = "头衔:";
                            if (item.title) {
                                str += item.title;
                            }
                            str += "\n";
                            str += "公司名称:"
                            if (item.companyName) {
                                str += item.companyName;
                            }
                            str += "\n";
                            str += "任期:";
                            if (item.dateRange) {
                                if (item.dateRange.start) {
                                    if (item.dateRange.start.year) {
                                        str += item.dateRange.start.year;
                                    } else {
                                        str += "****";
                                    }
                                    str += "-";
                                    if (item.dateRange.start.month) {
                                        str += item.dateRange.start.month;
                                    } else {
                                        str += "**";
                                    }
                                } else {
                                    str += "****-**";
                                }
                                str += " 至 "
                                if (item.dateRange.end) {
                                    if (item.dateRange.end.year) {
                                        str += item.dateRange.end.year;
                                    } else {
                                        str += "****";
                                    }
                                    str += "-";
                                    if (item.dateRange.end.month) {
                                        str += item.dateRange.end.month;
                                    } else {
                                        str += "**";
                                    }
                                } else {
                                    str += "****-**";
                                }
                            }
                            str += "\n-------------------------------";

                            values.push(str);
                        });
                        return values.join('\n');
                    },
                    exportData: function () {
                        var aoa = [], item, fileds = [], name = [], i, len, temp, key, j, jlen, values, filedName;
                        for (item in this.excelCfg) {
                            fileds.push(this.excelCfg[item]);
                            name.push(item);
                        }
                        aoa.push(name);
                        for (key in this.currentDictionary) {
                            temp = this.currentDictionary[key];
                            if (!temp || typeof (temp) != 'object' || Object.keys(temp).length <= 0) continue;
                            values = [];
                            for (j = 0, jlen = fileds.length; j < jlen; j++) {
                                filedName = fileds[j];
                                values.push(filedName == this.excelCfg["历任公司"] ? this.adjustCompanyData(key,temp[filedName]) : temp[filedName]);
                            }
                            aoa.push(values);
                        }
                        this.exportExcel(aoa);
                    },
                    exportExcel: function (aoa) {
                        var sheet = XLSX.utils.aoa_to_sheet(aoa);
                        this.openDownloadDialog(this.sheet2blob(sheet), "导出数据" + (+new Date()) + '.xlsx');
                    },
                    sheet2blob: function (sheet, sheetName) {
                        sheetName = sheetName || 'sheet1';
                        var workbook = {
                            SheetNames: [sheetName],
                            Sheets: {}
                        };
                        workbook.Sheets[sheetName] = sheet;
                        // 生成excel的配置项
                        var wopts = {
                            bookType: 'xlsx', // 要生成的文件类型
                            bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
                            type: 'binary'
                        };
                        var wbout = XLSX.write(workbook, wopts);
                        var blob = new Blob([s2ab(wbout)], { type: "application/octet-stream" });
                        // 字符串转ArrayBuffer
                        function s2ab(s) {
                            var buf = new ArrayBuffer(s.length);
                            var view = new Uint8Array(buf);
                            for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
                            return buf;
                        }
                        return blob;
                    },
                    /**
                       * 通用的打开下载对话框方法,没有测试过具体兼容性
                       * @param url 下载地址,也可以是一个blob对象,必选
                       * @param saveName 保存文件名,可选
                       */
                    openDownloadDialog: function (url, saveName) {
                        if (typeof url == 'object' && url instanceof Blob) {
                            url = URL.createObjectURL(url); // 创建blob地址
                        }
                        var aLink = document.createElement('a');
                        aLink.href = url;
                        aLink.download = saveName || ''; // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
                        var event;
                        if (window.MouseEvent) event = new MouseEvent('click');
                        else {
                            event = document.createEvent('MouseEvents');
                            event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
                        }
                        aLink.dispatchEvent(event);
                    },
                }
            });
        }
    }

    class UserInfoAnt extends TMBase {
        constructor() {
            super();
            var name = this.getURLParam(this.queueStorageName);
            this.getInfoAsync().then(rs => {
                this.log("name:" + name);
                this.log(rs);
                // 存入数据
                this.GM_setValue(name, JSON.stringify(rs));
            });
        }

        getInfoAsync() {
            return this.execByPromiseAsync(this, this._getInfoAsync);
        }

        async _getInfoAsync(dfd) {
            this.log("_getInfoAsync");
            var chkResultInfo = await this.waitAsync(() => {
                var domList = document.querySelectorAll('h2');
                var chkResult = this.find(domList, item => item.innerHTML.indexOf('Contact Info') != -1);
                if (!chkResult) return false;
                return { success: true, dom: chkResult }
            }, 500);
            this.log(chkResultInfo);
            var findObj = {};
            // /voyager/api/identity/dash/profiles
            var findDomCode = this.find(document.querySelectorAll('code'), item => item.innerHTML.indexOf('/voyager/api/identity/dash/profiles') != -1 && item.innerHTML.indexOf('FullProfileWithEntities') != -1);
            if (findDomCode) {
                // 找出
                var findCodeObj = JSON.parse(findDomCode.innerHTML);
                var targetCodeDom = document.getElementById(findCodeObj.body);
                if (targetCodeDom) {
                    var tempObj = JSON.parse(targetCodeDom.innerHTML);
                    //  com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities
                    var fullProfileWithEntities = this.find(tempObj.included, item => item.hasOwnProperty('$recipeTypes') && item['$recipeTypes'][0] == 'com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities');
                    this.log(fullProfileWithEntities);
                    if (fullProfileWithEntities) {
                        Object.assign(findObj, {
                            firstName: fullProfileWithEntities.firstName,
                            lastName: fullProfileWithEntities.lastName,
                            headline: fullProfileWithEntities.headline,
                            locationName: fullProfileWithEntities.locationName
                        });
                    }
                    // com.linkedin.voyager.dash.deco.identity.profile.FullProfilePosition
                    var fullProfilePositionArray = this.filter(tempObj.included, item => item.hasOwnProperty('$recipeTypes') && item['$recipeTypes'][0] == 'com.linkedin.voyager.dash.deco.identity.profile.FullProfilePosition');
                    this.log(fullProfilePositionArray);
                    var companys = [];
                    fullProfilePositionArray.forEach(position => {
                        var dateRange = position.dateRange || {};
                        companys.push({
                            title: position.title,
                            companyName: position.companyName,
                            dateRange: dateRange
                        });
                    });
                    if (companys.length > 0) findObj['company'] = JSON.stringify(companys);

                };
            }
            var findEmailDom = this.find(chkResultInfo.dom.parentElement.querySelectorAll('a'), item => item.getAttribute('href').indexOf('mailto:') != -1);
            findObj.Email = findEmailDom ? findEmailDom.innerText : "";
            dfd.resolve(findObj);
        }
    }

    unsafeWindow.Linkin = {};
    if (unsafeWindow.location.href.toLowerCase().indexOf('/detail/contact-info/') != -1) {
        unsafeWindow.Linkin.AutoCollectionAnt = new UserInfoAnt();
    } else {
        unsafeWindow.Linkin.AutoCollectionAnt = new PersonListAnt();
    }
})();