js玩儿爬虫

前言

提到爬虫可能大多都会想到python,其实爬虫的实现并不限制任何语言。
下面我们就使用js来实现,后端为express,前端为vue3。

实现功能

话不多说,先看结果:
image
这是项目链接:https://gitee.com/xi1213/worm
项目用到的库有:vue、axios、cheerio、cron、express、node-dev
计划功能有:

  1. 微博热榜爬取。
  2. 知乎热榜爬取。
  3. B站排行榜爬取。
  4. 三个壁纸网站爬取。
  5. 随机生成人脸。
  6. 爬取指定页面所有图片。
  7. 删除爬取的数据。
  8. 定时任务(开发中)。

使用形式为:
双击打包出的exe(最好右键管理员运行,以防权限不足)。
image
双击exe后会弹出node后端启动的黑框。
image
自动在浏览器中打开操作界面(用户界面)。
image
爬取出的数据在exe同级目录下的exportData中。
image

具体实现

微博热榜

image
打开微博官网,f12分析后台请求,会发现它的热榜数据列表在请求接口:https://weibo.com/ajax/side/hotSearch 中,无参。
image
在接口列表realtime中根据页面信息,推测其字段含义:

  1. word为关键字,
  2. category为类别,
  3. https://s.weibo.com/weibo?q=%23 + word为链接,
  4. num为热度。

既然数据是现成的,那我们直接使用axios即可。
获取到数据列表后将其遍历拼接成指定格式的字符串,写入txt,下面是具体方法:
weibo.js

let axios = require('axios'),
    writeTxt = require("../utils/writeTxt"),
    { addMsg } = require("../store/index");

//抓取weibo
async function weiboWorm(dir, time) {
    let com = 'https://weibo.com';
    addMsg(`${com} 爬取中...`)
    let res = await axios.get(`${com}/ajax/side/hotSearch`);
    //拼接数据
    let strData = `微博热榜\r\n爬取时间:${time}\r\n`
    await res.data.data.realtime.forEach((l, index) => {
        strData = strData +
            '\r\n序号:' + (index + 1) + '\r\n' +
            '关键字:' + l.word + '\r\n' +
            '类别:' + l.category + '\r\n' +
            '链接:https://s.weibo.com/weibo?q=%23' + l.word.replace(/\s+/g, "") + '\r\n' +
            '热度:' + l.num + '\r\n' +
            '\r\n\r\n=================================================================================================================='
    })
    writeTxt(`${dir}/weibo_${Date.now()}.txt`, strData);//写入txt
    addMsg('$END');
}

module.exports = weiboWorm;

writeTxt.js

let fs = require('fs');

//写入txt
function writeTxt(filePath, data) {
    fs.writeFile(filePath, data, (err) => {
    })
}

module.exports = writeTxt;

需要注意的是在windows中换行使用的是\r\n,在链接中需要去掉空格。

知乎热榜

image
打开知乎官网,会发现它是需要登录的。
f12后点击左上角第二个按钮,在浏览器中切换为手机布局,刷新后即可不登录显示文章信息。
分析请求发现文章数据在请求接口:https://www.zhihu.com/api/v3/explore/guest/feeds 中,参数为limit,限制文章数。
image
根据页面信息推测接口字段含义:

  1. target.question.title为问题标题,
  2. https://www.zhihu.com/question/ + target.question.id为问题链接,
  3. target.question.answer_count为问答数,
  4. target.question.author.name为提问的用户名,
  5. https://www.zhihu.com/org/ + target.question.author.url_token为提问的用户链接,
  6. target.content为高赞回答的内容。

需要注意的是高赞回答的内容中有html的标签,需要自己str.replace(/xxx/g,‘’)去除。
数据的具体获取方法同微博类似。

B站排行榜

image
打开B站官网,找到排行榜,f12后发现数据在接口请求:https://api.bilibili.com/x/web-interface/ranking/v2 中,无参。
image
推测接口字段含义:

  1. title为视频标题,
  2. short_link_v2为视频短链,
  3. stat.view为视频浏览量,
  4. desc为视频描述,
  5. pic为视频封面,
  6. owner.name为视频作者,
  7. pub_location为发布地址,
  8. https://space.bilibili.com/ + owner.mid为作者链接。

数据的具体获取方法同微博类似。

壁纸网站爬取

项目使用了下面三个网站作为例子:
http://www.netbian.com/
image
https://www.logosc.cn/so/
image
https://bing.ioliu.cn/
image
具体思路如下:

  1. 用axios请求页面。
  2. 将请求到的数据使用cheerio.load解析(cheerio为node中的jq,语法同jq)。
  3. f12分析需要的数据在什么元素中,使用cheerio获取到该目标元素。
  4. 获取到元素中img的src内容。
  5. axios请求src(需要encodeURI转码,防止中文报错),记得设置responseType为stream。
  6. 有分页的需要考虑到动态改变url中的页码。
  7. 需要保证下载顺序,一张图片下载完成后才能下载另一张,否则下载量过大会有下载失败的可能,使用for配合async与await即可。

具体实现代码如下:
bian.js

let fs = require('fs'),
    cheerio = require('cheerio'),
    axios = require('axios'),
    downloadImg = require("../utils/downloadImg.js"),
    { addMsg } = require("../store/index");

//抓取彼岸图片
async function bianWorm(dir, pageNum) {
    let page = pageNum,//抓取页数
        pagUrlList = [],
        imgList = [],
        index = 0,
        com = 'https://pic.netbian.com';
    addMsg(`${com} 爬取中...`)
    for (let i = 1; i <= page; i++) {
        let url = i == 1 ? `${com}/index.html` : `${com}/index_${i}.html`;
        let res = await axios.get(url);
        let $ = cheerio.load(res.data);//解析页面
        let slistEl = $('.slist');//找到元素列表
        slistEl.find('a').each(async (j, e) => {
            pagUrlList.push(`${com}${$(e).attr('href')}`);//获取到页面url列表
        })
    }
    pagUrlList.forEach(async (p, i) => {
        let pRes = await axios.get(p);
        let p$ = cheerio.load(pRes.data);//解析页面
        let imgEl = p$('.photo-pic').find('img');//找到元素列表
        let imgUrl = `${com}${imgEl.attr('src')}`;//获取图片url
        imgList.push(imgUrl);
        index++;
        //循环的次数等于列表长度时获取图片
        if (index == pagUrlList.length) {
            let dirStr = `${dir}/bian_${Date.now()}`;
            fs.mkdir(dirStr, (err) => { })
            downloadImg(imgList, dirStr);//下载图片
        }
    })
}

module.exports = bianWorm;

downloadImg.js

let fs = require('fs'),
    axios = require('axios'),
    { addMsg } = require("../store/index");

//下载图片
async function downloadImg(list, path) {
    if (list.length == 0) {
        addMsg('$END');
        return;
    }
    // console.log(list.length);
    for (let i = 0; i < list.length; i++) {
        let url = encodeURI(list[i]);//转码,防止url中文报错
        try {
            //计算下载的百分比
            let percent = ((i + 1) / list.length * 100).toFixed(2);
            let msgStr = `${percent}% 爬取中... ${url}`;
            addMsg(msgStr);
            if (i == list.length - 1) {
                msgStr = `图片爬取完成,共${list.length}项。`
                addMsg(msgStr);
                addMsg('$END');
            }
            let typeList = ['jpg', 'png', 'jpeg', 'gif', 'webp', 'svg', 'psd', 'bmp', 'tif', 'tiff', 'ico'];
            let type = typeList.find((item) => {
                return url.includes(item);
            });//获取图片类型
            (type == undefined) && (type = 'jpg');//判断type是否为undefined
            const imgPath = `${path}/${i + 1}.${type}`;//拼接本地路径
            const writer = fs.createWriteStream(imgPath);
            const response = await axios
                .get(url, { responseType: 'stream', timeout: 5000 }).catch(err => { });
            response.data.pipe(writer);
            await new Promise((resolve, reject) => {
                writer.on('finish', resolve);
                writer.on('error', reject);
            });

        } catch (error) { }
    }
}
module.exports = downloadImg;

值得注意的是需要保证准确获取图片资源的不同后缀。

随机生成人脸

这里可没有人脸算法之类的,调用的是https://thispersondoesnotexist.com/ 站点的接口,此接口每次刷新可生成不同人脸。
axios请求接口后,使用fs的createWriteStream创建可写流,将数据流写入文件中,下面是具体实现方法:
randomFace.js

let fs = require('fs'),
    axios = require('axios'),
    { addMsg } = require("../store/index");

//生成随机人脸
async function randomFace(dir, faceNum) {
    let com = 'https://thispersondoesnotexist.com';
    addMsg(`人脸生成中...`);
    let dirStr = `${dir}/randomFace_${Date.now()}`;
    fs.mkdir(dirStr, (err) => { })
    for (let i = 1; i <= faceNum; i++) {
        await axios.get(com, { responseType: 'stream' })
            .then((resp) => {
                const writer = fs.createWriteStream(`${dirStr}/${i}.jpg`);// 创建可写流
                resp.data.pipe(writer);// 将响应的数据流写入文件
                writer.on('finish', () => {
                    //计算下载的百分比
                    let percent = ((i) / faceNum * 100).toFixed(2);
                    let msgStr = `${percent}% 人脸生成中... ${dirStr}/${i}.jpg`;
                    addMsg(msgStr);
                    if (i == faceNum) {
                        msgStr = `人脸生成完成,共${faceNum}张。`
                        addMsg(msgStr);
                        addMsg('$END');
                    }
                });
                writer.on('error', (err) => { addMsg('$END'); });
            })
    }
}

module.exports = randomFace;

爬取指定页面所有图片

思路同上面获取壁纸类似,只不过这次是获取页面所有的img标签的src。
由于范围扩大到所有页面了,所以需要考虑的情况就会比较多。
有的src中是没有http或者https的,有的src使用的是相对路径,有的可能有中文字符,还有很多我没考虑到的情况。
所以并不能爬取任意页面的所有图片,比如页面加载过慢,或者用了懒加载、防盗链等技术。
下面是我实现的方法:
allWebImg.js

let fs = require('fs'),
    cheerio = require('cheerio'),
    axios = require('axios'),
    downloadImg = require("../utils/downloadImg.js"),
    { addMsg } = require("../store/index");

//网站所有图片
async function allWebImgWorm(dir, com) {
    let imgList = [];
    addMsg(`${com} 爬取中...`);
    let res = await axios.get(com).catch(err => { });
    if (!res) {
        addMsg('$END');
        return
    }
    let $ = cheerio.load(res.data);//解析页面
    //获取到页面所有图片标签组成的列表
    $('img').each(async (j, e) => {
        let imgUrl = e.attribs.src;//获取图片链接
        if (imgUrl) {
            !imgUrl.includes('https') && (imgUrl = `https:${imgUrl}`);//判断是否有https,没有则加上
            imgList.push(imgUrl);
        }
    })
    let dirStr = `${dir}/allWebImg_${Date.now()}`;
    fs.mkdir(dirStr, (err) => { })
    downloadImg(imgList, dirStr);//下载图片
}

module.exports = allWebImgWorm;

删除爬取的数据

使用fs.unlinkSync删除文件,fs.rmdirSync删除目录。
需要提前判断文件夹是否存在。
需要遍历文件,判断是否为文件。为文件则删除,否则递归遍历。
下面是我的方法:
deleteFiles.js

let fs = require('fs'),
    path = require('path');

//删除文件夹及文件夹下所有文件
const deleteFiles = (directory) => {
    if (fs.existsSync(directory)) {
        fs.readdirSync(directory).forEach((file) => {
            const filePath = path.join(directory, file);
            const stat = fs.statSync(filePath);
            if (stat.isFile()) {
                fs.unlinkSync(filePath);
            } else if (stat.isDirectory()) {
                deleteFiles(filePath);
            }
        });
        if (fs.readdirSync(directory).length === 0) {
            fs.rmdirSync(directory);
        }
    }
    fs.mkdir('./exportData', (err) => { })
};

module.exports = deleteFiles;

定时任务

项目中该功能正在开发中,只放了一个按钮,但思路已有了。
在node中的定时操作可用cron实现。
下面是一个小例子,每隔10秒打印一次1:

const cron = require('cron');

async function startTask() {
     let cronJob = new cron.CronJob(
    //秒、分、时、天、月、周
    //通配符:,(时间点)-(时间域)*(所有值)/(周期性,/之前的0与*等效)?(不确定)
     '0/10 * * * * *',
     async () => {
     console.log(1);
         },
         null,
         true,
         'Asia/Shanghai'//时区标识符
     );
};

注意事项

Server-Sent Events(SSE)

image
该项目中前后端数据交互接口大多使用的是get请求,但有一个除外,反显爬取进度的接口:/getTaskState。
该接口使用的是SSE,爬取的进度与链接是实时显示的。
最近火热的ChatGPT的流式输出(像人打字一样一个字一个字的显示)使用的便是这个。
SSE虽然与WebSocket一样都是长链接,但不同的是,WebSocket为双工通信(服务器与客户端双向通信),SSE为单工通信(只能服务器向客户端单向通信)。
项目中node服务端发送数据是这样的:

// 事件流获取任务状态
    app.get('/getTaskState', async (req, res, next) => {
        res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
        });
        let sendStr = ''//发送的消息
        let id = setInterval(() => {
            msgList = getMsg();
            //消息列表不为空且最后一条消息不等于上一次发送的消息才能执行
            if (msgList.length != 0 && msgList[msgList.length - 1] != sendStr) {
                sendStr = msgList[msgList.length - 1];
                console.log('\x1B[32m%s\x1B[0m', sendStr)
                res.write(`data: ${sendStr}\n\n`);//发送消息
            }
        }, 10);
        req.on('close', () => {
            clearMsg();//清空消息
            res.end();//结束响应
            clearInterval(id);//清除定时器(否则内存泄漏)
        });
    });

需要在res.writeHead中Content-Type设置为text/event-stream即表示使用SSE发送数据。
res.write(‘data: test\n\n’)即表示发送消息:test,每次发送消息需要以data:开头,\n\n结尾。
使用setInterval控制消息发送频率。
需要在服务端监听何时关闭,使用req.on(‘close’,()=>{})。
监听到关闭时执行响应结束res.end()与清除定时器clearInterval(id)。
在vue客户端接收数据是这样的:

//事件流获取任务状态
const getTaskState = () => {
  stateMsg.value = "";
  isState.value = true;
  let eventSource = new EventSource(origin.value + '/getTaskState');
  eventSource.onmessage = (event) => {
    if (event.data != '$END') {
      stateMsg.value = event.data;
    } else {
      eventSource.close();//关闭连接(防止浏览器3秒重连)
      stateMsg.value = '执行完成!是否打开数据文件夹?';
      isState.value = false;
      setTimeout(() => {
        confirm(stateMsg.value) &&
          axios.get(origin.value + '/openDir').then(res => { })//打开数据文件夹
      }, 100);
    }
  };
  //处理错误
  eventSource.onerror = (err) => {
    eventSource.close();//关闭连接
    stateMsg.value = ''
    isState.value = false;
  };
};

直接在方法中new一个EventSource(url),这是H5中新提出的对象,可用于接收服务器发送的事件流数据。
使用EventSource接收数据,直接在onmessage中获取event.data即可。
关闭连接记得使用eventSource.close()方法,因为服务器单方面关闭连接会触发浏览器3秒重连。
处理错误使用eventSource.onerror方法。
关于关闭SSE连接的时机,这是由node服务端决定的。
我在后端有一个store专门用于存储消息数据:
store/index.js

let msgList = [];//消息列表

function addMsg(msg) {
    msgList.push(msg);
}

function getMsg() {
    return msgList;
}

function clearMsg() {
    //清空msgList中元素
    msgList = [];
}

module.exports = {
    addMsg,
    getMsg,
    clearMsg
};
  1. 在爬取数据时,后端会计算爬取的进度,将生成的消息字符串push到msgList列表中,每隔10ms发送给前端msgList列表中的最后一个元素。
  2. 当后端数据爬取完成时会向msgList中push存入指定字符串:$END,表示获取完成。
  3. 当前端识别到获取的消息为$END时,关闭连接。
  4. 后端监听到前端连接被关闭,则后端也关闭连接。

pkg打包

全局安装pkg时最好网络环境为可访问github的环境,否则你只能手动下载那个失败的包再扔到指定路径。
pkg安装完成后需要在package.json中配置一番(主要是配置assets,将public与需要的依赖包打包进exe中)。
这是我的package.json配置:

{
  "name": "worm",
  "version": "0.1.3",
  "description": "",
  "bin": "./index.js",
  "scripts": {
    "start": "node-dev ./index.js",
    "dist": "node pkg-build.js"
  },
  "pkg": {
    "icon": "./public/img/icon.ico",
    "assets": [
      "public/**/*",
      "node_modules/axios/**/*.*",
      "node_modules/cheerio/**/*.*",
      "node_modules/cron/**/*.*",
      "node_modules/express/**/*.*"
    ]
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.27.2",
    "cheerio": "^1.0.0-rc.12",
    "cron": "^2.3.1",
    "express": "^4.18.2",
    "node-dev": "^8.0.0"
  }
}

我的打包命令是通过scripts中的dist在pkg-build.js中引入的,因为我需要将版本号输出在打包出的exe文件名中。
若打包命令直接写在package.json的scripts中会无法读取打包进程中项目的version。
这是我的pkg-build.js:

//只有通过node xxx.js方式执行的命令才能获取到package.json的version
const pkg = require('./package.json'),
    { execSync } = require('child_process');

const outputName = `dist/worm_v${pkg.version}.exe`;//拼接文件路径
const pkgCommand = `pkg . --output=${outputName} --target=win --compress=GZip`;//打包命令
execSync(pkgCommand);//执行打包命令

上面命令中的output表示输出路径(包含exe文件名),target表示打包的平台,compress表示压缩格式。
需要注意的是使用pkg打包时,项目中axios的版本不能太高。
否则即使你将axios写在pkg的打包配置里也无济于事,我使用的axios版本为0.27.2。

解决跨域

我node使用的是express,直接在header中配置Access-Control-Allow-Origin为* 即可。

app.all('*', (req, res, next) => {
        res.header("Access-Control-Allow-Origin", "*");//允许所有来源访问(设置跨域)
        res.header("Access-Control-Allow-Headers", "X-Requested-With,Content-Type");//允许访问的响应头
        res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");//允许访问的方法
        res.header("X-Powered-By", ' 3.2.1');//响应头
        res.header("Content-Type", "application/json;charset=utf-8");//响应类型
        next();
    });

child_process模块

众所周知,node是单线程运行的,在主线程中执行大量计算任务时会产生无响应的问题。
但node内置的child_process模块却可以创建新的进程,在新进程中执行操作不会影响到主进程的运行。
在此项目中自动打开浏览器、打开指定文件夹、执行打包命令用的就是它。

// 打开数据文件夹
app.get('/openDir', (req, res) => {
    res.send('ok');
   //打开文件夹,exe环境下需要使用exe所在目录
    let filePath = isPkg ?
        `${path.dirname(process.execPath)}${dir.replace('./', '\\')}` :
        path.resolve(__dirname, dir);
    exec(`start ${filePath}`);
});

//监听端口
app.listen(port, () => {
    let url = `http://${ipStr}:${port}`;
    isPkg && exec(`start ${url}`);//打包环境下自动打开浏览器
    //判断是否存在exportData文件夹,没有则创建
    fs.exists(dir, async (exists) => {
        !exists && fs.mkdir(dir, (err) => { });
    })
    console.log(
        '\x1B[31m%s\x1B[0m',
        `\n
${time} 爬虫服务开启!\n
运行过程中禁止点击此窗口!\n
如需关闭爬虫关闭此窗口即可!\n`
    );
});

设置静态资源

前端使用vue开发时,需要将vue.config.js中的publicPath配置设置为./之后再打包。
将vue打包后dist内的文件拷贝到node项目的public目录下。
需要在express设置请求头之前使用static(path.join(__dirname, ‘./public’))设置静态资源:

const app = express();
const isPkg = process.pkg;//判断是否为打包环境
const port = isPkg ? 2222 : 1111;//端口
const ipStr = getLocalIp();//获取本机ip
let time = getFormatTime();//获取格式化时间
let dir = './exportData';
app.use(express.json());//解析json格式
app.use(express.static(path.join(__dirname, './public')));//设置静态资源
app.all('*', (req, res, next) => {
    res.header("Access-Control-Allow-Origin", "*");//允许所有来源访问(设置跨域)
    res.header("Access-Control-Allow-Headers", "X-Requested-With,Content-Type");//允许访问的响应头
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");//允许访问的方法
    res.header("X-Powered-By", ' 3.2.1');//响应头
    res.header("Content-Type", "application/json;charset=utf-8");//响应类型
    next();
});

关于运行时的黑框

双击exe时,不仅会弹出浏览器用户页面,还会弹出黑框,点击黑框内部还会暂停程序运行。
我有想过使用pm2守护进程干掉黑框,但想到关闭爬虫时只需关闭黑框即可,便留下了黑框。
image

限制爬取次数

做人,特别是做开发,你得有道德,你把人家网站给玩儿崩了这好吗(′⌒`)?
没有任何东西是无限制的,我的限制是放在前端的(可能不太严谨),以爬取壁纸为例,调用inputLimit(num),入参为执行次数,方法是这样的:

//输入限制
const inputLimit = (pageNum) => {
  let val = prompt(`输入执行次数(小于等于${pageNum})`, "");
  if (val == null || isNaN(val) || parseInt(val) < 1 || parseInt(val) > pageNum) {
    return false;
  }
  return parseInt(val);
};

//彼岸壁纸
const bianWorm = () => {
  let val = inputLimit(10);
  if (val) {
    axios.get(origin.value + '/bianWorm?pageNum=' + val).then(res => { });
    getTaskState();
  }
};

后端获取到pageNum参数后,以此作为执行爬虫逻辑的循环依据。

结语

这是我第一次用js玩儿爬虫,很多地方可能不太完善,还请大佬们指出,谢谢啦!
此项目仅供学习研究,勿作他用。

原文链接:https://xiblogs.top/?id=60

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

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

相关文章

【云原生】kubernetes中容器的资源限制

目录 1 metrics-server 2 指定内存请求和限制 3 指定 CPU 请求和限制 资源限制 在k8s中对于容器资源限制主要分为以下两类: 内存资源限制: 内存请求&#xff08;request&#xff09;和内存限制&#xff08;limit&#xff09;分配给一个容器。 我们保障容器拥有它请求数量的…

【Spring专题】Spring之Bean的生命周期源码解析——阶段一(扫描生成BeanDefinition)

目录 前言阅读指引阅读建议 课程内容一、生成BeanDefinition1.1 简单回顾1.2 概念回顾1.3 核心方法讲解 二、方法讲解2.1 ClassPathBeanDefinitionScanner#scan2.2 ClassPathBeanDefinitionScanner#doScan2.3 ClassPathScanningCandidateComponentProvider#findCandidateCompon…

SpringBoot 整合JDBC

SpringData简介 Sping Data 官网&#xff1a;https://spring.io/projects/spring-data数据库相关的启动器 &#xff1a;可以参考官方文档&#xff1a;https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#using-boot-starter 整合JDBC 创建测试项目测试数据…

访企聚力促创新:长安大学来访闪马智能

7月31日&#xff0c;长安大学运输工程学院院长葛颖恩教授、学院副书记李婷以及学办主任董彬一行来访闪马智能&#xff0c;闪马智能创始人兼CEO彭垚、城市交通行业总经理兼营销副总裁詹诚以及公共交通行业总经理熊天圣等出席了本次交流会。 长安大学运输工程学院院长葛颖恩教授…

人人都是系统装机高手,利用windows官方的工具,安装超简单

前言 电脑出故障了或者C盘文件饱满、电脑系统卡顿&#xff0c;第一个想法就是重装系统&#xff0c;在网上搜一下&#xff0c;各种重装系统的镜像层出不穷&#xff0c;该怎么去选择呢&#xff1f;我适合很多方法&#xff0c;最后选择了微软官方的系统安装工具。因为系统纯净&am…

LeetCode209. 长度最小的子数组

题目&#xff1a;LeetCode209. 长度最小的子数组 描述&#xff1a; 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl1, …, numsr-1, numsr] &#xff0c;并返回其长度。如果不存在符合条件的子…

渠道订货管理:品牌商建立渠道连接的纽带

当品牌商&#xff08;厂商&#xff09;渠道拓展到一定规模&#xff0c;处理不同渠道交易数据&#xff0c;面对信息流、物流、资金流链路&#xff0c;提升厂商端、经销商端、终端门店的订货体验就会变得尤为重要&#xff0c;特别是一些实力级厂商&#xff0c;渠道下沉能够掌握终…

[保研/考研机试] KY56 数制转换 北京大学复试上机题 C++实现

题目链接&#xff1a; 数制转换https://www.nowcoder.com/share/jump/437195121691734210665 描述 求任意两个不同进制非负整数的转换&#xff08;2进制&#xff5e;16进制&#xff09;&#xff0c;所给整数在long所能表达的范围之内。 不同进制的表示符号为&#xff08;0&a…

Paragon NTFS15安装包下载免费版ntfs磁盘读写工具

Paragon NTFS for Mac 15 简体中文标准版轻量级的快捷菜单&#xff0c;可执行挂载、卸载和验证操作。软件设计优秀&#xff0c;与 macOS 无缝衔接&#xff0c;操作简单。轻量级的快捷菜单&#xff0c;可执行挂载、卸载和验证操作。软件设计优秀&#xff0c;与 macOS 无缝衔接&a…

安卓13不再支持PPTP怎么办?新的连接解决方案分享

随着Android 13的发布&#xff0c;我们迎来了一个令人兴奋的新品时刻。然而&#xff0c;对于一些用户而言&#xff0c;这也意味着必须面对一个重要的问题&#xff1a;Android 13不再支持PPTP协议。如果你是一个习惯使用PPTP协议来连接换地址的用户&#xff0c;那么你可能需要重…

【BASH】回顾与知识点梳理(二十二)

【BASH】回顾与知识点梳理 二十二 二十二. Linux 账号管理22.1 Linux 的账号与群组使用者标识符&#xff1a; UID 与 GID使用者账号/etc/passwd 文件结构/etc/shadow 文件结构 关于群组&#xff1a; 有效与初始群组、groups, newgrp/etc/group 文件结构有效群组(effective grou…

技术分享 | 如何编写同时兼容 Vue2 和 Vue3 的代码?

LigaAI 的评论编辑器、附件展示以及富文本编辑器都支持在 Vue2&#xff08;Web&#xff09;与 Vue3&#xff08;VSCode、lDEA&#xff09;中使用。这样不仅可以在不同 Vue 版本的工程中间共享代码&#xff0c;还能为后续升级 Vue3 减少一定阻碍。 那么&#xff0c;同时兼容 Vue…

爬虫017_urllib库_get请求的quote方法_urlencode方法_---python工作笔记036

按行来看get请求方式 比如这个地址 上面这个地址复制粘贴过来以后 可以看到周杰伦变成了一堆的Unicode编码了 所以这个时候我们看,我们说https这里,用了UA反爬,所以这里 我们构建一个自定义的Request对象,里面要包含Us

【kubectl详解】

目录 一、陈述式资源管理方法二、基本信息查看1、查看 master 节点状态2、查看命名空间3、查看命名空间的所有资源4、创建命名空间app5、删除命名空间app6、在命名空间kube-public 创建副本控制器&#xff08;deployment&#xff09;来启动Pod&#xff08;nginx-dz&#xff09;…

优思学院|质量第一的目的是什么?

国外有一句很著名的话&#xff1a;Quality comes first, profit is its logical sequence&#xff0c;意思是&#xff1a;质量第一&#xff0c;利润是其合理的结果&#xff0c;这句话也是很多公司或者商店使用的标语。 简而言之&#xff0c;只要你把质量放在第一位&#xff0c…

Windows下安装Sqoop

Windows下安装Sqoop 一、Sqoop简介二、Sqoop安装2.1、Sqoop官网下载2.2、Sqoop网盘下载2.3、Sqoop安装&#xff08;以version&#xff1a;1.4.7为例&#xff09;2.3.1、解压安装包到 D:\bigdata\sqoop\1.4.7 目录2.3.2、新增环境变量 SQOOP_HOME2.3.3、环境变量 Path 添加 %SQO…

07-3_Qt 5.9 C++开发指南_文件目录操作

文章目录 1. 文件目录操作相关的类2. 实例概述2.1 实例功能2.2 信号发射信息的获取 3. QCoreApplication 类4. QFile类5. QFileInfo类6. QDir类7. QTemporaryDir 和QTemporaryFile8. QFileSystemWatcher 类9. 框架和源码9.1 可视化UI设计9.2 dialog.cpp 1. 文件目录操作相关的类…

【网络基础实战之路】基于三个分公司的内网搭建并连接运营商的实战详解

系列文章传送门&#xff1a; 【网络基础实战之路】设计网络划分的实战详解 【网络基础实战之路】一文弄懂TCP的三次握手与四次断开 【网络基础实战之路】基于MGRE多点协议的实战详解 【网络基础实战之路】基于OSPF协议建立两个MGRE网络的实验详解 PS&#xff1a;本要求基于…

Qt应用开发(基础篇)——拆分器窗口 QSplitter

一、前言 QSplitter继承于QFrame&#xff0c;QFrame继承于QWidget&#xff0c;是Qt的一个基础工具类。 框架类QFrame介绍 QSplitter拆分器&#xff0c;用户通过拖动子部件之间的边界来控制子部件的大小&#xff0c;在应用开发中数据分模块展示、图片展示等场景下使用。 二、QSp…

适合大学生的兼职有什么?推荐四个靠谱的副业!

不知不觉新学期也要开始了&#xff0c;大学生们会不会因为苦恼的钱不够花而不够尽兴&#xff0c;很多大学生都想利用下课的时间来做一份兼职&#xff0c;但是有很多鉴于学校封校不知道自己可以做什么&#xff0c;我来为大家整理了以下几个大学生在学校内相对可靠的兼职。 第一个…