我是如何作到寫EXCEL時速3k行的

原由

之因此有了這篇文,徹底就是前兩天,老師又給你們派了一個好麻煩的項目javascript

統計某某期刊的信息。前端

粗粗看了一下14我的的羣裏,有我這樣延畢的老狗 同窗,也有正當主力的研一研二的同窗,貌似還有大四一直跟着老師作項目,美其名曰本科階段就進入實驗室的小朋友(固然仍是蠻好的),好是很好啦,可是一看要我複製粘貼的文章有650+,頓時有點難頂,還好聰明的小徐同窗很快想出了辦法:java

CODING!!!node

開幹吧

首先肯定技術棧,由於主攻前端不懂就問,因此選擇node做爲主要的開發語言,加之要作的是統計文章的信息,稍微想了一下,這個需求不就是爬蟲CV嘛。git

puppeteernodejs中一個很好用的自動化工具,都不能說他是爬蟲,由於他普遍應用於自動化測試中,能夠看看這篇文章github

借鑑一下我朋友的這個文章,首先:數據庫

npm i -S puppeteer
複製代碼

這裏由於一下衆所周知的緣由,下載Chromium可能有點費勁,我這邊以前玩puppeteer的時候就裝好了,看官能夠自行解決一下(搬瓦工啥的);npm

puppeteer 做爲一個自動化測試的庫,其實就是本身在操做Chrome瀏覽器在進行一下指令,因此使用這個編寫的代碼我以爲仍是很直觀的。json

觀察需求

  • 獲取2014-2015年焊接學報的全部學術文章的標題,做者與單位,起止頁碼,摘要關鍵詞等信息數組

  • 做者須要按行分開,做者和單位須要對應上

  • 在上面的基礎上,其餘行須要合併。

    image-20200306214754381

    文章列表頁

    image-20200306214726098

    單個文章的示例

解決方案

  • 入口是CNKI的期刊文章列表頁,基於ASPX生成。
  • 在文章列表頁,就能夠獲得一部分信息
  • 摘要,關鍵詞須要進入對應的文章頁面去獲取
  • 做者和單位的對應須要進入pdf查看**(未完成)**
  • 完成抓取以後再將數據導出成excel

能夠看出,信息呈現三層形式保存。

爬取全部的首層信息

首先一些準備工做,引入包和規定的格式:

const puppeteer = require('puppeteer');
const url = 'https://navi.cnki.net/knavi/JournalDetail?pcode=CJFD&pykm=HJXB';
// 統一設定一個等待時間,防止操做太快被目標認出來
const TIME = 3000;
複製代碼

接下來就是主函數:

// 一個當即執行的異步函數
(async () => {
    const browser = await puppeteer.launch({
		// headless: false, // false瀏覽器界面啓動
		slowMo: 100, // 放慢瀏覽器執行速度,方便測試觀察
		args: [
			// 啓動 Chrome 的參數
			'–no-sandbox',
			// '--window-size=1280,960',
		],
	});
    // 建立新頁面
    const page = await browser.newPage();
    // 這一句就是前往目標頁面
    await page.goto(url, {
		// 網絡空閒說明已加載完畢
		waitUntil: 'networkidle2',
	});
	console.log('page加載完成!');
})()
複製代碼

通過上面的描述能夠看出,puppeteerElectron等有點相似,都是主進程中建立子進程進行操做。

接着就是在列表頁選擇對應的年份和期數,而且循環執行。

puppeteer意爲提線木偶,因此想讓瀏覽器作什麼就發出對應的指令便可:

首先是用到的兩個util函數:

// 由於網頁上年份的按鈕的id是數字開頭,直接S()會出錯
// 因此須要把它轉換成Unicode
function getID(year) {
	let num = year - 2010;
	return `#\\0032\\0030\\0031\\003${num}\\005f\\0059\\0065\\0061\\0072\\005f\\0049\\0073\\0073\\0075\\0065`;
}

// 選擇某一年某一期的id
function getNoDotID(year, num) {
	let _num = num < 10 ? `0${num}` : `${num}`;
	return `#yq${year}${_num}`;
}
複製代碼

接下來:

// 選擇2014年,對每一期進行點擊
// 年份點擊事件
let yearNum = 2014;
const yearBtn = await page.$(getID(yearNum));
await yearBtn.click();
await page.waitFor(TIME);
let accNum = 1;
// 輸出的結果,是一個二維數組。
let output = [];
// 從第一期開始,一個月一期
while (accNum < 13) {
    // 循環選擇第幾期
    let NoDot = await page.$(getNoDotID(yearNum, accNum));
    NoDot.click();

    // 保存全部的信息
    await page.waitFor(TIME);

    console.log('選擇列表...' + accNum);
    const list = await page.$('#CataLogContent');
    const items = await list.?('dd');

    const res = await page.evaluate(list => {
        // ...
    }, list);
    output.push(res);
    accNum++;
}
複製代碼
  • page.$(), page.?()相似於document.querySelector/querySelectorAll,返回一個節點元素
  • page.evaluate(function,node) 是對上面選擇到的對應的node節點進行瀏覽器內操做的方法,在function中實現。,function接受node做爲參數。

page.evaluate的內部,咱們將文章的信息(標題,起止頁碼等)以及連接提取出來保存起來。

const res = await page.evaluate(list => {
    // 在這裏就可使用browser的對象啦
    const itemList = list.querySelectorAll('dd');
    let arr = [];
    // console.log(itemList);
    for (let item of itemList) {
        // 這裏是發現cnki是基於aspx的網頁
        // 而且跳轉到對應的頁面是有規律的,和filename以後的id有關
        // 另外,不一樣的年份有不一樣的數據庫
        const getPaperId = function(id) {
            let match = /filename=(\w+)&/i.exec(id);
            return match[1];
        }
        let paperID = item.querySelector('.opts > .btn-view >a').href;
        let id = getPaperId(paperID);
        // 最後將2014年某一條的innerText和id保存成一個字符串,留着以後解析
        let content = item.innerText + '&' +id;
        arr.push(content);
    }
    return arr;
}, list);
複製代碼

這樣運行一下npm start,獲得的數據就log出來了。目前我就是直接複製了一下,固然也有其餘的辦法。

最終獲得的data.txt:

[
    ["5052鋁合金/鍍鋅鋼塗粉CO2激光熔釺焊工藝特性\n樊丁;蔣鍇;餘淑榮;張健;\n1-4+113&HJXB201401001","鋁合金超聲-MIG焊接電弧行爲\n範成磊;謝偉峯;楊春利;寇毅;\n5-8+113&HJXB201401002",...],
     ...
 ]
複製代碼

爬取摘要,關鍵詞等信息

目前是有了部分信息,可是摘要和關鍵詞還須要在第二層裏面獲取;

對數據進行一些預處理

npm run analysis

這一部分就是對上面獲得的list進行處理,首先把2維數組拍平:

const out2014S = require('./output2014');
const out2015S = require('./output2015');
const fs = require('fs');

// 獲取引用
let out2014 = out2014S;
let out2015 = out2015S;
// flat
while (out2014.some(Array.isArray)) {
	out2014 = [].concat(...out2014);
}

while (out2015.some(Array.isArray)) {
	out2015 = [].concat(...out2015);
}
複製代碼

目前獲得的數據示例以下:

"5052鋁合金/鍍鋅鋼塗粉CO2激光熔釺焊工藝特性\n樊丁;蔣鍇;餘淑榮;張健;\n1-4+113&HJXB201401001",
    ...
複製代碼

須要對這個進行分析,自定義一個split函數:

function SecondeSplit(arr, year) {
    // 數據序列化一下,保存下\n用於分割
	let str = JSON.stringify(arr);
	console.log('str' + str);
	let nArr = str.split('\\n');
	console.log('nArr' + nArr);
	// 0 title
	// 1 string authors
	// 2 pages and link
	let res = {};
    // clean
	res.title = nArr[0].replace(/\"/i, '');
	let names = nArr[1].split(';');
	res.name = names.slice(0, names.length - 1);
    // 存在有的文章沒有頁碼和連接等問題
	if (nArr[2]) {
		let linkArr = nArr[2].split('&');
        // clean
		let link = linkArr[1].replace(/\"/i, '');
        // 兩年的dbname稍有不一樣
		if (year === 2014) {
			res.link = `http://kns.cnki.net/kcms/detail/detail.aspx?dbcode=CJFD&filename=${link}&dbname=CJFD2014`;
		}
		if (year === 2015) {
			res.link = `http://kns.cnki.net/kcms/detail/detail.aspx?dbcode=CJFD&filename=${link}&dbname=CJFDLAST2015`;
		}
		let pages = linkArr[0].split('+');
		let pageArr = pages[0].split('-');
		res.start = pageArr[0];
		res.end = pageArr[1];
	}
	return res;
}

// 對兩年的數據進行操做
let ret2014 = [];
out2014.forEach(i => {
	let tmp = SecondeSplit(i, 2014);
	ret2014.push(tmp);
});
// ... 2015同樣

let ret = ret2014.concat(ret2015);

let jsonObj = {};
jsonObj.data = ret;
// \t可以保存一個比較美觀的json
let wObj = JSON.stringify(jsonObj, '', '\t');
fs.writeFile('data.json', wObj, err => {
	console.log(err);
});

複製代碼

爬取摘要等

npm run abstract

這裏的主要思路就是繼續操做puppeteer,對每個連接,獲取對應摘要,學校和關鍵詞信息

這裏的puppeteer並無用基於async的寫法,用then也很方便。

const obj = require('../data1.json');
const fs = require('fs');
const puppeteer = require('puppeteer');
// 由於要對obj操做
let data = obj;
const len = data.data.length;
puppeteer
	.launch({
		headless: true,
	})
	.then(async browser => {
		for (let i = 0; i < len; i++) {
			if (data.data[i].link) {
				const res = await getAbstract(i, data.data[i].link, browser);
                // 這裏就用keyword來判斷是否抓取成功了
				console.log(i + ': ' + res.keywords);
				data.data[i].abstract = res.abstract;
				data.data[i].school = res.school;
				data.data[i].keywords = res.keywords;
			}
		}
	})
	.then(() => {
		console.log('獲取信息完成!');
		// console.log(data.data[0].abstract);
    	// 保存到data1.json
		save(data);
	});
複製代碼

getAbstract是一個獲取摘要的函數,須要傳browser實例,連接和序號:

async function getAbstract(num, link, browser) {
	const page = await browser.newPage();
	await page.goto(link);
	await page.waitFor(3000);
    // 摘要
	let abs = await page.$('#ChDivSummary');
	let abstract = await page.evaluate(abs => {
		return abs.innerText;
	}, abs);
    // 學校
	let schoolDOM = await page.$('.orgn');
	let school = await page.evaluate(schoolDOM => {
		let arr = schoolDOM.querySelectorAll('span > a');
		let res = '';
		arr.forEach(i => {
			res += i.text + ',';
		});
        // 拼接爲字符串後就刪掉最後一個逗號
		return res.slice(0, res.length - 1);
	}, schoolDOM);
    // 關鍵詞
	let keysDOM = await page.$('#catalog_KEYWORD');
	let keys = await page.evaluate(keysDOM => {
    // let arr = keysDOM.querySelectorAll('p')[2].querySelectorAll('a');
    // 上面的寫法並很差,由於有的掛了基金有的沒掛,因此不必定是第三個
    // 發現關鍵詞裏面一個dom是有id的
    // 因此選用了兄弟節點的方法。
    let arr = keysDOM.parentNode.children;
    let res = '';
    for(let j=1;j<arr.length;j++){
      res += arr[j].text.replace(/ /g, '').replace(/\n/g, '');
    }
		return res;
	}, keysDOM);
	await page.waitFor(3000);
    // 節省內存,每次查詢完就關閉頁面
	await page.close();
	return {
		abstract: abstract,
		school: school,
		keywords: keys,
	};
}
複製代碼

這樣就獲得了完整的數據:

{
	"data": [
		{
			"title": "5052鋁合金/鍍鋅鋼塗粉CO2激光熔釺焊工藝特性",
			"name": [
				"樊丁",
				"蔣鍇",
				"餘淑榮",
				"張健"
			],
			"link": "http://kns.cnki.net/kcms/detail/detail.aspx?dbcode=CJFD&filename=HJXB201401001&dbname=CJFD2014",
			"start": "1",
			"end": "4",
			"abstract": "以5052鋁合金和熱鍍鋅ST04Z鋼爲研究對象,採用預置塗粉CO2激光搭接熔釺焊方法進行工藝試驗.利用光學顯微鏡、掃描電鏡和拉伸試驗機對熔釺焊接頭的微觀組織和力學性能進行了研究.結果代表,塗助溶劑和粉末後,焊縫成形明顯改善,鍍鋅層沒有燒損;熔—釺焊接頭過渡層最大厚度小於10μm,針狀Al-Fe金屬間化合物沒有向熔化的鋁側明顯析出;接頭具備較高的力學性能,最大機械抗載能力可達到208 MPa,約爲5052鋁合金母材抗拉強度的95.41%. ",
			"school": "蘭州理工大學甘肅省有色金屬新材料省部共建國家重點實驗室,蘭州理工大學有色金屬合金及加工教育部重點實驗室",
			"keywords": "鋁鋼;激光焊接;熔釺焊;粉末;"
		},
        ...
    ]
}
複製代碼

將數據導出到EXCEL

這裏就是將數據導出啦,需求裏面寫的仍是很明白的:

image-20200307210201326

個人想法就是根據每個item的做者list的長度,首先是寫出若干行,而後再將除了做者和單位以外的行進行合併。

const Excel = require('exceljs');
const data = require('../data1.json');

// 數據預處理
let input = [];
let obj = data.data;
obj.forEach((item, index) => {
	let len = item.name.length;

	let link = item.link;
	let reg = /HJXB201(4|5)([0-9]{2})/i;

	let year = -1;
	let juan = -1;
	let vol = -1;
	if (link) {
		year = link.substring(link.length - 4, link.length);
        // 2014年是35卷,2015=36卷
		juan = year == 2014 ? 35 : 36;
        // 期數在連接裏面就能夠查出,是第二個匹配項
		vol = reg.exec(link)[2];
	}

	for (let i = 0; i < len; i++) {
        // 將數據整理成exceljs須要的樣子
		input.push({
			index: index + 1,
			title: item.title,
			name: item.name[i],
			lang: '中文',
			school: item.school,
			abstract: item.abstract,
			year: year,
			juan: juan,
			vol: vol,
			keyType: '關鍵詞',
			paperName: '焊接學報',
			keywords: item.keywords,
			start: item.start,
			end: item.end,
		});
	}
});
複製代碼

接着使用exceljs來建立工做表:

// excel處理
let workbook = new Excel.Workbook();

workbook.creator = 'xujx';

let sheet = workbook.addWorksheet('sheet 1');

sheet.columns = [
	{ header: '序號', key: 'index', width: 10 },
	{ header: '惟一標識類型', key: 'onlykey', width: 10 },
	{ header: '惟一標識', key: 'onlyid', width: 10 },
	{ header: '題名', key: 'title', width: 15 },
	{ header: '正文語種', key: 'lang', width: 10 },
	{ header: '責任者/責任者姓名', key: 'name', width: 15 },
	{ header: '責任者/責任者機構/責任機構名稱', key: 'school', width: 15 },
	{ header: '摘要', key: 'abstract', width: 15 },
	{ header: '主題/主題元素類型', key: 'keyType', width: 15 },
	{ header: '主題/主題名稱', key: 'keywords', width: 15 },
	{ header: '期刊名稱', key: 'paperName', width: 15 },
	{ header: '出版年', key: 'year', width: 15 },
	{ header: '規範期刊URI', key: 'URI', width: 15 },
	{ header: '卷', key: 'juan', width: 15 },
	{ header: '期', key: 'vol', width: 15 },
	{ header: '起始頁碼', key: 'start', width: 15 },
	{ header: '結束頁碼', key: 'end', width: 15 },
	{ header: '收錄信息/收錄類別代碼', key: 'typeCode', width: 15 },
];

sheet.addRows(input);
複製代碼

在這以後就合併單元格:

// 合併單元格
// 首先獲取每一項的做者個數,保存在一個array中
let nameLength = [];
obj.forEach(item => {
	if (item.name.length) {
		nameLength.push(item.name.length);
	} else {
		nameLength.push(0);
	}
});
複製代碼

合併單元格從第二行開始(第一行是表頭):

for (let j = 0; j < ret.length; j += 2) {
	sheet.mergeCells(`A${ret[j]}:A${ret[j + 1]}`);
	sheet.mergeCells(`B${ret[j]}:B${ret[j + 1]}`);
	sheet.mergeCells(`C${ret[j]}:C${ret[j + 1]}`);
	sheet.mergeCells(`D${ret[j]}:D${ret[j + 1]}`);
	sheet.mergeCells(`E${ret[j]}:E${ret[j + 1]}`);
	sheet.mergeCells(`H${ret[j]}:H${ret[j + 1]}`);
	sheet.mergeCells(`I${ret[j]}:I${ret[j + 1]}`);
	sheet.mergeCells(`J${ret[j]}:J${ret[j + 1]}`);
	sheet.mergeCells(`K${ret[j]}:K${ret[j + 1]}`);
	sheet.mergeCells(`L${ret[j]}:L${ret[j + 1]}`);
	sheet.mergeCells(`M${ret[j]}:M${ret[j + 1]}`);
	sheet.mergeCells(`N${ret[j]}:N${ret[j + 1]}`);
	sheet.mergeCells(`O${ret[j]}:O${ret[j + 1]}`);
	sheet.mergeCells(`P${ret[j]}:P${ret[j + 1]}`);
	sheet.mergeCells(`Q${ret[j]}:Q${ret[j + 1]}`);
	sheet.mergeCells(`R${ret[j]}:R${ret[j + 1]}`);
}

workbook.xlsx.writeFile('1.xlsx').then(function() {
	// done
	console.log('done');
});
複製代碼

上面的數組ret是這樣獲得的,它保存了合併單元格的起止位置。

let ret = [];
// 是從第2行開始
ret.push(2);
// 對於每個做者長度
for (let i = 0; i < nameLength.length; i++) {
    // 表示尾部的那個節點的位置
	let head = ret[ret.length - 1];
    // 目前數組長度爲偶數,說明如今是成對的,所以須要把尾部節點的下一個數加入數組
	if (ret.length % 2 === 0) {
		ret.push(head + 1);
        // 同時,因爲這一循環並無用到nameLength數組,因此不算作循環++
    	i--;
	} else {
        // 若是是奇數,說明須要添加一個步長,來合併單元格
        // 因此須要一個做者個數-1的步長
    	ret.push(head + nameLength[i] - 1);
	}
}
複製代碼

這樣就完成了99%了!

未完成的部分

  • 可是需求裏面還說須要做者和做者的單位對應,這就須要把文章下載下來分析了。
  • 我目前的嘗試是pdf2json,不過並不成功,時間緊迫就開啓人工智能模式 ——手動搞了一下
  • 確實有點累。

源碼地址

Github 求個star吧555

相關文章
相關標籤/搜索