Greasy Fork

Greasy Fork is available in English.

领英导出好友信息

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
    }
})();