🏠 返回首頁 

Greasy Fork is available in English.

#英导出好友信息

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


安装此脚本?
  1. // ==UserScript==
  2. // @name #英导出好友信息
  3. // @namespace ༺黑白༻
  4. // @version 1.3
  5. // @description 基于#英好友列表界面,导出好友信息 excel
  6. // @author Paul
  7. // @connect *
  8. // @match *linkedin.com/search/r###lts/people*
  9. // @include *linkedin.com/search/r###lts/people*
  10. // @match *linkedin.com/mynetwork/invite-connect/connections/*
  11. // @include *linkedin.com/mynetwork/invite-connect/connections/*
  12. // @match *linkedin.com/in/*/detail/contact-info/*
  13. // @include *linkedin.com/in/*/detail/contact-info/*
  14. // @require https://lib.baomitu.com/vue/2.6.11/vue.js
  15. // @require https://lib.baomitu.com/element-ui/2.12.0/index.js
  16. // @resource elementui https://lib.baomitu.com/element-ui/2.12.0/theme-chalk/index.css
  17. // @require https://cdn.staticfile.org/xlsx/0.15.1/xlsx.core.min.js
  18. // @grant GM_addStyle
  19. // @grant GM_openInTab
  20. // @grant GM_addValueChangeListener
  21. // @grant GM_removeValueChangeListener
  22. // @grant GM_setValue
  23. // @grant GM_getValue
  24. // @grant GM_deleteValue
  25. // @grant GM_getResourceText
  26. // @run-at document-end
  27. // @noframes true
  28. // @license MIT
  29. // ==/UserScript==
  30. (function () {
  31. 'use strict';
  32. class AntBase {
  33. constructor() {
  34. this.queueStorageName = 'local_name';
  35. this.ArrayPrototype = Array.prototype;
  36. }
  37. appendHtml(html, dom = document.body) {
  38. var temp = document.createElement('div');
  39. temp.innerHTML = html;
  40. var frag = document.createDocumentFragment();
  41. frag.appendChild(temp.firstElementChild);
  42. dom.appendChild(frag);
  43. }
  44. execByPromiseAsync(scope, fn) {
  45. var args = Array.prototype.slice.call(arguments);
  46. args.splice(0, 2)
  47. return new Promise((resolve, reject) => {
  48. args.unshift({
  49. resolve: resolve,
  50. reject: reject
  51. });
  52. fn.apply(scope, args);
  53. });
  54. }
  55. waitAsync(chkFn, ts = 1000) {
  56. var hasChkFn = typeof chkFn == 'function';
  57. var setTimeoutFn = hasChkFn ?
  58. async (dfd) => {
  59. var chkR###lt = chkFn();
  60. var resolve = chkR###lt == null ? false : typeof chkR###lt == 'object' ? chkR###lt.success : chkR###lt;
  61. if (resolve) {
  62. dfd.resolve(chkR###lt);
  63. }
  64. else setTimeout(setTimeoutFn, ts, dfd);
  65. }
  66. : (dfd) => {
  67. setTimeout(() => dfd.resolve(), ts);
  68. }
  69. return this.execByPromiseAsync(this, setTimeoutFn);
  70. }
  71. sleepAsync(ts = 1000) {
  72. return this.waitAsync(null, ts);
  73. }
  74. getRandom(n, m) {
  75. return parseInt(Math.random() * (m - n + 1) + n);
  76. }
  77. log(msg) {
  78. console.log(msg);
  79. }
  80. appendURLParam(url, name, val) {
  81. if (typeof url != 'string' || url.length <= 0) return url;
  82. if (url.indexOf('?') == -1) {
  83. url += "?"
  84. } else {
  85. url += "&"
  86. }
  87. return url += `${name}=${val}`;
  88. }
  89. getURLParam(name) {
  90. var query = unsafeWindow.location.search.substring(1);
  91. var vars = query.split("&");
  92. for (var i = 0; i < vars.length; i++) {
  93. var pair = vars[i].split("=");
  94. if (pair[0] == name) { return pair[1]; }
  95. }
  96. return "";
  97. }
  98. appendURLStorageParam(url, val) {
  99. return this.appendURLParam(url, this.queueStorageName, val);
  100. }
  101. getURLStorageParam() {
  102. return this.getURLParam(this.queueStorageName);
  103. }
  104. fireKeyEvent(el, evtType, keyCode) {
  105. var evtObj;
  106. if (document.createEvent) {
  107. if (unsafeWindow.KeyEvent) {//firefox 浏览器下模拟事件
  108. evtObj = document.createEvent('KeyEvents');
  109. evtObj.initKeyEvent(evtType, true, true, unsafeWindow, true, false, false, false, keyCode, 0);
  110. } else {//chrome 浏览器下模拟事件
  111. evtObj = document.createEvent('UIEvents');
  112. evtObj.initUIEvent(evtType, true, true, unsafeWindow, 1);
  113. delete evtObj.keyCode;
  114. if (typeof evtObj.keyCode === "undefined") {//为了模拟keycode
  115. Object.defineProperty(evtObj, "keyCode", { value: keyCode });
  116. } else {
  117. evtObj.key = String.fromCharCode(keyCode);
  118. }
  119. if (typeof evtObj.ctrlKey === 'undefined') {//为了模拟ctrl键
  120. Object.defineProperty(evtObj, "ctrlKey", { value: true });
  121. } else {
  122. evtObj.ctrlKey = true;
  123. }
  124. }
  125. el.dispatchEvent(evtObj);
  126. } else if (document.createEventObject) {//IE 浏览器下模拟事件
  127. evtObj = document.createEventObject();
  128. evtObj.keyCode = keyCode
  129. el.fireEvent('on' + evtType, evtObj);
  130. }
  131. }
  132. find(source, fn) {
  133. return this.ArrayPrototype.find.call(source, fn);
  134. }
  135. filter(source, fn) {
  136. return this.ArrayPrototype.filter.call(source, fn);
  137. }
  138. }
  139. class TMBase extends AntBase {
  140. constructor() {
  141. super();
  142. this.GM_getValue_old = GM_getValue;
  143. this.GM_setValue = GM_setValue;
  144. this.GM_deleteValue = GM_deleteValue;
  145. this.GM_addValueChangeListener = GM_addValueChangeListener;
  146. this.GM_openInTab = GM_openInTab;
  147. this.GM_removeValueChangeListener = GM_removeValueChangeListener;
  148. this.GM_addStyle = GM_addStyle;
  149. this.GM_getResourceText = GM_getResourceText;
  150. }
  151. GM_getValue(name, defaultValue = '') {
  152. return this.GM_getValue_old(name, defaultValue);
  153. }
  154. }
  155. class PersonListAnt extends TMBase {
  156. constructor() {
  157. super();
  158. this.App = null;
  159. this.GM_addStyle(this.GM_getResourceText("elementui"));
  160. // 加载 element 字体
  161. 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}');
  162. var id = `vue${Date.now()}`;
  163. this.GM_addStyle(`
  164. .${id}-drawerswitch{
  165. position: fixed;
  166. bottom: 0;
  167. left: 0;
  168. height: 60px;
  169. width: 60px;
  170. z-index: 999;
  171. border-radius: 50%;
  172. background-color: #fff; }
  173. `);
  174. // 创建Vue承载容器
  175. this._buildHtml(id);
  176. // // 创建Vue
  177. this.App = this._buildVue();
  178. this.App.instance = this;
  179. this.App.$mount(`#${id}`);
  180. }
  181. _buildHtml(id) {
  182. this.appendHtml(`
  183. <div id="${id}">
  184. <el-button class="${id}-drawerswitch" @click="drawer = true" ><i class="el-icon-thumb"></i></el-button>
  185. <el-drawer
  186. title="抓取用户信息"
  187. :visible.sync="drawer"
  188. :close-on-press-escape="false"
  189. :before-close="closeDrawer"
  190. direction="ltr"
  191. size="20%"
  192. >
  193. <el-container>
  194. <el-main>
  195. <el-row>
  196. <el-col :span="8"><el-button type="primary" @click="refresh">刷新</el-button></el-col>
  197. </el-row>
  198. <el-row style="margin-top:10px;">
  199. <el-col :span="8"><el-button type="primary" @click="starting" :disabled="!canExecute" >开始抓取</el-button></el-col>
  200. <el-col :span="5">&nbsp;</el-col>
  201. <el-col :span="8"><el-button type="primary" @click="stop" :disabled="!this.isRunning" >停止</el-button></el-col>
  202. </el-row>
  203. <el-row style="margin-top:10px;">
  204. <el-col :span="8">进度(共{{progressTotal}} 个):</el-col>
  205. <el-col :span="14"><el-progress :text-inside="true" :stroke-width="20" :percentage="progress"></el-progress></el-col>
  206. </el-row>
  207. <el-row style="margin-top:10px;">
  208. <el-col :span="24"><el-button type="primary" :disabled="!canExprot" @click="exportData" >导出</el-button></el-col>
  209. </el-row>
  210. </el-main>
  211. </el-container>
  212. </el-drawer>
  213. </div>
  214. `)
  215. }
  216. _buildVue() {
  217. return new Vue({
  218. data() {
  219. this.currentDictionary = {};
  220. this.userStop = false;
  221. this.excelCfg = {
  222. 'firstName': 'firstName',
  223. 'lastName': 'lastName',
  224. '历任公司': 'company',
  225. '职位': 'headline',
  226. '地址': 'locationName',
  227. '邮箱': 'Email'
  228. };
  229. return {
  230. drawer: false,
  231. dataLinks: [],
  232. isRunning: false,
  233. progressTotal: 0,
  234. progressIdx: 0,
  235. };
  236. },
  237. computed: {
  238. progress: function () {
  239. if (this.progressTotal <= 0) return 0;
  240. return Math.round((this.progressIdx / this.progressTotal) * 100);
  241. },
  242. canExecute() {
  243. return !this.isRunning && this.progressIdx != this.progressTotal;
  244. },
  245. canExprot() {
  246. return !this.isRunning && this.progressIdx > 0 ;
  247. }
  248. },
  249. methods: {
  250. refresh() {
  251. var links = document.querySelectorAll('.mn-connection-card a.mn-connection-card__link');
  252. links.forEach(item => {
  253. var href = item.getAttribute('href');
  254. if (!this.currentDictionary.hasOwnProperty(href)) {
  255. this.currentDictionary[href] = null;
  256. }
  257. if (this.currentDictionary[href] === null) {
  258. this.currentDictionary[href] = {};
  259. this.dataLinks.push({ href });
  260. this.progressTotal++;
  261. }
  262. });
  263. },
  264. closeDrawer(done) {
  265. if (this.isRunning) return;
  266. done();
  267. },
  268. // 停止
  269. stop() {
  270. this.userStop = true;
  271. },
  272. // 开始抓取
  273. starting() {
  274. if (this.dataLinks.length <= 0) {
  275. this.$message.error('当前没有可执行的记录哦~~');
  276. return;
  277. }
  278. this.isRunning = true;
  279. this.userStop = false;
  280. this.singleExecuteAsync();
  281. },
  282. async singleExecuteAsync() {
  283. var obs = false;
  284. if (!this.userStop) {
  285. obs = this.dataLinks.shift();
  286. }
  287. if (!obs) {
  288. this.isRunning = false;
  289. this.$message({
  290. message: this.userStop ? '用户停止' : '执行完成!',
  291. type: 'success'
  292. });
  293. return;
  294. }
  295. var href = obs.href;
  296. if (Object.keys(this.currentDictionary[href]).length == 0) {
  297. this.currentDictionary[href] = await this.openLoadPageAsync(`${unsafeWindow.location.origin}${href}detail/contact-info/`);
  298. this.progressIdx++;
  299. }
  300. setTimeout(this.singleExecuteAsync.bind(this), 100);
  301. },
  302. openLoadPageAsync(link) {
  303. return this.instance.execByPromiseAsync(this, dfd => {
  304. var index = 0;
  305. var name = `${Date.now()}_${index}`,
  306. listennerName = `listener_${name}`,
  307. listennerTabName = `listener_tab_${name}`;
  308. link = this.instance.appendURLParam(link, this.instance.queueStorageName, name);
  309. this[listennerName] = this.instance.GM_addValueChangeListener(name, this._valueChangeListener.bind(this, dfd, listennerName, listennerTabName));
  310. this[listennerTabName] = this.instance.GM_openInTab(link);
  311. });
  312. },
  313. _valueChangeListener(dfd, listennerName, listennerTabName, name, old_v, new_v, remote) {
  314. if (new_v && remote) {
  315. this.instance.log(new_v);
  316. this.instance.GM_deleteValue(name);
  317. this.instance.GM_removeValueChangeListener(this[listennerName]);
  318. delete this[listennerName];
  319. if (this[listennerTabName]) this[listennerTabName].close();
  320. delete this[listennerTabName];
  321. dfd.resolve(JSON.parse(new_v));
  322. }
  323. },
  324. //调整公司数据
  325. adjustCompanyData: function (key,dataJson) {
  326. var companyArray =[],values = [];
  327. try{
  328. companyArray = JSON.parse(dataJson);
  329. }catch(e){
  330. console.log(key,'error');
  331. }
  332. companyArray.sort((item1, item2) => {
  333. if (!item1.hasOwnProperty('dateRange')) return -1;
  334. if (!item2.hasOwnProperty('dateRange')) return 1;
  335. if (!item1.dateRange.hasOwnProperty('start')) return -1;
  336. if (!item2.dateRange.hasOwnProperty('start')) return 1;
  337. if (!item1.dateRange.start.hasOwnProperty('year')) return -1;
  338. if (!item2.dateRange.start.hasOwnProperty('year')) return -1;
  339. var styear = item1.dateRange.start.year - item2.dateRange.start.year;
  340. if (styear != 0) return styear;
  341. if (!item1.dateRange.start.hasOwnProperty('month')) return -1;
  342. if (!item2.dateRange.start.hasOwnProperty('month')) return -1;
  343. var stmonth = item1.dateRange.start.month - item2.dateRange.start.month;
  344. if (stmonth != 0) return stmonth;
  345. if (!item1.dateRange.hasOwnProperty('end')) return -1;
  346. if (!item2.dateRange.hasOwnProperty('end')) return 1;
  347. if (!item1.dateRange.end.hasOwnProperty('year')) return -1;
  348. if (!item2.dateRange.end.hasOwnProperty('year')) return -1;
  349. var etyear = item1.dateRange.end.year - item2.dateRange.end.year;
  350. if (etyear != 0) return etyear;
  351. if (!item1.dateRange.end.hasOwnProperty('month')) return -1;
  352. if (!item2.dateRange.end.hasOwnProperty('month')) return -1;
  353. var etmonth = item1.dateRange.end.month - item2.dateRange.end.month;
  354. if (etmonth != 0) return etmonth;
  355. return 0;
  356. });
  357. companyArray.forEach(function (item) {
  358. var str = "头衔:";
  359. if (item.title) {
  360. str += item.title;
  361. }
  362. str += "\n";
  363. str += "公司名称:"
  364. if (item.companyName) {
  365. str += item.companyName;
  366. }
  367. str += "\n";
  368. str += "任期:";
  369. if (item.dateRange) {
  370. if (item.dateRange.start) {
  371. if (item.dateRange.start.year) {
  372. str += item.dateRange.start.year;
  373. } else {
  374. str += "****";
  375. }
  376. str += "-";
  377. if (item.dateRange.start.month) {
  378. str += item.dateRange.start.month;
  379. } else {
  380. str += "**";
  381. }
  382. } else {
  383. str += "****-**";
  384. }
  385. str += " 至 "
  386. if (item.dateRange.end) {
  387. if (item.dateRange.end.year) {
  388. str += item.dateRange.end.year;
  389. } else {
  390. str += "****";
  391. }
  392. str += "-";
  393. if (item.dateRange.end.month) {
  394. str += item.dateRange.end.month;
  395. } else {
  396. str += "**";
  397. }
  398. } else {
  399. str += "****-**";
  400. }
  401. }
  402. str += "\n-------------------------------";
  403. values.push(str);
  404. });
  405. return values.join('\n');
  406. },
  407. exportData: function () {
  408. var aoa = [], item, fileds = [], name = [], i, len, temp, key, j, jlen, values, filedName;
  409. for (item in this.excelCfg) {
  410. fileds.push(this.excelCfg[item]);
  411. name.push(item);
  412. }
  413. aoa.push(name);
  414. for (key in this.currentDictionary) {
  415. temp = this.currentDictionary[key];
  416. if (!temp || typeof (temp) != 'object' || Object.keys(temp).length <= 0) continue;
  417. values = [];
  418. for (j = 0, jlen = fileds.length; j < jlen; j++) {
  419. filedName = fileds[j];
  420. values.push(filedName == this.excelCfg["历任公司"] ? this.adjustCompanyData(key,temp[filedName]) : temp[filedName]);
  421. }
  422. aoa.push(values);
  423. }
  424. this.exportExcel(aoa);
  425. },
  426. exportExcel: function (aoa) {
  427. var sheet = XLSX.utils.aoa_to_sheet(aoa);
  428. this.openDownloadDialog(this.sheet2blob(sheet), "导出数据" + (+new Date()) + '.xlsx');
  429. },
  430. sheet2blob: function (sheet, sheetName) {
  431. sheetName = sheetName || 'sheet1';
  432. var workbook = {
  433. SheetNames: [sheetName],
  434. Sheets: {}
  435. };
  436. workbook.Sheets[sheetName] = sheet;
  437. // 生成excel的配置项
  438. var wopts = {
  439. bookType: 'xlsx', // 要生成的文件类型
  440. bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
  441. type: 'binary'
  442. };
  443. var wbout = XLSX.write(workbook, wopts);
  444. var blob = new Blob([s2ab(wbout)], { type: "application/octet-stream" });
  445. // 字符串转ArrayBuffer
  446. function s2ab(s) {
  447. var buf = new ArrayBuffer(s.length);
  448. var view = new Uint8Array(buf);
  449. for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
  450. return buf;
  451. }
  452. return blob;
  453. },
  454. /**
  455. * 通用的打开下载对话框方法,没有测试过具体兼容性
  456. * @param url 下载地址,也可以是一个blob对象,必选
  457. * @param saveName 保存文件名,可选
  458. */
  459. openDownloadDialog: function (url, saveName) {
  460. if (typeof url == 'object' && url instanceof Blob) {
  461. url = URL.createObjectURL(url); // 创建blob地址
  462. }
  463. var aLink = document.createElement('a');
  464. aLink.href = url;
  465. aLink.download = saveName || ''; // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
  466. var event;
  467. if (window.MouseEvent) event = new MouseEvent('click');
  468. else {
  469. event = document.createEvent('MouseEvents');
  470. event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
  471. }
  472. aLink.dispatchEvent(event);
  473. },
  474. }
  475. });
  476. }
  477. }
  478. class UserInfoAnt extends TMBase {
  479. constructor() {
  480. super();
  481. var name = this.getURLParam(this.queueStorageName);
  482. this.getInfoAsync().then(rs => {
  483. this.log("name:" + name);
  484. this.log(rs);
  485. // 存入数据
  486. this.GM_setValue(name, JSON.stringify(rs));
  487. });
  488. }
  489. getInfoAsync() {
  490. return this.execByPromiseAsync(this, this._getInfoAsync);
  491. }
  492. async _getInfoAsync(dfd) {
  493. this.log("_getInfoAsync");
  494. var chkR###ltInfo = await this.waitAsync(() => {
  495. var domList = document.querySelectorAll('h2');
  496. var chkR###lt = this.find(domList, item => item.innerHTML.indexOf('Contact Info') != -1);
  497. if (!chkR###lt) return false;
  498. return { success: true, dom: chkR###lt }
  499. }, 500);
  500. this.log(chkR###ltInfo);
  501. var findObj = {};
  502. // /voyager/api/identity/dash/profiles
  503. var findDomCode = this.find(document.querySelectorAll('code'), item => item.innerHTML.indexOf('/voyager/api/identity/dash/profiles') != -1 && item.innerHTML.indexOf('FullProfileWithEntities') != -1);
  504. if (findDomCode) {
  505. // 找出
  506. var findCodeObj = JSON.parse(findDomCode.innerHTML);
  507. var targetCodeDom = document.getElementById(findCodeObj.body);
  508. if (targetCodeDom) {
  509. var tempObj = JSON.parse(targetCodeDom.innerHTML);
  510. // com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities
  511. var fullProfileWithEntities = this.find(tempObj.included, item => item.hasOwnProperty('$recipeTypes') && item['$recipeTypes'][0] == 'com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities');
  512. this.log(fullProfileWithEntities);
  513. if (fullProfileWithEntities) {
  514. Object.assign(findObj, {
  515. firstName: fullProfileWithEntities.firstName,
  516. lastName: fullProfileWithEntities.lastName,
  517. headline: fullProfileWithEntities.headline,
  518. locationName: fullProfileWithEntities.locationName
  519. });
  520. }
  521. // com.linkedin.voyager.dash.deco.identity.profile.FullProfilePosition
  522. var fullProfilePositionArray = this.filter(tempObj.included, item => item.hasOwnProperty('$recipeTypes') && item['$recipeTypes'][0] == 'com.linkedin.voyager.dash.deco.identity.profile.FullProfilePosition');
  523. this.log(fullProfilePositionArray);
  524. var companys = [];
  525. fullProfilePositionArray.forEach(position => {
  526. var dateRange = position.dateRange || {};
  527. companys.push({
  528. title: position.title,
  529. companyName: position.companyName,
  530. dateRange: dateRange
  531. });
  532. });
  533. if (companys.length > 0) findObj['company'] = JSON.stringify(companys);
  534. };
  535. }
  536. var findEmailDom = this.find(chkR###ltInfo.dom.parentElement.querySelectorAll('a'), item => item.getAttribute('href').indexOf('mailto:') != -1);
  537. findObj.Email = findEmailDom ? findEmailDom.innerText : "";
  538. dfd.resolve(findObj);
  539. }
  540. }
  541. unsafeWindow.Linkin = {};
  542. if (unsafeWindow.location.href.toLowerCase().indexOf('/detail/contact-info/') != -1) {
  543. unsafeWindow.Linkin.AutoCollectionAnt = new UserInfoAnt();
  544. } else {
  545. unsafeWindow.Linkin.AutoCollectionAnt = new PersonListAnt();
  546. }
  547. })();