Puppeteer 使用实战:如何将自己的 CSDN 专栏文章导出并用于 Hexo 博客(一)

文章目录

  • 效果展示
  • 说明
  • 利用工具
  • 整体思路
  • Puppeteer 使用笔记
    • 保持登录状态
    • 打开新的页面
    • 点击 dialog
    • 跳转页面
    • 设置页面可见窗口大小
    • 寻找元素
    • 等待元素出现
  • 整体代码


效果展示

请添加图片描述
请添加图片描述

说明

  • 看了看网上很少做这个功能,但是我有这个需求,就抽出时间写了个简单的工具
  • 目前只能导出专栏的文章,不过思路类似
  • 并没有做 Promise 失败的重新发送,代码仍然待完善,欢迎提交分支 PR,仓库地址:github 代码仓库地址

利用工具

  • puppeteer:自动化库
  • asyncPool:并发控制库

整体思路

  1. 获取到你想提取的文章 id
  2. 根据 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(" &nbsp")[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();
  });
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/400402.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

opengl 学习着色器

一.GLSL 着色器是使用一种叫GLSL的类C语言写成的。GLSL着色器编码顺序&#xff1a;声明版本》定义输入输出》uniform》main函数。每个着色器的入口点是main函数&#xff0c;在main函数中我们处理所有的输入变量&#xff0c;并将结果输出到输出变量中。如下图&#xff1a; #ver…

RocketMQ高可用架构涉及常用功能整理

RocketMQ高可用架构涉及常用功能整理 1. 集群高可用系统架构和相关组件1.1 架构说明1.2 相关概念说明1.3 消息模型1.3.1 点对点模型1.3.2 发布订阅模型1.3.3 消息过滤 2. rocketmq的核心参数3. rocketmq常用命令4. 事务性4.1 数据写入流程4.2 数据读流程4.3 事务消息 5. 疑问和…

QT_day2

1.思维导图 2.使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#xff…

哈希应用 | 布隆过滤器概念 | 代码实现 | 哈希切割

文章目录 1.布隆过滤器1.1.布隆过滤器的基本概念1.2.代码实现1.3.测试代码分析误判率1.4.布隆过滤器的优点1.5.关于几道面试题 关于位图&#xff1a;往期分析的 博客链接 1.布隆过滤器 1.1.布隆过滤器的基本概念 布隆过滤器的引出 位图使用1个比特位 直接定址法&#xff0c;…

深入浅出JVM(一)之Hotspot虚拟机中的对象

本篇文章思维导图 对象的创建 对象的创建可以分为五个步骤:检查类加载,分配内存,初始化零值,设置对象头,执行实例构造器 类加载检查 HotSpot虚拟机遇到一条new指令,会先检查能否在常量池中定位到这个类的符号引用,检查这个类是否类加载过 没有类加载过就去类加载类加载过就进…

中国 AI 开课速度直逼美国 AI 颠覆性创新速度

原文链接&#xff1a; 中国 AI 开课速度直逼美国 AI 颠覆性创新速度 今日热帖&#xff0c;有网友发帖称&#xff1a;Sora 和 ChatGPT 告诉我们&#xff0c;美国确实是遥遥领先&#xff0c;而且还越拉越远。 是不是遥遥领先暂且不说&#xff0c;但领先我们的确是事实。 主要是…

多任务互斥及队列

一.互斥的引入 在FreeRTOS中&#xff0c;互斥&#xff08;Mutex&#xff09;是一种用于保护共享资源的机制。互斥锁可以确保同一时间只有一个任务能够访问共享资源&#xff0c;从而避免了竞态条件和数据不一致的问题。 FreeRTOS中互斥的引入方法&#xff1a; 创建互斥锁&#…

【笔记】【算法设计与分析 - 北航童咏昕教授】绪论

算法设计与分析 - 北航童咏昕教授 文章目录 算法的定义定义性质 算法的表示自然语言编程语言伪代码 算法的分析算法分析的原则渐近分析 算法的定义 定义 给定计算问题&#xff0c;算法是一系列良定义的计算步骤&#xff0c;逐一执行计算步骤即可得预期的输出。 性质 有穷性确…

【Linux】git操作 - gitee

1.使用 git 命令行 安装 git yum install git 2.使用gitee 注册账户 工作台 - Gitee.com 进入gitee&#xff0c;根据提示注册并登录 新建仓库 仓库名称仓库简介初始换仓库 3.Linux-git操作 进入仓库&#xff0c;选择“克隆/下载” 复制下面的两行命令进行git配置 然后将仓库clo…

c语言经典测试题2

1.题1 我们来思考一下它的结果是什么&#xff1f; 我们来分析一下&#xff1a;\\是转义为字符\&#xff0c;\123表示的是一个八进制&#xff0c;算一个字符&#xff0c;\t算一个字符&#xff0c;加上\0&#xff0c;应该有13个&#xff0c;但是strlen只计算\0前的字符个数。所以…

快速学习springsecurity最新版 (版本6.2)---用户认证

简介 ​ Spring Security 是 Spring 家族中的一个安全管理框架。目前比较主流的是另外一个安全框架Shiro&#xff0c;它提供了更丰富的功能&#xff0c;社区资源也比Shiro丰富,但是shiro并不简便,这里轻量级安全框架更推荐国产安全框架satokensatoken官网 ​ 一般大型的项目都…

QT应用软件【协议篇】周立功CAN接口卡代码示例

文章目录 USBCAN系列CAN接口卡规格参数资料下载QT引用周立功的库安装sdk代码USBCAN系列CAN接口卡 USBCAN系列CAN接口卡兼容USB2.0全速规范,可支持1/2/4/8路CAN接口。采用该接口卡,PC机可通过USB连入CAN网络,进行CAN总线数据采集和处理,主要具备以下几大优势特点: 支持车载…

Matlab图像处理——图像编码解码

1.霍夫曼编码和解码 clear clc Iimread(lena.bmp); Iim2double(I)*255; [height,width]size(I); %求图像的大小 HWmatrixzeros(height,width); Matzeros(height,width); %建立大小与原图像大小相同的矩阵HWmatrix和Mat&#xff0c;矩阵元素为0。 HWmatrix(1,1)I(1,1); …

清洁力强的洗地机什么牌子最好?深度清洁的洗地机推荐

在相信很多人在做家务时&#xff0c;或许都会遇到一个尴尬的境地&#xff1a;虽然使用吸尘器清理了地面上的尘土和杂物&#xff0c;然后再使用拖把擦洗地板&#xff0c;但往往还是无法达到十分干净的效果。扫地机器人对于着色严重的垃圾&#xff0c;往往会出现越拖越脏的情况。…

Vue3之生命周期基础介绍

让我为大家介绍一下vue3的生命周期吧&#xff01; 创建阶段&#xff1a;setup 我们直接console.log就可以了 console.log("创建");挂载阶段&#xff1a;onBeforeMount(挂载前)、onMounted(挂载完毕) import { onBeforeMount, onMounted } from vue; // 挂载前 on…

【前端】Vue-Cli 快速创建Vue3项目及一些项目初始化相关

文章目录 前言1. 安装1.1 安装 Vue 脚手架1.2 创建项目1.3 本地运行项目 2. 推送到仓库2.1 远程仓库准备2.2 关于gitIgnore文件2.3 通过git推送至远程仓库 3. 补充与总结3.1 npm 版本是否要升级到最新&#xff1f;3.2 这个项目建议的各种版本3.3 一般前端gitIgnore文件3.4 推荐…

蚂蚁集团开始招聘华为鸿蒙应用研发工程师

早在2023年12月7日支付宝宣布将全面启动鸿蒙原生应用开发。华为表示&#xff0c;支付宝将基于HarmonyOS NEXT版本开发应用&#xff0c;给消费者带来全场景的新体验。头部应用伙伴的加入&#xff0c;大力推动了鸿蒙生态进一步完善。 就近期蚂蚁集团开始招聘华为鸿蒙应用研发工程…

【2024软件测试面试必会技能】Jmeter+ant+jenkins实现持续集成

jmeterantjenkins持续集成 一、下载并配置jmeter 首先下载jmeter工具&#xff0c;并配置好环境变量&#xff1b;参考&#xff1a;https://www.cnblogs.com/YouJeffrey/p/16029894.html jmeter默认保存的是.jtl格式的文件&#xff0c;要设置一下bin/jmeter.properties,文件内容…

成为高级性能测试:发现性能瓶颈掌握性能调优

当下云计算、大数据盛行的背景下&#xff0c;大并发和大吞吐量的需求已经是摆在企业面前的问题了&#xff0c;其中网络的性能要求尤为关键&#xff0c;除了软件本身需要考虑到性能方面的要求&#xff0c;一些硬件上面的优化也是必不可少的。 作为一名测试工作者&#xff0c;对…

SICTF Round#3 の WP

Misc 签到 SICTF{1f4ce05a-0fed-42dc-9510-6e76dff8ff53} Crypto [签到]Vigenere 附件内容&#xff1a; Gn taj xirly gf Fxgjuakd, oe igywnd mt tegbs mnrxxlrivywd sngearbsw wakksre. Bs kpimj gf tank, it bx gur bslenmngn th jfdetagur mt ceei yze Ugnled Lystel t…