爬虫(没)入门:用 node-crawler 爬取 blog

news2024/11/9 0:05:48

起因

前几天想给一个项目加 eslint,记得自己曾经在博客里写过相关内容,所以来搜索。但是发现 csdn 的只能按标题,没办法搜正文,所以我没搜到自己想要的内容。

没办法只能自己又重新折腾了一通 eslint,很烦躁。迁怒于 CSDN(?),所以打算写一个爬虫,自己搜。


新建项目

只想做一个很简单的爬虫,爬取我自己的 blog,获取所有文章的标题、摘要、分类、标签、内容、发布更新时间。

一说到爬虫我就想到 python,但是懒得配置 python 的开发环境了,用 nodejs 随便搞一搞吧。简单查了一下,决定用 node-crawler

npm init -y
git init
创建 .gitignore
pnpm i crawler
创建 index.js


爬取文章内容页面

此页面可以获取标题、分类、标签、正文内容。
页面中也展示了发布和更新日期,但获取较麻烦,不在此页面爬取。

// index.js
const Crawler = require("crawler");

const crawler = new Crawler();
crawler.direct({
  uri: `https://blog.csdn.net/tangran0526/article/details/125663417`,
  callback: (error, res) => {
    if (error) {
      console.error(error);
    } else {
      const $ = res.$;	// node-crawler 内置 cheerio 包。$ 是 cheerio 包提供的,用法和 jQuery 基本一致
      const title = $("#articleContentId").text();
      const content = $("#article_content").text();
    }
  },
});

获取标题、正文很简单,通过 id 直接锁定元素,然后用 text() 获取文本内容。但是分类和标签就要复杂一点了。

在这里插入图片描述

对应的 html 结构是:

在这里插入图片描述

分类和标签的 class 都是 tag-link,只能从 attr 区分。
注意:cheerio 的 map() 返回伪数组对象,需要调用 get() 获取真数组

const categories = $(`.tag-link[href^="https://blog.csdn.net/tangran0526"]`)
  .map((_i, el) => $(el).text())
  .get();
const tags = $(`.tag-link[href^="https://so.csdn.net/so/search"]`)
  .map((_i, el) => $(el).text())
  .get();

发布和更新日期就不在详情页面获取了。因为对有更新和无更新的文章,日期的展示形式不同,而且没有特殊的 class 能锁定元素,获取起来比较麻烦。


获取文章列表

找到文章列表页面,惊喜的发现有滚动加载,也许能找到获取列表的接口。打开浏览器控制台,Network 中看到疑似请求:

在这里插入图片描述

返回值为:

在这里插入图片描述

因为是 get 请求,所以可以直接把接口地址放到浏览器地址栏里访问。这样改参数看效果更直接。可以避免吭哧吭哧写代码试,最后发现参数无效或者哪有错的倒霉情况。

在地址栏里改参数发现 size 能正常工作。给它设大一点,就可以一次获取所有文章。
在这里插入图片描述

let url = "https://blog.csdn.net/community/home-api/v1/get-business-list";
const queryParams = {
  businessType: "blog",
  username: "tangran0526",
  page: 1,
  size: 1000, // 设大一点,一次取完所有文章
};
url +=   "?" +   Object.entries(queryParams).map(([key, value]) => key + "=" + value).join("&");
crawler.direct({
  uri: url,
  callback: (error, res) => {
    if (error) {
      console.error(error);
    } else {
      console.log(res);
    }
  },
});

这次返回值是 json,不是 html 了。不确定返回的 res 是什么结构,node-crawler 的官方文档中有写:

在这里插入图片描述
打断点查看:

在这里插入图片描述
res.body 是一段 html。内容是提示:当前访问人数过多,请完成安全认证后继续访问
应该是网站的防爬虫、防网络攻击的策略。google 了“爬虫、安全验证、验证码”等相关内容,找到了解决方法。

找到刚才用浏览器成功访问的请求,把它的 request header 全部赋给 crawler 里面的 header

crawler.direct({
  uri: url,
  headers: {   
    // 全部放这里
  },
  callback,
}

再次重试,res.body 是想要的结果了。现在是字符串, 再 JSON.parse 一下就可以了。

在这里插入图片描述

下面来精简 request headers。之前是把所有的 headers 都拿过来了,但应该不需要那么多。筛查哪些 headers 是必不可少的:

在这里插入图片描述

二分法排除,确认了只有 cookie 是必须的。

去浏览器中清除 cookie 后,在浏览器中重新访问接口,也弹出了这个安全验证页面。双重实锤 cookie 是关键!

在这里插入图片描述

cookies 内容也很多,根据名称猜测,二分法筛查找到了两个关键cookie:

在这里插入图片描述
只要有它们两个,就不会触发安全验证。

const cookie_yd_captcha_token =  "略";
const cookie_waf_captcha_marker =  "略";
crawler.direct({
  uri: url,
  headers: {
    Cookie: `yd_captcha_token=${cookie_yd_captcha_token}; waf_captcha_marker=${cookie_waf_captcha_marker}`,
  },
  callback
});

这两个 cookie 的值应该是有时效的。每次失效后都必须:打开浏览器——清除cookies后访问接口——遇到安全验证——通过后获取新的有效cookie。
所以这是一个人工爬虫——需要人力辅助的爬虫。。。。我知道这很烂,但没兴趣继续研究了,就这样吧。
如果想真正解决,应该使用 puppeteer 这类爬虫:内置 Headless Browser,可以模拟用户操作,也许能解决图形、滑块等验证。


将爬取结果输出到文件

先调用 list,再对每一篇文章获取详情。最后将结果输出到文件。

执行 node index.js,等待一会后,输出 result.json 文件:

在这里插入图片描述

完整代码如下:

// index.js

const Crawler = require("crawler");
const { writeFile } = require("fs");
const { username, cookie_yd_captcha_token, cookie_waf_captcha_marker } = require("./configs.js");

const crawler = new Crawler();

// https://github.com/request/request
// https://github.com/bda-research/node-crawler/tree/master?tab=readme-ov-file#basic-usage
// https://github.com/cheeriojs/cheerio/wiki/Chinese-README

const articleList = [];

function getArticleList() {
  let url = "https://blog.csdn.net/community/home-api/v1/get-business-list";
  const queryParams = {
    businessType: "blog",
    username,
    page: 1,
    size: 1000,
  };
  url +=
    "?" +
    Object.entries(queryParams)
      .map(([key, value]) => key + "=" + value)
      .join("&");
  return new Promise((resolve, reject) => {
    crawler.direct({
      uri: url,
      headers: {
        Cookie: `yd_captcha_token=${cookie_yd_captcha_token}; waf_captcha_marker=${cookie_waf_captcha_marker}`,
      },
      callback: (error, res) => {
        if (error) {
          reject(error);
        } else {
          const { code, data, message } = JSON.parse(res.body);
          if (code === 200) {
            resolve(data);
          } else {
            reject({ code, message });
          }
        }
      },
    });
  });
}

function getArticleDetail(articleId) {
  return new Promise((resolve, reject) => {
    crawler.direct({
      uri: `https://blog.csdn.net/${username}/article/details/${articleId}`,
      callback: (error, res) => {
        if (error) {
          reject(error);
        } else {
          const $ = res.$;
          const title = $("#articleContentId").text();
          const content = $("#article_content").text();
          const categories = $(`.tag-link[href^="https://blog.csdn.net"]`)
            .map((_i, el) => $(el).text())
            .get();
          const tags = $(`.tag-link[href^="https://so.csdn.net/so/search"]`)
            .map((_i, el) => $(el).text())
            .get();
          const articleDetail = {
            title,
            categories,
            tags,
            content,
          };
          resolve(articleDetail);
        }
      },
    });
  });
}

async function start() {
  const { list } = await getArticleList();
  for (let i = 0; i < list.length; i++) {
    const { articleId, description, formatTime, postTime, title } = list[i];
    const { categories, tags, content } = await getArticleDetail(articleId);
    articleList.push({
      articleId,
      title,
      description,
      categories,
      tags,
      content,
      postTime,
      formatTime,
    });
  }
  outputToJson();
}

function outputToJson() {
  const path = "./result.json";
  writeFile(path, JSON.stringify(articleList), (error) => {
    if (error) {
      console.log("An error has occurred ", error);
      return;
    }
    console.log("Data written successfully to disk");
  });
}
start();

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

新手上路:Linux虚拟机创建与Hadoop集群配置指南①(未完)

一、基础阶段 Linux操作系统: 创建虚拟机 1.创建虚拟机 打开VM,点击文件,新建虚拟机,点击自定义,下一步 下一步 这里可以选择安装程序光盘映像文件,我选择稍后安装 选择linux系统 位置不选C盘,创建一个新的文件夹VM来放置虚拟机,将虚拟机名字改为master方便后续识别…

AI框架之Spring AI与Spring Cloud Alibaba AI使用讲解

文章目录 1 AI框架1.1 Spring AI 简介1.2 Spring AI 使用1.2.1 pom.xml1.2.2 可实现的功能 1.3 Spring Cloud Alibaba AI1.4 Spring Cloud Alibaba AI 实践操作1.4.1 pom.xml1.4.2 配置文件1.4.3 对接文本模型1.4.4 文生图模型1.4.5 语音合成模型 1 AI框架 1.1 Spring AI 简介…

什么是APP加固?

APP加固是一系列技术手段的集合&#xff0c;旨在提升移动应用程序的安全性&#xff0c;保护其免受各种攻击和威胁。加固技术可以对应用程序的代码、数据、运行环境等多个方面进行保护&#xff0c;从而提高应用的整体安全性和韧性。 常见的APP加固技术 代码混淆&#xff1a; 代码…

Ubuntu系统本地搭建WordPress网站并发布公网实现远程访问

文章目录 前言1. 搭建网站&#xff1a;安装WordPress2. 搭建网站&#xff1a;创建WordPress数据库3. 搭建网站&#xff1a;安装相对URL插件4. 搭建网站&#xff1a;内网穿透发布网站4.1 命令行方式&#xff1a;4.2. 配置wordpress公网地址 5. 固定WordPress公网地址5.1. 固定地…

java中事务中遇到锁会造成什么问题,以及该如何解决?

在spring中实现事务有多种方式&#xff0c;主要是两种&#xff1a;一种是声明式事务&#xff0c;一种是编程式事务&#xff0c;今天我们就讲声明式事务中的一种&#xff0c;使用注解Transactional&#xff0c;这个注解的作用就是帮助我们在代码执行完毕之后自动提交事务&#x…

Coolmuster Android助手评测:简化Android到电脑的联系人传输

产品概述 Coolmuster Android助手是一款旨在简化Android设备与计算机之间数据管理和传输过程的全面工具。它以用户友好的界面和全面的功能&#xff0c;成为寻求高效数据管理解决方案的Android用户的热门选择。 主要特点和功能Coolmuster Android助手拥有一系列使其成为管理Andr…

优思学院|谈汽车零部件企业生产精益及现场管理

精益生产&#xff08;Lean Production&#xff09;和现场管理作为现代制造企业的核心管理理念&#xff0c;正在越来越多的企业中得到应用。尤其是在中国&#xff0c;许多汽车零部件企业通过精益管理和六西格玛方法&#xff0c;显著提高了生产效率&#xff0c;降低了生产成本&am…

白酒:茅台镇白酒的地域特色与环境优势

茅台镇&#xff0c;位于中国贵州省仁怀市&#xff0c;因其与众不同的自然环境和酿酒工艺而成为世界著名的白酒产区。作为茅台镇的品牌&#xff0c;云仓酒庄豪迈白酒以其卓着的品质和口感赢得了广大消费者的喜爱。而这一切&#xff0c;都离不开茅台镇的地域特色和环境优势。 茅台…

SQL性能优化 ——OceanBase SQL 性能调优实践分享(3)

相比较之前的两篇《连接调优》和《索引调优》&#xff0c;本篇文章主要是对先前两篇内容的整理与应用&#xff0c;这里不仅归纳了性能优化的策略&#xff0c;也通过具体的案例&#xff0c;详细展示了如何分析并定位性能瓶颈的步骤。 SQL 调优 先给出性能优化方法和分析性能瓶…

二叉树创建和遍历

个人主页 &#xff1a;敲上瘾-CSDN博客二叉树介绍&#xff1a;二叉树(详解)-CSDN博客 目录 一、二叉树的创建 二、二叉树的遍历 1.前序遍历 2.中序遍历 3.后序遍历 4.层序遍历 三、相关计算 1.总节点个数计算 2.叶子节点个数计算 3.深度计算 一、二叉树的创建 关于…

Lodop 实现局域网打印

文章目录 前言一、Lodop支持打印的方式lodop 打印方式一般有3种&#xff1a;本地打印局域网集中打印广域网AO打印 二、集成步骤查看lodop 插件的服务端口&#xff1a;查看ip后端提供接口返回ip&#xff0c;前端动态获取最后步骤 前言 有时候会根据不同的ip来获取资源文件&…

天锐绿盾 | 源代码防泄密软件

#源代码防泄密# 天锐绿盾是一款专为企业设计的&#xff0c;旨在保护企业核心数据安全与防止代码泄露的软件。它通过一系列技术手段实现对信息的加密、访问控制、行为审计、外发控制等&#xff0c;以确保企业内部的代码和敏感信息不会未经授权就被泄露出去。 PC地址&#xff1a…

音视频开发19 FFmpeg 视频解码- 将 h264 转化成 yuv

视频解码过程 视频解码过程如下图所示&#xff1a; ⼀般解出来的是420p FFmpeg流程 这里的流程是和音频的解码过程一样的&#xff0c;不同的只有在存储YUV数据的时候的形式 存储YUV 数据 如果知道YUV 数据的格式 前提&#xff1a;这里我们打开的h264文件&#xff0c;默认是YU…

电子元器件采购商城的售后服务保障

电子元器件采购商城的售后服务保障是用户在采购电子元器件时的重要考量因素之一。以下是常见的售后服务保障内容&#xff1a; 退换货政策&#xff1a; 质量问题退换货&#xff1a;如果用户收到的元器件存在质量问题&#xff0c;通常可以在一定时间内申请退换货。无理由退换货&a…

Linux--Socket编程基础

一、Socket简介 套接字&#xff08; socket &#xff09;是 Linux 下的一种进程间通信机制&#xff08; socket IPC &#xff09;&#xff0c; 使用 socket IPC 可以使得在不同主机上的应用程序之间进行通信&#xff08;网络通信&#xff09;&#xff0c;当然也可以是同一台…

【C++课程学习】:C++入门(输入输出,缺省参数)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;C课程学习 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 &#x1f369;1.关于C输入输出&#xff1a; &#x1f369;2.缺省参数函数&#xff1a; 缺省参数的概…

手上3个offer怎么选,深信服的技服、奇安信的安全服务、绿盟的渗透工程师

我是一个应届生&#xff0c;目前手上收到了3个offer比较纠结选哪个&#xff1f; 1、深信服技术服务工程师&#xff0c;base 西安&#xff0c;月薪 14k 据说还有些异地补助 2、奇安信安全服务工程师&#xff0c;base 重庆&#xff0c;月薪 12k 3、绿盟科技的渗透工程师&#…

轻松掌握PDF文本内容修改技巧,让文档编辑更高效便捷,职场工作从此得心应手!

在快节奏的职场生活中&#xff0c;PDF文档已经成为我们工作中不可或缺的一部分。然而&#xff0c;如何高效、便捷地修改PDF文本内容&#xff0c;一直是许多职场人士头疼的问题。今天&#xff0c;就让我来为你介绍一款神奇的编辑工具——首助编辑高手&#xff0c;它将帮助你轻松…

http://app.hrss.xm.gov.cn/xmggfw/index

厦门市人力资源和社会保障局 厦门市国际职业资格比照认定职称审批表

【Cesium4UE】使用问题及解法统计

本期作者&#xff1a;尼克 易知微3D引擎技术负责人 1.加载3dtiles模型很慢 1.3dtiles是否做了重建顶层处理。如果3dtiles的tiles块太多使用CesiumLab重建顶层。 2.将3dtiles模型放置到固态硬盘中 3.如果有多块3dtiles&#xff0c;考虑使用CesiumLab合并3dtiles处理 4.如果不需…