之因此有了这篇文,彻底就是前两天,老师又给你们派了一个好麻烦的项目javascript
统计某某期刊的信息。前端
粗粗看了一下14我的的群里,有我这样延毕的老狗 同窗,也有正当主力的研一研二的同窗,貌似还有大四一直跟着老师作项目,美其名曰本科阶段就进入实验室的小朋友(固然仍是蛮好的),好是很好啦,可是一看要我复制粘贴的文章有650+,顿时有点难顶,还好聪明的小徐同窗很快想出了办法:java
CODING!!!node
首先肯定技术栈,由于主攻前端不懂就问,因此选择node做为主要的开发语言,加之要作的是统计文章的信息,稍微想了一下,这个需求不就是爬虫CV嘛。git
puppeteer
是nodejs
中一个很好用的自动化工具,都不能说他是爬虫,由于他普遍应用于自动化测试中,能够看看这篇文章。github
借鉴一下我朋友的这个文章,首先:数据库
npm i -S puppeteer
复制代码
这里由于一下众所周知的缘由,下载Chromium
可能有点费劲,我这边以前玩puppeteer
的时候就装好了,看官能够自行解决一下(搬瓦工啥的);npm
puppeteer
做为一个自动化测试的库,其实就是本身在操做Chrome
浏览器在进行一下指令,因此使用这个编写的代码我以为仍是很直观的。json
能够看出,信息呈现三层形式保存。
首先一些准备工做,引入包和规定的格式:
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加载完成!'); })() 复制代码
通过上面的描述能够看出,puppeteer
和Electron
等有点相似,都是主进程中建立子进程进行操做。
接着就是在列表页选择对应的年份和期数,而且循环执行。
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": "铝钢;激光焊接;熔钎焊;粉末;" }, ... ] } 复制代码
这里就是将数据导出啦,需求里面写的仍是很明白的:
个人想法就是根据每个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
,不过并不成功,时间紧迫就开启人工智能模式 ——手动搞了一下