文章目录
- 效果展示
- 说明
- 利用工具
- 整体思路
- Puppeteer 使用笔记
- 保持登录状态
- 打开新的页面
- 点击 dialog
- 跳转页面
- 设置页面可见窗口大小
- 寻找元素
- 等待元素出现
- 整体代码
效果展示
说明
- 看了看网上很少做这个功能,但是我有这个需求,就抽出时间写了个简单的工具
- 目前只能导出专栏的文章,不过思路类似
- 并没有做
Promise
失败的重新发送,代码仍然待完善,欢迎提交分支 PR,仓库地址:github 代码仓库地址
利用工具
puppeteer
:自动化库asyncPool
:并发控制库
整体思路
- 获取到你想提取的文章
id
- 根据
id
打开编辑器,利用自带的导出按钮
Puppeteer 使用笔记
保持登录状态
如果想保持登录状态的话,就需要把登录信息保存一下
const browser = await puppeteer.launch({
userDataDir: "./userData",
});
打开新的页面
用 browser
对象的 newPage()
方法
const page = await browser.newPage();
点击 dialog
类似这样的弹窗,我们也可以监听到,通过 page.on()
方法,可以一直监听页面上的 dialog
我们实现一下,dialog
出现就点击接受按钮
page.on("dialog", async (dialog) => {
await dialog.accept();
});
跳转页面
await page.goto(targetURL);
设置页面可见窗口大小
await page.setViewport({ width: 1080, height: 1024 });
寻找元素
官网上的 page 方法
比方说,我要寻找CSDN上,当前页面的所有文章
我可以先通过类名拿到 ul
const lis = await page.$(".column_article_list");
然后遍历一下,获取想要的信息,比方说我想获取文章的标题
就可以用 $$eval
来获取到元素的值
const titles = await lis.$$eval(".title", (elements) => {
return elements.map((e) =>
e.innerHTML
.replace("\n", "")
.split("<!--####试读-->")[0]
.replace("\n", "")
.trim()
);
});
等待元素出现
这里可能遇到,由于网速不好等原因,page
中的元素可能不存在,我们可以通过这个方法,等待元素出现
举个例子,我想等待导出按钮的出现
const importButton =
"div.layout__panel.layout__panel--navigation-bar.clearfix > nav > div.scroll-box > div:nth-child(1) > div:nth-child(22) > button";
await page.waitForSelector(importButton);
整体代码
// index.js
import puppeteer from "puppeteer";
import asyncPool from "tiny-async-pool";
import { getPage, waitingOpenURL, findElement, clickImport } from "./tools.js";
(async () => {
// 关闭无头模式,显示浏览器窗口
// userDataDir 表示把登录信息放到当前目录下,省着我们每次调用脚本都需要登录
const browser = await puppeteer.launch({
headless: false,
userDataDir: "./userData",
});
const page = await browser.newPage();
page.on("dialog", async (dialog) => {
await dialog.accept();
});
let targetURL = "https://blog.csdn.net/u010263423/category_9162796.html";
await page.goto(targetURL);
await page.setViewport({ width: 1080, height: 1024 });
const targetPageCount = await getPage(page);
const willOpenArr = await waitingOpenURL(targetPageCount, targetURL);
const findArray = [];
findArray.push(...(await findElement(page)));
if (targetPageCount > 1) {
for (let i = 0; i < willOpenArr.length; i++) {
await page.goto(willOpenArr[i]);
findArray.push(...(await findElement(page)));
}
}
const baseWriteURL = `https://editor.csdn.net/md/?articleId=`;
const baseWriteURLArray = findArray.map((i) => `${baseWriteURL}${i.id}`);
let successHandle = 0;
function handleURL(url) {
return new Promise(async (resolve) => {
const page = await browser.newPage();
page.on("dialog", async (dialog) => {
await dialog.accept();
});
await page.goto(url);
await clickImport(page);
await page.close();
await new Promise((r) => setTimeout(r, 300));
resolve(`${url} 解析完成 ${++successHandle}`);
});
}
for await (const ms of asyncPool(2, baseWriteURLArray, handleURL)) {
console.log(ms);
}
console.log("***已完成所有解析***");
})();
// tools.js
/**
* 功能:获取文章标题
* @param {*} lis
* @returns
*/
export async function getTitle(lis) {
const titles = await lis.$$eval(".title", (elements) => {
return elements.map((e) =>
e.innerHTML
.replace("\n", "")
.split("<!--####试读-->")[0]
.replace("\n", "")
.trim()
);
});
return titles;
}
/**
* 功能:获取文章写作时间
* - nth-child 选择器从1开始,前面尽量是标签名吧,如果是类的话,我试了一下选择不到
* @param {*} lis
*/
export async function getDate(lis) {
const titleDate = await lis.$$eval(
".column_article_data span:nth-child(2)",
(elements) => {
return elements.map((e) => e.innerHTML.trim().split("  ")[0]);
}
);
return titleDate;
}
export async function getID(lis) {
const titleId = await lis.$$eval("a", (elements) => {
return elements.map((e) => e.href.split("details/")[1]);
});
return titleId;
}
export async function getPage(page) {
const pageContainer = await page.$(".ui-paging-container");
let pageCount = 1;
if (pageContainer) {
const pageContext = await pageContainer.$$eval(".ui-pager", (elements) => {
return elements.map((e) => e.innerHTML);
});
pageCount = Number(pageContext[pageContext.length - 3]);
}
console.log(pageCount);
return pageCount;
}
export async function waitingOpenURL(targetPageCount, targetURL) {
const arr = [];
if (targetPageCount > 1) {
for (let i = 2; i <= targetPageCount; i++) {
const front = targetURL.split(".html")[0];
const url = `${front}_${i}.html`;
arr.push(url);
}
}
return arr;
}
export async function findElement(page) {
// 等待页面选择器的出现
await page.waitForSelector(".column_article_list");
const lis = await page.$(".column_article_list");
// 获取文章标题、写作时间、文章id
const titles = await getTitle(lis);
const titleId = await getID(lis);
const titleDate = await getDate(lis);
// 整理成数组对象
const notes = [];
titles.forEach((item, index) => {
const obj = {
title: item,
date: titleDate[index],
id: titleId[index],
};
notes.push(obj);
});
return notes;
}
/**
* 功能: 点击导出按钮
* @param {*} page
*/
export async function clickImport(page) {
await new Promise((r) => setTimeout(r, 1500));
const importButton =
"div.layout__panel.layout__panel--navigation-bar.clearfix > nav > div.scroll-box > div:nth-child(1) > div:nth-child(22) > button";
await page.waitForSelector(importButton);
await page.click(importButton);
// await new Promise((r) => setTimeout(r, 500));
const nextImportButton =
"div.side-bar__inner > div.side-bar__panel.side-bar__panel--menu > a:nth-child(1)";
await page.waitForSelector(nextImportButton);
await page.click(nextImportButton);
// 这个时间是不能省的,一定要给点击事件留点时间,
// 不然直接跳转页面,下载就失效了
await new Promise((r) => setTimeout(r, 500));
}
export function handleWriteURLs(url) {
return new Promise((resolve) => {
console.log(url);
resolve();
});
}