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

起因

前几天想给一个项目加 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();

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

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

相关文章

tomcat配置请求的最大参数个数和请求数据大小

maxParameterCount"10000" maxPostSize"10485760" maxParameterCount&#xff1a;单个请求最大请求参数个数&#xff1b; maxPostSize&#xff1a;单个请求最大数据大小&#xff0c;1048576010M&#xff1b;

flutter3-os:基于flutter3.x+dart3+getx手机版os管理系统

flutter3-os-admin跨平台手机后台OS系统。 原创Flutter3.22Dart3.4Getxfl_chart等技术开发仿ios手机桌面OA管理系统。自研栅格化布局引擎、自定义桌面壁纸、小部件、底部Dock菜单、可拖拽悬浮球等功能。 全新自研栅格化OS菜单布局引擎。 使用技术 编辑器&#xff1a;VScode技术…

【架构模型】

一、客户端/服务端模式 二、单击应用模式 单机应用系统是最简单的软件结构&#xff0c;是指运行在一台物理机器上的独立应用程序。

【大模型】基于Hugging Face调用及微调大模型(1)

文章目录 一、前言二、Transformer三、Hugging Face3.1 Hugging Face Dataset3. 2 Hugging Face Tokenizer3.3 Hugging Face Transformer3.4 Hugging Face Accelerate 四、基于Hugging Face调用模型4.1 调用示例4.2 调用流程概述4.2.1 Tokenizer4.2.2 模型的加载4.2.3 模型基本…

spring源码初始学习基础-环境

环境&#xff1a;在这里插入代码片 allprojects {repositories {maven { url file:///D:/software/repository} // 本地仓库地址&#xff0c;如果没有依次向下寻找maven { url "https://maven.aliyun.com/repository/public" }mavenLocal()mavenCentral()}buildscri…

CopilotKit:开源 Copilot 框架,部署应用内 AI 代理,使用 Langchain 自动执行任何任务!

原文链接&#xff1a;&#xff08;更好排版、视频播放、社群交流、最新AI开源项目、AI工具分享都在这个公众号&#xff01;&#xff09; CopilotKit&#xff1a;开源 Copilot 框架&#xff0c;部署应用内 AI 代理&#xff0c;使用 Langchain 自动执行任何任务&#xff01; &am…

前端工程化工具系列(九)—— mddir(v1.1.1):自动生成文件目录结构工具

mddir 是一个基于项目目录结构动态生成 Markdown 格式目录结构的工具&#xff0c;方便开发者在文档中展示文件和文件夹的组织结构。 1. 安装 全局安装改工具&#xff0c;方便用于各个项目。 pnpm i -g mddir2. 使用 在想要生成目录接口的项目内打开命令行工具&#xff0c;输…

RocketMQ教程(一):RocketMQ的基本概念

RocketMQ是什么? RocketMQ 是一个分布式消息中间件和流计算平台,由阿里巴巴团队开源并贡献给 Apache 软件基金会,现为 Apache 顶级项目。它主要用于处理大规模数据的传输问题,支持高吞吐量、高可用性和可扩展性的消息发布和订阅服务。RocketMQ 能够确保消息的可靠传输,支持…

从报名到领证:软考高级【系统分析师】报名考试全攻略

本文共计13156字&#xff0c;预计阅读39分钟。包括七个篇章&#xff1a;报名、准考证打印、备考、考试、成绩查询、证书领取及常见问题。 不想看全文的可以点击目录&#xff0c;找到自己想看的篇章进行阅读。 一、报名篇 报名条件要求&#xff1a; 1.凡遵守中华人民共和国宪…

R语言探索与分析17-股票题目

Value at Risk&#xff08;VaR&#xff09;是一种统计技术&#xff0c;用于量化投资组合在正常市场条件下可能遭受的最大潜在损失。它是风险管理和金融领域中一个非常重要的概念。VaR通常以货币单位表示&#xff0c;用于估计在给定的置信水平和特定时间范围内&#xff0c;投资组…

码农危是否到来? AI大模型时代到来程序员能做啥?

前言 “马斯克提到人工智能会让工作变得毫无意义&#xff0c;并建议人们可能需要去编写人工智能程序&#xff0c;以避免被AI剥夺就业”&#xff0c;AI大模型的爆发&#xff0c;各种自动化编码应用工具&#xff0c;AI机器人出现&#xff0c;“前有2023年2月份&#xff0c;ChatG…

基于FPGA的任意点滑动平均(滑动窗长度和数据位宽参数化,例化时参数可设置)

目录 1.前言2.原理3.举例说明4.Matlab实现5.FPGA实现滑动平均 微信公众号获取更多FPGA相关源码&#xff1a; 1.前言 对于一维信号&#xff0c;我们可以使用类似移动平均滤波&#xff08;Moving Average Filtering&#xff09;实现denoising。Moving Average Filtering 是一种…

算法金 | 再见!!!KNN

大侠幸会&#xff0c;在下全网同名「算法金」 0 基础转 AI 上岸&#xff0c;多个算法赛 Top 「日更万日&#xff0c;让更多人享受智能乐趣」 KNN算法的工作原理简单直观&#xff0c;易于理解和实现&#xff0c;这使得它在各种应用场景中备受青睐。 我们将深入探讨KNN算法&…

微服务+分库分表的自增主键ID该如何设计?

一. 前言 分布式ID 是分布式系统里面非常重要的一个组成部分&#xff0c;那么我们在设计分布式ID的时候&#xff0c;需要考虑什么问题呢&#xff1f; ❓简单结构下是怎么实现 ID 的控制的&#xff1f; 单实例系统 &#xff1a;通过时间戳&#xff0c;系统内自增&#xff0c;上…

【高校科研前沿】新疆生地所陈亚宁研究员团队在GeoSus发文:在1.5°C和2°C全球升温情景下,中亚地区暴露于极端降水的人口增加

目录 文章简介 1.研究内容 2.相关图件 3.文章引用 文章简介 论文名称&#xff1a;Increased population exposures to extreme precipitation in Central Asia under 1.5 ◦C and 2 ◦C global warming scenarios&#xff08;在1.5C和2C全球变暖情景下&#xff0c;中亚地区…

flutter LINK : ...fatal error LNK1168: �޷���...

执行 flutter run -d windows 后报错 LINK : fatal error LNK1168: &#xfffd;޷&#xfffd;&#xfffd;&#xfffd; E:\xiaoli\flutter_project\huapu_update_hardware\build\windows\x64\runner\Debug\huapu_update_hardware.exe &#xfffd;&#xfffd;&#xfffd;…

gstreamer+mpp调用硬解码播放视频

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、cpu解码二、gstreamermpp1.默认已安装2.没安装必要软件 总结 前言 以前一直在MPP上开发硬解码推理&#xff0c;最近想弄一个盒子支持调用mpp硬解码播放视频…

OZON快蜗牛数据工具,OZON数据分析工具

在当今的电商时代&#xff0c;数据已经成为了商家们最宝贵的资产之一。无论是产品选品、市场定位&#xff0c;还是营销策略的制定&#xff0c;都离不开对数据的深入分析和精准把握。而在众多电商平台中&#xff0c;OZON以其独特的商业模式和庞大的用户群体&#xff0c;吸引了众…

Docker高级篇之Dockerfile解析

文章目录 1. DockerFile简介2. DockerFile的构建过程3. DockerFile的常用保留字4. 使用案例5. 虚悬镜像 1. DockerFile简介 DockerFile是用来构建Docker镜像的文本文件&#xff0c;是由一条条构建镜像的指令和参数构成的脚本。 2. DockerFile的构建过程 DockerFile内容的基…

DpEasy社区版1.4.0 | 体验全新的数据库特权账号管理平台!

上周&#xff0c;我们在社区发布了 DpEasy 调整为数据库特权账号管理平台的说明&#xff08;详见文章《“一键”掌控数据库特权&#xff0c;DpEasy 新版本即将起航》&#xff09;。今天&#xff0c;DpEasy 作为数据库特权账号管理平台正式发布了第一个版本&#xff0c;即 DpEas…