剑走偏锋,无头浏览器是什么神奇的家伙

news2025/2/8 10:29:38

大家好,我是风筝,公众号「古时的风筝」,专注于 Java技术 及周边生态。
我的个人网站:古时的风筝

在这里插入图片描述

浏览器是再熟悉不过的东西了,几乎每个人用过,比如 Chrome、FireFox、Safari,尤其是我们程序员,可谓开发最强辅助,摸鱼最好的伴侣。

浏览器能干的事儿,无头浏览器都能干,而且很多时候比标准浏览器还要更好用,而且能实现一些很好玩儿的功能,我们能借助无头浏览器比肩标准浏览器强大的功能,而且又能灵活的用程序控制的特性,做出一些很有意思的产品功能来,稍后我们细说。

什么是浏览器

关于浏览器还有一个很好玩儿的梗,对于一些对计算机、对互联网不太了解的同学,你跟他说浏览器,他/她就默认是百度了,因为好多小白的浏览器都设置了百度为默认页面。所以很多小白将浏览器和搜索引擎(99%是百度)划等号了。

浏览器里我百分之99的时间都是用 Chrome,不过有一说一,这玩意是真耗内存,我基本上是十几、二十几个的 tab 开着,再加上几个 IDEA 进程,16G 的内存根本就不够耗的。

以 Chrome 浏览器为例,Chrome 由以下几部分组成:

  1. 渲染引擎(Rendering Engine):Chromium使用的渲染引擎主要有两个选项:WebKit和Blink。WebKit是最初由苹果开发的渲染引擎,后来被Google采用并继续开发。Blink则是Google从WebKit分支出来并进行独立开发的渲染引擎,目前Chromium主要使用Blink作为其默认的渲染引擎。
  2. JavaScript引擎(JavaScript Engine):Chromium使用V8引擎作为其JavaScript引擎。V8是由Google开发的高性能JavaScript引擎,它负责解析和执行网页中的JavaScript代码。
  3. 网络栈(Network Stack):Chromium的网络栈负责处理网络通信。它支持各种网络协议,包括HTTP、HTTPS、WebSocket等,并提供了网络请求、响应处理和数据传输等功能。
  4. 布局引擎(Layout Engine):Chromium使用布局引擎来计算网页中元素的位置和大小,并确定它们在屏幕上的布局。布局引擎将CSS样式应用于DOM元素,并计算它们的几何属性。
  5. 绘制引擎(Painting Engine):绘制引擎负责将网页内容绘制到屏幕上,生成最终的图像。它使用图形库和硬件加速技术来高效地进行绘制操作。
  6. 用户界面(User Interface):Chromium提供了用户界面的支持,包括地址栏、标签页、书签管理、设置等功能。它还提供了扩展和插件系统,允许用户根据自己的需求进行个性化定制。
  7. 其他组件:除了上述主要组件外,Chromium还包括其他一些辅助组件,如存储系统、安全模块、媒体处理、数据库支持等,以提供更全面的浏览器功能。

Chrome 浏览器光源码就有十几个G,2000多万行代码,可见,要实现一个功能完善的浏览器是一项浩大的工程。

什么是无头浏览器

无头浏览器(Headless Browser)是一种浏览器程序,没有图形用户界面(GUI),但能够执行与普通浏览器相似的功能。无头浏览器能够加载和解析网页,执行JavaScript代码,处理网页事件,并提供对DOM(文档对象模型)的访问和操作能力。

与传统浏览器相比,无头浏览器的主要区别在于其没有可见的窗口或用户界面。这使得它在后台运行时,不会显示实际的浏览器窗口,从而节省了系统资源,并且可以更高效地执行自动化任务。

常见的无头浏览器包括Headless Chrome(Chrome的无头模式)、PhantomJS、Puppeteer(基于Chrome的无头浏览器库)等。它们提供了编程接口,使开发者能够通过代码自动化控制和操作浏览器行为。

无头浏览器其实就是看不见的浏览器,所有的操作都要通过代码调用 API 来控制,所以浏览器能干的事儿,无头浏览器都能干,而且很多事儿做起来比标准的浏览器更简单。

我举几个常用的功能来说明一下无头浏览器的主要使用场景

  1. 自动化测试: 无头浏览器可以模拟用户行为,执行自动化测试任务,例如对网页进行加载、表单填写、点击按钮、检查页面元素等。
  2. 数据抓取: 无头浏览器可用于爬取网页数据,自动访问网站并提取所需的信息,用于数据分析、搜索引擎优化等。
  3. 屏幕截图: 无头浏览器可以加载网页并生成网页的截图,用于生成快照、生成预览图像等。
  4. 服务器端渲染: 无头浏览器可以用于服务器端渲染(Server-side Rendering),将动态生成的页面渲染为静态HTML,提供更好的性能和搜索引擎优化效果。
  5. 生成 PDF 文件:使用浏览器自带的生成 PDF 功能,将目标页面转换成 PDF 。

使用无头浏览器做一些好玩的功能

开篇就说了使用无头浏览器可以实现一些好玩儿的功能,这些功能别看不大,但是使用场景还是很多的,有些开发者就是抓住这些小功能,开发出好用的产品,运气好的话还能赚到钱,尤其是在国外市场。(在国内做收费的产品确实不容易赚到钱)

下面我们就来介绍两个好玩儿而且有用的功能。

前面的自动化测试、服务端渲染就不说了。

自动化测试太专业了,一般用户用不到,只有开发者或者测试工程师用。

服务端渲染使用无头浏览器确实没必要,因为有太多成熟的方案了,连 React 都有服务端渲染的能力(RSC)。

网页截图功能

我们可能见过一些网站提供下载文字卡片或者图文卡片的功能。比如读到一段想要分享的内容,选中之后将文本端所在的区域生成一张图片。

其实就是通过调用浏览器自身的 API page.screenshot,可以对整个页面或者选定的区域生成图片。

通过这个方法,我们可以做一个浏览器插件,用户选定某个区域后,直接生成对应的图片。这类功能在手机APP上很常见,在浏览器上一搬的网站都不提供。

说到这儿好像和无头浏览器都没什么关系吧,这都是标准浏览器中做的事儿,用户已经打开了页面,在浏览器上操作自己看到的内容,顺理成章。

但是如果这个操作是批量的呢,或者是在后台静默完成的情况呢?

那就需要无头浏览器来出手了,无头浏览器虽然没有操作界面,但是也具备绘制引擎的完整功能,仍然可以生成图像,利用这个功能,就可以批量的、静默生成图像了,并且可以截取完整的网页或者部分区域。

Puppeteer 是无头浏览器中的佼佼者,提供了简单好用的 API ,不过是 nodejs 版的。

如果是用 Java 开发的话,有一个替代品,叫做 Jvppeteer,提供了和 Puppeteer 几乎一模一样的 API。

下面这段代码就展示了如何用 Jvppeteer 来实现网页的截图。

下面这个方法是对整个网页进行截图,只需要给定网页 url 和 最终的图片路径就可以了。

public static boolean screenShotWx(String url, String path) throws IOException, ExecutionException, InterruptedException {
  BrowserFetcher.downloadIfNotExist(null);
  ArrayList<String> arrayList = new ArrayList<>();
  // MacOS 要这样写,指定Chrome的位置
  String executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
  LaunchOptions options = new LaunchOptionsBuilder().withExecutablePath(executablePath).withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
  // Windows 和 Linux 这样就可以,不用指定 Chrome 的安装位置
  //LaunchOptions options = new LaunchOptionsBuilder().withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
  arrayList.add("--no-sandbox");
  arrayList.add("--disable-setuid-sandbox");
  arrayList.add("--ignore-certificate-errors");
  arrayList.add("--disable-gpu");
  arrayList.add("--disable-web-security");
  arrayList.add("--disable-infobars");
  arrayList.add("--disable-extensions");
  arrayList.add("--disable-bundled-ppapi-flash");
  arrayList.add("--allow-running-insecure-content");
  arrayList.add("--mute-audio");
  Browser browser = Puppeteer.launch(options);
  Page page = browser.newPage();
  page.setJavaScriptEnabled(true);
  page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37");
  page.setCacheEnabled(true);
  page.onConsole((msg) -> {
    log.info("==> {}", msg.text());
  });


  PageNavigateOptions pageNavigateOptions = new PageNavigateOptions();
  pageNavigateOptions.setTimeout(1000000);
  //dom加载完毕就算导航完成
  pageNavigateOptions.setWaitUntil(Collections.singletonList("domcontentloaded"));
  page.goTo(url, pageNavigateOptions, true);

  autoScroll(page);
  ElementHandle body = page.$("body");
  double width = body.boundingBox().getWidth();
  double height = body.boundingBox().getHeight();
  Viewport viewport = new Viewport();

  viewport.setWidth((int) width); // 设置视口宽度
  viewport.setHeight((int) height + 100); // 设置视口高度
  page.setViewport(viewport);
  ScreenshotOptions screenshotOptions = new ScreenshotOptions();
  screenshotOptions.setType("jpeg");
  screenshotOptions.setFullPage(Boolean.FALSE);
  //screenshotOptions.setClip(clip);
  screenshotOptions.setPath(path);
  screenshotOptions.setQuality(100);
  // 或者转换为 base64
  //String base64Str = page.screenshot(screenshotOptions);
  //System.out.println(base64Str);

  browser.close();
  return true;
}

一个自动滚屏的方法。

虽然可以监听页面上的事件通知,比如 domcontentloaded,文档加载完成的通知,但是很多时候并不能监听到网页上的所有元素都加载完成了。对于那些滚动加载的页面,可以用这种方式模拟完全加载,加载完成之后再进行操作就可以了。

使用自动滚屏的操作,可以模拟我们人为的在界面上下拉滚动条的操作,随着滚动条的下拉,页面上的元素会自然的加载,不管是同步的还有延迟异步的,比如图片、图表等。

private static void autoScroll(Page page) {
  if (page != null) {
    try {
      page.evaluate("() => {\n" +
                    "   return new Promise((resolve, reject) => {\n" +
                    "            //滚动的总高度\n" +
                    "            let totalHeight = 0;\n" +
                    "            //每次向下滚动的高度 500 px\n" +
                    "            let distance = 500;\n" +
                    "            let k = 0;\n" +
                    "            let timeout = 1000;\n" +
                    "            let url = window.location.href;\n" +
                    "            let timer = setInterval(() => {\n" +
                    "                //滚动条向下滚动 distance\n" +
                    "                window.scrollBy(0, distance);\n" +
                    "                totalHeight += distance;\n" +
                    "                k++;\n" +
                    "                console.log(`当前第${k}次滚动,页面高度: ${totalHeight}`);\n" +
                    "                //页面的高度 包含滚动高度\n" +
                    "                let scrollHeight = document.body.scrollHeight;\n" +
                    "                //当滚动的总高度 大于 页面高度 说明滚到底了。也就是说到滚动条滚到底时,以上还会继续累加,直到超过页面高度\n" +
                    "                if (totalHeight >= scrollHeight || k >= 200) {\n" +
                    "                    clearInterval(timer);\n" +
                    "                    resolve();\n" +
                    "                    window.scrollTo(0, 0);\n" +
                    "                }\n" +
                    "            }, timeout);\n" +
                    "        })\n" +
                    "  }");
    } catch (Exception e) {

    }
  }
}

调用截图方法截图,这里是对一篇公众号文章进行整个网页的截图。

public static void main(String[] args) throws Exception {
  screenShotWx("https://mp.weixin.qq.com/s/MzCyWqcH1TCytpnHI8dVjA", "/Users/fengzheng/Desktop/PICTURE/wx.jpeg");
}

或者也可以截取页面中的部分区域,比如某篇文章的正文部分,下面这个方法是截图一个博客文章的正文部分。

public static boolean screenShotJueJin(String url, String path) throws IOException, ExecutionException, InterruptedException {
  BrowserFetcher.downloadIfNotExist(null);
  ArrayList<String> arrayList = new ArrayList<>();
  String executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
  LaunchOptions options = new LaunchOptionsBuilder().withExecutablePath(executablePath).withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();

  //LaunchOptions options = new LaunchOptionsBuilder().withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
  arrayList.add("--no-sandbox");
  arrayList.add("--disable-setuid-sandbox");
  Browser browser = Puppeteer.launch(options);
  Page page = browser.newPage();

  PageNavigateOptions pageNavigateOptions = new PageNavigateOptions();
  pageNavigateOptions.setTimeout(1000000);
  //dom加载完毕就算导航完成
  pageNavigateOptions.setWaitUntil(Collections.singletonList("domcontentloaded"));
  page.goTo(url, pageNavigateOptions, true);

  WaitForSelectorOptions waitForSelectorOptions = new WaitForSelectorOptions();
  waitForSelectorOptions.setTimeout(1000 * 15);
  waitForSelectorOptions.setVisible(Boolean.TRUE);
  // 指定截图的区域
  ElementHandle elementHandle = page.waitForSelector("article.article", waitForSelectorOptions);
  Clip clip = elementHandle.boundingBox();
  Viewport viewport = new Viewport();
  ElementHandle body = page.$("body");
  double width = body.boundingBox().getWidth();
  viewport.setWidth((int) width); // 设置视口宽度
  viewport.setHeight((int) clip.getHeight() + 100); // 设置视口高度
  page.setViewport(viewport);
  ScreenshotOptions screenshotOptions = new ScreenshotOptions();
  screenshotOptions.setType("jpeg");
  screenshotOptions.setFullPage(Boolean.FALSE);
  screenshotOptions.setClip(clip);
  screenshotOptions.setPath(path);
  screenshotOptions.setQuality(100);
  // 或者生成图片的 base64编码
  String base64Str = page.screenshot(screenshotOptions);
  System.out.println(base64Str);
  return true;
}

调用方式:

public static void main(String[] args) throws Exception {
  screenShotJueJin("https://juejin.cn/post/7239715628172902437", "/Users/fengzheng/Desktop/PICTURE/juejin.jpeg");
}

最后的效果是这样的,可以达到很清晰的效果。

网页生成 PDF 功能

这个功能可太有用了,可以把一些网页转成离线版的文档。有人说直接保存网页不就行了,除了程序员,大部分人还是更能直接读 PDF ,而不会用离线存储的网页。

我们可以在浏览器上使用浏览器的「打印」功能,用来将网页转换成 PDF 格式。

但这是直接在页面上操作,如果是批量操作呢,比如想把一个专栏的所有文章都生成 PDF呢,就可以用无头浏览器来做了。

有的同学说,用其他的库也可以呀,Java 里面有很多生成 PDF 的开源库,可以把 HTML 转成 PDF,比如Apache PDFBox、IText 等,但是这些库应对一般的场景还行,对于那种页面上有延迟加载的图表啊、图片啊、脚本之类的就束手无策了。

而无头浏览器就可以,你可以监听页面加载完成的事件,可以模拟操作,主动触发页面加载,甚至还可以在页面中添加自定义的样式、脚本等,让生成的 PDF 更加完整、美观。

下面这个方法演示了如何将一个网页转成 PDF 。

public static boolean pdf(String url, String savePath) throws Exception {
  Browser browser = null;
  Page page = null;
  try {
    //自动下载,第一次下载后不会再下载
    BrowserFetcher.downloadIfNotExist(null);
    ArrayList<String> arrayList = new ArrayList<>();
    // MacOS
    String executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
    LaunchOptions options = new LaunchOptionsBuilder().withExecutablePath(executablePath).withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
		// windows 或 linux
    //LaunchOptions options = new LaunchOptionsBuilder().withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();

    arrayList.add("--no-sandbox");
    arrayList.add("--disable-setuid-sandbox");
    arrayList.add("--ignore-certificate-errors");
    arrayList.add("--disable-gpu");
    arrayList.add("--disable-web-security");
    arrayList.add("--disable-infobars");
    arrayList.add("--disable-extensions");
    arrayList.add("--disable-bundled-ppapi-flash");
    arrayList.add("--allow-running-insecure-content");
    arrayList.add("--mute-audio");

    browser = Puppeteer.launch(options);
    page = browser.newPage();

    page.onConsole((msg) -> {
      log.info("==> {}", msg.text());
    });

    page.setViewport(viewport);
    page.setJavaScriptEnabled(true);
    page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37");
    page.setCacheEnabled(true);

    //设置参数防止检测
    page.evaluateOnNewDocument("() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => undefined } }) }");
    page.evaluateOnNewDocument("() =>{ window.navigator.chrome = { runtime: {},  }; }");
    page.evaluateOnNewDocument("() =>{ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); }");
    page.evaluateOnNewDocument("() =>{ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5,6], }); }");

    PageNavigateOptions pageNavigateOptions = new PageNavigateOptions();
    pageNavigateOptions.setTimeout(1000000);
    //dom加载完毕就算导航完成
 pageNavigateOptions.setWaitUntil(Collections.singletonList("domcontentloaded"));

    page.goTo(url, pageNavigateOptions, true);
    // 添加自定义演示
    StyleTagOptions styleTagOptions1 = new StyleTagOptions();
    styleTagOptions1.setContent("html {-webkit-print-color-adjust: exact} .table > table > tr:nth-child(1),.table > table > tr:nth-child(2) {background: #4074b0;} #tableB td:nth-child(2) {width:60%;}");
    page.addStyleTag(styleTagOptions1);

    //滚屏
    autoScroll(page);
    Thread.sleep(1000);

    PDFOptions pdfOptions = new PDFOptions();
    //            pdfOptions.setHeight("5200");
    pdfOptions.setPath(savePath);
    page.pdf(pdfOptions);

  } catch (Exception e) {
    log.error("生成pdf异常:{}", e.getMessage());
    e.printStackTrace();
  } finally {
    if (page != null) {
      page.close();
    }
    if (browser != null) {
      browser.close();
    }
  }
  return true;
}

调用生成 PDF 的方法,将一个微信公众号文章转成 PDF。

    public static void main(String[] args) throws Exception {
        String pdfPath = "/Users/fengzheng/Desktop/PDF";
        String filePath = pdfPath + "/hello.pdf";
			 JvppeteerUtils.pdf("https://mp.weixin.qq.com/s/MzCyWqcH1TCytpnHI8dVjA", filePath);
    }

最终的效果,很清晰,样式都在,基本和页面一模一样。

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

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

相关文章

ASEMI代理台湾光宝LTV-3120光耦合器中文资料

编辑-Z LTV-3120是一种高性能光耦&#xff0c;由于其可靠性、效率和多功能性&#xff0c;在各种应用中都很受欢迎。本文将全面了解LTV-3120其功能、应用以及它如何改进您的电子设计。 什么是光电耦合器&#xff1f; 光耦&#xff0c;也称为光隔离器&#xff0c;是一种利用光在…

时序数据库的流计算支持

一、时序数据及其特点 时序数据&#xff08;Time Series Data&#xff09;是基于相对稳定频率持续产生的一系列指标监测数据&#xff0c;比如一年内的道琼斯指数、一天内不同时间点的测量气温等。时序数据有以下几个特点&#xff1a; 历史数据的不变性数据的有效性数据的时效…

Linux:cp、mv、rm命令

1、cp命令&#xff1a;用于复制文件/文件夹&#xff0c;语法&#xff1a; cp [-r] 参数1 参数2 r参数可选&#xff0c;用于对文件夹进行复制&#xff0c;表示递归 参数1&#xff1a;Linux路径&#xff0c;表示被复制的文件或文件夹 参数2&#xff1a;Linux&#xff0c;表示要复…

数据库期末复习第一章(看完包过)

目录 第一章 第一章 主要是一些概念&#xff0c;了解几个基本概念&#xff0c;并且记住英文缩写 DB是数据库&#xff08;database&#xff09; 0.数据 Data 1.数据库 DB &#xff08;database 数据库&#xff09; 2.数据库管理系统 DBMS &#xff08;M&#xff1a;mange 管…

论文解读 | 基于蒙特卡罗树搜索的触觉目标识别主动末端执行器姿态选择

原创 | 文 BFT机器人 本论文提出了一种新颖的方法&#xff0c;使用仅触觉来进行主动物体识别。该算法通过蒙特卡罗树搜索来选择最佳的手腕姿态序列进行物体识别。 具体来说&#xff0c;该算法将问题建模为马尔可夫决策过程&#xff08;MDP&#xff09;&#xff0c;并通过观察和…

记一次简单的SQL调优

记一次简单的SQL调优 前言排查分析问题分析SQL的执行流程存在的问题优化思路 验证解决验证执行流程一些执行计划的基本知识 优化先执行where条件过滤不需要的行&#xff0c;再进行左连接提示排序走索引为常用查询条件创建索引调整查询条件的顺序 前言 相信大家对后端数据库的S…

【Uniapp】小程序携带Token请求接口+无感知登录方案2.0

本次改进原文《【Uniapp】小程序携带Token请求接口无感知登录方案》&#xff0c;在实际使用过程中我发现以下bug&#xff1a; 若token恰好在用户访问接口时到期&#xff0c;就会直接查询为空&#xff0c;不反映token过期问题&#xff08;例如&#xff1a;弹窗显示订单查询记录…

使用python脚本将视频素材转化为图片数据集并打标

1.前言 在深度学习的训练过程中&#xff0c;第一步要做的制作数据集。本文所介绍的内容就是在面临视频素材时&#xff0c;如何将这个视频素材转换成图片&#xff0c;并将生成的图片给打上标签。 2.视频转图片的代码展示 # codingutf-8 import cv2 import osroot "F:\H…

MySQL数据库及基础操作

MySQL数据库 一、数据库的基本概念二、数据库发展史1、第一代数据库2、第二代数据库3、第三代数据库 三、当今主流数据库介绍四、关系型数据库1、结构2、理解关系数据库 五、非关系数据库六、数据库基本操作1、常用的数据类型2、查看数据库结构3、SQL语句3.1 DDL3.2 DML3.3 DQL…

pycharm 命令行创建版本库

pycharm中已建好的项目如何上传自已的git服务器中 通过命令行创建一个新的版本库 touch README.mdgit initgit add README.mdgit commit -m "first commit"git remote add origin ssh://XXXXip:29418/newbb.gitgit push -u origin master 通过命令行推送一个已存在的…

数据挖掘课件01-07

1、数挖的定义、产生原因&#xff0c;解决问题 数据挖掘就是寻找数据中隐含的知识并用于产生商业价值。 数据挖掘产生动因&#xff1a;海量数据、维度众多、问题复杂 解决问题&#xff1a;分类问题&#xff0c;聚类问题、回归问题、关联问题、 分类算法&#xff1a;C4.5&…

微信如何设置快速回复?

目前微信线上沟通已经成为绝大多数人士的首选&#xff0c;尤其不管是企业还是个人&#xff0c;都会利用微信打造私域。而做微信沟通的话&#xff0c;大家最为关心的就是怎么做到最快捷的回复对方。 可以看看这个功能&#xff0c;提前设置好常见问题的回复话术&#xff0c;可个…

JS + 浮动 + 递归实现图片瀑布流懒加载

思路 页面 pege 分成左浮动的数列 lineBox&#xff0c;每列中图片 sinImg 依次向下摆放每加载一张图片时&#xff0c;判断页面中哪列的高度最小&#xff0c;将当前图片放到最小的那列尾部监听当前图片 onload 事件&#xff0c;当前图片加载完成后&#xff0c;再加载下一张图片…

【python】前方弹幕高能:教你一键实现自动化指定直播间发送弹幕,为你喜欢的女主播疯狂打call叭~

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 不知道你们平时看不看直播&#xff0c;每次看到界面中的滚动弹幕&#xff0c;还挺有意思。 无中生事—— 前几天看到喜欢的主播打比赛呢~ 精彩时刻恨不得双手打字打call的飞快&#xff0c;那激动的无语轮次了都&#…

Android生态下的Kotlin有哪些更新#GoogleIO 2023

Android生态下Kotlin有哪些更新#GoogleIO 2023 自Android官方宣布Kotlin作为Android开发的第一语言&#xff0c;过去将近6年时间。下面是GoogleIO 2023宣布的Android生态的4项重要更新。 Kotlin编译器2.0版本 如果你是一位Kotlin开发者&#xff0c;你可能会对听到这个消息感…

PMP®增持CSPM-2等级证书,免重新考试与学习,申请简单!

好消息&#xff01;好消息&#xff01;好消息&#xff01; 2023年起&#xff0c;持有PMP证书的朋友可以直接增持一个同等级证书CSPM-2&#xff0c;不用重新考试&#xff0c;不用重新学习&#xff0c;原PMP证书不影响正常使用&#xff0c;相当于多了一个国标项目管理领域的证书…

前端和后端分别是什么?

从技术工具来看&#xff1a; 前端&#xff1a;常见的 html5、JavaScript、jQuery... 后端&#xff1a;spring、tomcet、JVM&#xff0c;MySQL... 毕竟&#xff0c;如果这个问题问一个老后端&#xff0c;他掰掰手指可以给你罗列出一堆的名词来&#xff0c;比如设计模式、数据库…

Golang gin middleware的编写与使用 context.Next函数

中间件 在web应用服务中&#xff0c;完整的一个业务处理在技术上包含客户端操作、服务器端处理、返回处理结果给客户端三个步骤。 在实际的业务开发和处理中&#xff0c;会有更负责的业务和需求场景。一个完整的系统可能要包含鉴权认证、权限管理、安全检查、日志记录等多维度…

【软件分析/静态分析】chapter3 课程03/04 数据流分析的应用(Data Flow Analysis)

&#x1f517; 课程链接&#xff1a;李樾老师和谭天老师的&#xff1a;南京大学《软件分析》课程03&#xff08;Data Flow Analysis I&#xff09;_哔哩哔哩_bilibili 南京大学《软件分析》课程04&#xff08;Data Flow Analysis II&#xff09;_哔哩哔哩_bilibili 这篇文章总结…

识别一切模型RAM(Recognize Anything Model)及其前身 Tag2Text 论文解读

img 总览 大家好&#xff0c;我是卷了又没卷&#xff0c;薛定谔的卷的AI算法工程师「陈城南」~ 担任某大厂的算法工程师&#xff0c;带来最新的前沿AI知识和工具&#xff0c;欢迎大家交流~ 继MetaAI 的 SAM后&#xff0c;OPPO 研究院发布识别一切模型&#xff08;Recognize Any…