文章目录
- 效果展示
- 说明
- 利用工具
- 整体思路
- 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");// 获取文章标题、写作时间、文章idconst 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();});
}