2023年10月

引言

近期,三大主流浏览器引擎均发布最新版本,支持W3C的CSS Color 4标准,包含新的取色方法
color()
和相应语法,可展示更多的色域及色彩空间,这意味着web端能展示更丰富更高清的色彩。虽然目前只有最新版本的现代浏览器才支持,我们可以先提前了解一下这项新标准。

本文首先会先简单介绍几个色彩的基础概念,了解为何需要新标准,之后会介绍新标准中的方法和语法使用。

基础概念

色域(color gamut)

指颜色的可选范围。如sRGB色域,目前web广泛应用的色域标准,使用红(red)绿(green)蓝(blue)作为基础色,色值范围0~255,三种基础色互相混合起来可展示255*255*255种颜色,这大致可理解为sRGB的色域。

现代web css使用的sRGB色域仅满足基础性的色彩需求,能展示的色彩范围远小于人类肉眼所能感知的颜色范围,也远低于高清展示的要求。

以下是sRGB与其他几种色域标准的色值范围大小比较:

以下是sRGB与人类肉眼可感知的色域比较:

色彩空间(color space)

色彩空间可以理解为一个基于某一色域标准下构建的空间数学模型,例如一些简单的方块、圆柱的3D模型,可以用来标记出色域中每个颜色的空间位置,各个颜色之间的关系等。

再用sRGB举例,红(red)绿(green)蓝(blue)三种基础色可设置为3个直线坐标轴,每种颜色便可标记为这个立方体中的一个点,在css中便是使用rgb()方法来取色,参数为指定颜色在色彩空间中的坐标(R, G, B)。

再再比如css的另一个取色方法hsl(),使用的是一套完全不同的色彩空间HSL,H色相(hue)是取值范围为0~360的角度,可作为角轴;S饱和度(Saturation)和L亮度(Lightness)作为两个直线轴,可构建为一个圆柱形的空间,css中使用hsl(H, S, L)表示颜色。

一种色域标准可以使用不同的色彩空间来描述,不同的色域标准也可以使用的是同一类的色彩空间表示。例如sRGB可以使用
rgb()

hsl()

hwb()
等方式进行描述,而像Display-P3、Rec2020色域都可以使用(R,G,B)的色彩空间来描述,只是空间的边界范围有所不同。

为什么要支持高清色彩

高清意味着更高范围的色域,让我们先直观感觉一下窄色域与广色域的视觉差距:

在实际的css颜色取值中,我们常用的方法有很多
rgb()

rgba()

hsl()

hwb()
,对应不同的色彩空间,但取的都是同一色域范围内的颜色,即sRGB,大概只能展示人类肉眼可感知的色彩中的30%,仿佛在使用一台90年代的电视机播放4K电影。

虽然目前的网络显示设备很多还是sRGB标准,并不支持显示更广色域标准的色彩,仅部分HDR显示器、或视频录制设备、电影制造中使用了如Display P3这类更广的色域标准。但对于高清的需求只会越来越多,支持更广色域标准注定也是未来web端显示的目标之一。

为应对这一趋势,W3C的CSS Color 4标准定义了新方法
color()
和其他语法能更灵活的指定各种不同色域标准下的颜色,以及更好的色彩渐变展示。最近,主流三大web浏览器也都已支持了W3C的新标准。

CSS Color 4

回顾现有的色彩空间

2000年以来,我们有多种方式指定色值:
hex
色值(
#rgb

#rrggbb
)、
rgb()

rgba()
、或是一些特定颜色的字符(如
white

pink
等);2010年左右开始,浏览器开始支持
hsl()
方法;2017年,
hex
色值扩展了对于透明度的支持,
#rrggbbaa
;之后各种浏览器又陆续增加对
hwb()
方法的支持。

不同的方法对应的是不同的色彩空间,但色域都是同一个,即sRGB。

HEX

使用十六进制的数字来分别表示R、G、B、A的值

.valid-css-hex-colors {
  /* 一般标准 */
  --3-digits: #49b;
  --6-digits: #4499bb;

  /* 带透明度 */
  --4-digits-opaque: #f9bf; /* 不透明 */
  --8-digits-opaque: #ff99bbff; /* 不透明 */
  --4-digits-with-opacity: #49b8; /* 透明度88% */
  --8-digits-with-opacity: #4499bb88; /* 透明度88% */
}



RGB

使用0
255的十进制数字,或是0%
100%的百分比来指明R、G、B,透明度A使用百分比或0~1的数字表示

.valid-css-rgb-colors{
  --classic:rgb(64, 149, 191);
  --modern:rgb(64 149 191);
  --percents:rgb(25% 58% 75%);

  --classic-with-opacity-percent:rgba(64, 149, 191, 50%);
  --classic-with-opacity-decimal:rgba(64, 149, 191, .5);

  --modern-with-opacity-percent:rgb(64 149 191 / 50%);
  --modern-with-opacity-decimal:rgb(64 149 191 / .5);

  --percents-with-opacity-percent:rgb(25% 58% 75% / 50%);
  --percents-with-opacity-decimal:rgb(25% 58% 75% / 50%);

  --empty-channels:rgb(none none none);
}


HSL

这种色彩空间更符合人类自然理解,无需了解红绿蓝基础色是如何混合的。参数分别表示:

  • H:hue,色相,取值0deg~360deg
  • S:Saturation,饱和度,取值0%~100%
  • L:Lightness,亮度,取值0%~100%
.valid-css-hsl-colors{
  --classic:hsl(200deg, 50%, 50%);
  --modern:hsl(200 50% 50%);

  --classic-with-opacity-percent:hsla(200deg, 50%, 50%, 50%);
  --classic-with-opacity-decimal:hsla(200deg, 50%, 50%, .5);

  --modern-with-opacity-percent:hsl(200 50% 50% / 50%);
  --modern-with-opacity-decimal:hsl(200 50% 50% / .5);

  /* 无色相和饱和度,仅用亮度可表示黑白色 */
  --empty-channels-white:hsl(none none 100%);
  --empty-channels-black:hsl(none none 0%);
}



HWB

形式上和HSL类似,但使用的3个维度为:

  • H:Hue,色相,取值0deg~360deg;
  • W:Whiteness,白色的浓度(0~100%);
  • B:Blackness,黑色的浓度(0~100%);
.valid-css-hwb-colors{
  --modern:hwb(200deg 25% 25%);
  --modern2:hwb(200 25% 25%);

  --modern-with-opacity-percent:hwb(200 25% 25% / 50%);
  --modern-with-opacity-decimal:hwb(200 25% 25% / .5);

  /* 无色相和饱和度,仅用亮度可表示黑白色 */
  --empty-channels-white:hwb(none 100% none);
  --empty-channels-black:hwb(none none 100%);
}



新方法color()

新的
color()
方法的参数类似于
rgb()
方法,使用R、G、B三个直线轴上的数值来指明色彩,不同的是
color()
方法的第一个参数可以接收除sRGB以外的其他色域下的色彩空间标识符,且R、G、B的值仅支持0
1或0%
100%。

.valid-css-color-function-colors {
  --srgb: color(srgb 1 1 1);
  --srgb-linear: color(srgb-linear 100% 100% 100% / 50%);
  --display-p3: color(display-p3 1 1 1);
  --rec2020: color(rec2020 0 0 0);
  --a98-rgb: color(a98-rgb 1 1 1 / 25%);
  --prophoto: color(prophoto-rgb 0% 0% 0%);
  --xyz: color(xyz 1 1 1);
}



方法定义:color(colorspace c1 c2 c3[ / A])

  • 参数colorspace:标识符,指明使用哪种色彩空间,可选值包括:
    srgb
    ,
    srgb-linear
    ,
    display-p3
    ,
    a98-rgb
    ,
    prophoto-rgb
    ,
    rec2020
    ,
    xyz
    ,
    xyz-d50
    , and
    xyz-d65
    .
  • 参数c1、c2、c3:可以是number(0~1)、百分比或none,对应指定色彩空间下的各参数值,比如
    srgb
    ,
    srgb-linear
    ,
    display-p3
    对应的是R、G、B的值,具体需要看指定色彩空间描述颜色的维度。
  • 参数A:可选项,可以是number(0~1)、百分比或none,指明颜色的透明度

使用color()描述不同的色彩空间

sRGB

不再支持0
255取值,改为0
1范围,其实和百分比的形式是等价的。如果传了大于1的数值也默认当作1来解析。

.valid-css-srgb-colors{
  --percents:color(srgb 34% 58% 73%);
  --decimals:color(srgb .34 .58 .73);

  --percents-with-opacity:color(srgb 34% 58% 73% / 50%);
  --decimals-with-opacity:color(srgb .34 .58 .73 / .5);

  /* 色值为none或空时,表示黑色 */
  --empty-channels-black:color(srgb none none none);
  --empty-channels-black2:color(srgb);
}



Linear sRGB

Linear sRGB和sRGB是不同的色彩空间,sRGB的取值是通过一个伽马曲线函数做过校正的,并不是线性变化的,更适应人眼的感知特性,即对明暗的感知是非线性的;而Linear sRGB的颜色变化是线性的,以下是明暗从0-1渐变时,两种色彩空间实际的渐变走向。

.valid-css-srgb-linear-colors{
  --percents:color(srgb-linear 34% 58% 73%);
  --decimals:color(srgb-linear .34 .58 .73);

  --percents-with-opacity:color(srgb-linear 34% 58% 73% / 50%);
  --decimals-with-opacity:color(srgb-linear .34 .58 .73 / .5);

  /* 色值为none或空时,表示黑色 */
  --empty-channels-black:color(srgb-linear none none none);
  --empty-channels-black2:color(srgb-linear);
}


Display P3、Rec2020

display P3是最早由苹果公司推行的。如今这一标准已成为HDR显示的基础标准,能显示的颜色比sRGB多50%。而Rec2020标准比display P3的色域更广,可以用来显示4K甚至8K的影像,但目前支持这一标准的终端显示器还很少。两种色域都是使用RGB来描述的。

.valid-css-display-p3-colors{
  --percents:color(display-p3 34% 58% 73%);
  --decimals:color(display-p3 .34 .58 .73);

  --percent-opacity:color(display-p3 34% 58% 73% / 50%);
  --decimal-opacity:color(display-p3 .34 .58 .73 / .5);

  /* 无色度色相,展示为黑色 */
  --empty-channels-black:color(display-p3 none none none);
  --empty-channels-black2:color(display-p3);
}

.valid-css-rec2020-colors {
  --percents: color(rec2020 34% 58% 73%);
  --decimals: color(rec2020 .34 .58 .73);

  --percent-opacity: color(rec2020 34% 58% 73% / 50%);
  --decimal-opacity: color(rec2020 .34 .58 .73 / .5);

  /* 无色度色相,展示为黑色 */
  --empty-channels-black: color(rec2020 none none none);
  --empty-channels-black2: color(rec2020);
}



CIE标准

让我们先回到开头的两张色域图,会发现基于RGB描述的色域基本是一个三角形,因为都是使用3个基础色混合而成,但人眼所能感知的色域是形似马蹄的图形(具体如何绘制出的,感兴趣的可自行搜索了解)。基于RGB标准的色彩空间,都很难完全覆盖人眼能感知的所有颜色。而基于CIE标准(国际照明委员会制定的一种测定颜色的国际标准,它描述了人眼对颜色的感知和色彩的测量方法)的色彩空间,理论上是能够包括人视觉所能感知到的所有颜色。

CSS Color 4新标准也新增了对于CIE标准色域的支持。下面介绍的lab()、lch()、oklab()、oklch()都是基于CIE的取色新方法。

lab()

lab()
方法描述的是基于CIE标准的色彩空间中的颜色,能够覆盖人眼所能看到的全色域。和与基于RGB来描述色彩的维度不同,lab使用的维度分别为:

  • L:lightness,视觉上线性渐变的亮度,取值范围
    0~100

    0%~100%
  • A:代表更贴合人眼视觉特性的两个色轴之其一:红-绿,取值范围均为
    -125~125

    -100%~100%
    。当A为正值,则为更偏红色;为负值时,更偏绿;
  • B:代表更贴合人眼视觉特性的两个色轴之其二:蓝-黄,取值范围均为
    -125~125

    -100%~100%
    。值为正值,更偏黄;为负值,更偏蓝。
.valid-css-lab-colors{
  --percent-and-degrees:lab(58% -16 -30);
  --minimal:lab(58 -16 -30);

  --percent-opacity:lab(58% -16 -30 / 50%);
  --decimal-opacity:lab(58% -16 -30 / .5);

  /* 后两个参数为none是可表示纯灰度 */
  --empty-channels-white:lab(100 none none);
  --empty-channels-black:lab(none none none);
}



lch()

lch使用的维度分别是:

  • L:lightness,视觉上线性渐变的亮度,取值范围0
    100或0%
    100%;
  • C:chroma,颜色的纯度,类似于饱和度,取值范围0~230,但实际上,这个值是没有上限的;
  • H:hue,色相,类似hsl和hwb,是个角轴,取值范围0deg~360deg;
.valid-css-lch-colors{
  --percent-and-degrees:lch(58% 32 241deg);
  --just-the-degrees:lch(58 32 241deg);
  --minimal:lch(58 32 241);

  --percent-opacity:lch(58% 32 241 / 50%);
  --decimal-opacity:lch(58% 32 241 / .5);

  /* 后两个参数为none是可表示纯灰度 */
  --empty-channels-white:lch(100 none none);
  --empty-channels-black:lch(none none none);
}



oklab()

oklab是校正版的lab,优化了图片处理质量,在CSS中意味着渐变优化和颜色处理函数优化,消除了色相偏移(hue shift,即在lab中改变颜色纯度,色相也会变化),使用的维度和lab()是一致的。

.valid-css-oklab-colors{
  --percent-and-degrees:oklab(64% -.1 -.1);
  --minimal:oklab(64 -.1 -.1);

  --percent-opacity:oklab(64% -.1 -.1 / 50%);
  --decimal-opacity:oklab(64% -.1 -.1 / .5);

  /* 后两个参数为none是可表示纯灰度 */
  --empty-channels-white:oklab(100 none none);
  --empty-channels-black:oklab(none none none);
}



oklch()

相应的,oklch是lch的校正版,取色的逻辑和hsl类似,在圆色盘中选择一个角度从而选中一个色相,再通过调节亮度和纯度,也就是hsl中的饱和度,纯度和饱和度基本可认为是等价的,区分仅在于纯度和亮度的调节通常是同步进行的,否则纯度很容易超出目标色域的范围。这里有一个
oklch的拾色器
,可以体验下。

.valid-css-oklch-colors{
  --percent-and-degrees:oklch(64% .1 233deg);
  --just-the-degrees:oklch(64 .1 233deg);
  --minimal:oklch(64 .1 233);

  --percent-opacity:oklch(64% .1 233 / 50%);
  --decimal-opacity:oklch(64% .1 233 / .5);

  /* 后两个参数为none是可表示纯灰度 */
  --empty-channels-white:oklch(100 none none);
  --empty-channels-black:oklch(none none none);
}



color-mix()

除了新增的一些取色方法外,新标准还有一个混色函数,可以将上边提到的各种不同色彩空间的中颜色进行混合计算出新颜色。

color-mix(in lch, plum, pink);
color-mix(in lch, plum 40%, pink);
color-mix(in srgb, #34c9eb 20%, white);
color-mix(in hsl longer hue,hsl(120 100% 50%) 20%, white);



方法定义:color-mix(method, color1[ p1], color2[ p2])

  • 参数method:指定混色的色彩空间,以 in
    的形式, 包含: srgb , srgb-linear , lab , oklab , xyz , xyz-d50 , xyz-d65 , hsl , hwb , lch , or oklch
  • 参数color1、color2:为对应method中指定色彩空间中的任一颜色;
  • 参数p1、p2:为可选参数,取值范围为0%~100%,可以指明混色的比例,如果为空,默认color1和color2各为50%;

项目中如何使用高清色彩

在我们应用一项新语法时,我们通常会有两种策略:优雅降级和渐进增强,具体实施方案:

优雅降级

这种实施起来比较简单,即同时使用新旧取色方法,让浏览器自动判断展示哪种

/* 原代码 */
color: red;
color:color(display-p3 1 0 0);

/* 如果浏览器不支持display-p3,则会只解析第一行 */
color: red;

/* 如果浏览器支持,则会最终使用第二行 */
color:color(display-p3 1 0 0);


渐进增强

使用@supports和@media先判断当前浏览器是否支持新的色域标准,并在条件的情况下提供新的色值。

色域媒体查询

  • dynamic-range
    :取值standard或high,用于判断当前硬件设备是否支持高清、高对比度、高色彩精度,不过这一属性判断的比较笼统,并不能准确判断浏览器是否支持新色域和色彩空间。

@media(dynamic-range: high){
  /* safe to use HD colors */
  color: color(display-p3 34% 58% 73%);
}



  • color-gamut
    :取值 srgb、p3 或 rec2020,对应可判断用户设备是否支持sRGB、Display P3 或 REC2020色域。

@media(color-gamut: srgb){
  /* safe to use srgb colors */
  color: #4499bb;
}

@media(color-gamut: p3){
  /* safe to use p3 colors */
  color: color(display-p3 34% 58% 73%);
}

@media(color-gamut: rec2020){
  /* safe to use rec2020 colors */
  color: color(rec2020 34% 58% 73%);
}



除了可以直接使用css媒体查询,还可用途JavaScript中的
window.matchMedia()
方法来进行媒体查询。

const hasHighDynamicRange = window.matchMedia('(dynamic-range: high)').matches;

console.log(hasHighDynamicRange);// true || false

const hasP3Color = window.matchMedia('(color-gamut: p3)').matches;

console.log(hasP3Color);// true || false


色彩空间查询

  • 使用
    [@supports](https://my.oschina.net/u/688773)
    判断某个css方法或属性是否支持

@supports(background:rgb(0 0 0)){
  /* rgb color space supported */
  background:rgb(0 0 0);
}


@supports(background:color(display-p3 0 0 0)){
  /* display-p3 color space supported */
  background:color(display-p3 0 0 0);
}


@supports(background:oklch(0 0 0)){
  /* oklch color space supported */
  background:oklch(0 0 0);
}



应用实例

在实际应用中,在新旧标准过渡期间,可以综合使用上边的查询方法,下面是一个兼容新旧标准的实例:

:root{
  --neon-red:rgb(100% 0 0);
  --neon-blue:rgb(0 0 100%);
}

/* 设备是否支持展示高清 */
@media(dynamic-range: high){

  /* 浏览器是否能解析display-p3 */
  @supports(color:color(display-p3 0 0 0)){

    /* 安全使用display-p3 */
    --neon-red:color(display-p3 1 0 0);
    --neon-blue:color(display-p3 0 0 1);
  }
}


开发调试

如果更新了最新版本的chrome浏览器的话,就能发现DevTools里的拾色器已经支持了CSS Color 4中的新语法,点击页面元素中的颜色属性,在弹出的拾色器中,中间色值右侧的箭头,之前的版本中,点击箭头是在hex、rgb、hsl和hwb之间切换,但新版本中,点击箭头会出现下拉框,可以看到所有新增的色彩空间和方法,以及当前色值所对应的可替换色值。

同时在选择了不同的色彩空间后,色彩的可调节参数也会相应改变。

当我们选择了一个非sRGB色域的色值后,会发现拾色器的上方区域里会展示一条sRGB的分界线,可以清晰地看出当前选择的颜色所在的色域。这能帮助开发者分辨高清色与非高清色。

而当我们选择一个超出sRGB范围的颜色后,再来点击色值右侧的箭头弹出选项列表时,会发现sRGB色域下的色值后边会带上一个三角叹号。这说明当前色值已超出了sRGB所能描述的范围,只能使用相近的颜色作为替代。

关于chrome DevTools更多关于高清颜色的更新,可参阅
官方文档

总结

sRGB之外的色域和色彩空间目前虽然还刚刚在web端起步,但未来的设计和开发要求可能会慢慢出现,尤其是H5动画、游戏、3D图像等等,对于色彩显示的要求不会永远停留在sRGB阶段,希望本文简陋的介绍能让大家多少开始了解一些关于色彩的东西。如有错误或疏漏,欢迎指正讨论。

参考文章:

1.
https://web.dev/articles/color-spaces-and-functions?hl=en

2.
https://developer.chrome.com/articles/high-definition-css-color-guide/

3.
https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color

作者:京东科技 郑莉

来源:京东云开发者社区 转载请注明来源

一、背景:

微软的.net core开发工具,目前来看,winform界面软件还没有打算要支持linux系统下运行的意思,要想让c#桌面软件在linux系统上运行,开发起来还比较麻烦。微软只让c#的控制台软件支持在linux运行。

二、解决方案:

我想到的一个方案是自定义封装软件的System.Windows.Forms组件,把支持windows和linux的界面框架GTK封装进System.Windows.Forms中!

三、组件封装

这个System.Windows.Forms是实现C#界面的关键组件,Form界面的所有控件都封装在这个组件里。在.net core环境里,这个组件在框架Microsoft.WindowsDesktop.App.WindownsForms下。当开发工程的输出模式是“windows应用程序”时,就会自动引用Microsoft.WindowsDesktop.App.WindownsForms,如果开发工程的输出模式是“控制台应用程序”时,工程不会引用Microsoft.WindowsDesktop.App.WindownsForms,也无法开桌面软件的界面。

为了兼容VS原生界面表单开发,我开发了这个组件GTKSystem.Windows.Forms,这个组件的控件类库命名空间和类名称沿用了原生System.Windows.Forms的类库名称,可以在原生开发的C#软件工程里,直接引用GTKSystem.Windows.Forms就能兼容运行。

四、技术开发

目前实现的控件:
Form、Button、CheckBox、CheckedListBox、ComboBox、DataGridView、DateTimePicker、GroupBox、Label、LinkLabel、MaskedTextBox、MenuStrip、MonthCalendar、NumericUpDown、Panel、PictureBox、RadioButton、RichTextBox、SplitContainer、SplitterPanel、TabControl、TextBox、TreeView

以上控件都只实现了常用功能的属性和方法,事件主要实现了鼠标事件、验证事件、加载事件,还有很多平常不用的属性事件已经实现了接口,但是没有实现执行功能,主要是因为程序量太多,没有去做。对于有能力的开发人员,组件也是可以拿到相关的属性事件(如WidgetEvent)去实现需要的功能。

重写的类也很多,重点说一下这几个类:
Bitmap、Image、System.ComponentModel.ComponentResourceManager、System.Resources.ResourceManager
。这几个类是Form界面引用图像资源必需的。在控制台程序架构里,是没有Bitmap、Image类库的,而且ComponentResourceManager和ResourceManager都不能读取资源图像数据。我在GTKSystem.Windows.Forms里封装实现了Bitmap和Image类,实现了ComponentResourceManager和ResourceManager读取资源图像数据。

Bitmap和Image类除了常用属性外,新增属性Image.PixbufData存放图像数据,用于GTKSystem的使用。

ComponentResourceManager和ResourceManager主要是实现了GetObject方法,读取资源数据。

五、使用方法

修改.net core的windows应用程序工程属性,把输出类型改为“控制台应用程序”,或者把windows窗体配置勾选去掉,配置变为<UseWindowsForms>false</UseWindowsForms>。

<PropertyGroup>
<OutputType>WinExe</OutputType>
<UseWindowsForms>false</UseWindowsForms>
</PropertyGroup>

1、新建System.Resources.ResourceManager类
在项目下新建System.Resources.ResourceManager类,继承GTKSystem.Resources.ResourceManager,用于覆盖原生System.Resources.ResourceManager类。
GTKSystem.Resources.ResourceManager实现了项目资源文件和图像文件读取。
如果项目里没有使用资源图像文件,可以不用新建此文件。

2、新建System.ComponentModel.ComponentResourceManager类
在项目下新建System.ComponentModel.ComponentResourceManager类,继承GTKSystem.ComponentModel.ComponentResourceManager,用于覆盖原生System.ComponentModel.ComponentResourceManager类。
GTKSystem.ComponentModel.ComponentResourceManager实现了项目资源文件和图像文件读取(调用GTKSystem.Resources.ResourceManager)。
如果项目里没有使用资源图像文件,可以不用新建此文件。

3、GTKWinFormsApp.csproj
配置UseWindowsForms为false,或者使用控制台应用程序
<UseWindowsForms>false</UseWindowsForms>

4、引用GTKSystem.Windows.Forms、System.Resources.Extensions
System.Resources.Extensions是空程序dll,VS加载Form界面时验证需要此dll.

5、GTKWinFormsApp\obj\Debug\netcoreapp3.1\GTKWinFormsApp.designer.runtimeconfig.json
GTKWinFormsApp\obj\Release\netcoreapp3.1\GTKWinFormsApp.designer.runtimeconfig.json
将name设置为Microsoft.WindowsDesktop.App,用于VS支持可视化Form表单,重新加载工程或重启VS
"runtimeOptions": {
"framework": {
"name": "Microsoft.WindowsDesktop.App"
},

六、使用效果:

VS开发界面:

运行效果:

统信系统上运行效果:

最后:

此程序在统信系统(linux)上测试完美运行,实现一次编译,跨平台运行,显示界面样式与windows上运行的显示效果基本一样。

目前这个组件没有完全完成,但是主要功能和技术难点都已经解决,现公布出来给有需要的开发人员参考。

项目下载:https://files.cnblogs.com/files/easywebfactory/WinFormsAppDemo.zip?t=1698380585&download=true

这是做什么用的

框架用途

在采集大量新闻网站时,不可避免的遇到动态加载的网站,这给配模版的人增加了很大难度。本来配静态网站只需要两个技能点:xpath和正则,如果是动态网站的还得抓包,遇到加密的还得js逆向。

所以就需要用浏览器渲染这些动态网站,来减少了配模板的工作难度和技能要求。动态加载的网站在新闻网站里占比很低,需要的硬件资源相对于一个人工来说更便宜。

实现方式

采集框架使用浏览器渲染有两种方式,一种是直接集成到框架,类似
GerapyPyppeteer
,这个项目你看下源代码就会发现写的很粗糙,它把浏览器放在
_process_request
方法里启动,然后采集完一个链接再关闭浏览器,大部分时间都浪费在浏览器的启动和关闭上,而且采集多个链接会打开多个浏览器抢占资源。

另一种则是将浏览器渲染独立成一个服务,类似
scrapy-splash
,这种方式比直接集成要好,本来就是两个不同的功能,实际就应该解耦成两个单独的模块。不过听前辈说这东西不太好用,会有内存泄漏的情况,我就没测试它。

自己实现

原理:在自动化浏览器中嵌入http服务实现http控制浏览器。这里我选择
aiohttp+pyppeteer
。之前看到有大佬使用go的
rod
来做,奈何自己不会go语言,还是用Python比较顺手。

后面会考虑用playwright重写一遍,pyppeteer的github说此仓库不常维护了,建议使用playwright。

开始写代码

web服务

from aiohttp import web

app = web.Application()
app.router.add_view('/render.html', RenderHtmlView)
app.router.add_view('/render.png', RenderPngView)
app.router.add_view('/render.jpeg', RenderJpegView)
app.router.add_view('/render.json', RenderJsonView)

然后在RenderHtmlView类中写
/render.html
请求的逻辑。
/render.json
是用于获取网页的某个ajax接口响应内容。有些情况网页可能不方便解析,想拿到接口的json响应数据。

初始化浏览器

浏览器只需要初始化一次,所以启动放到on_startup,关闭放到on_cleanup

c = LaunchChrome()
app.on_startup.append(c.on_startup_tasks)
app.on_cleanup.append(c.on_cleanup_tasks)

其中on_startup_tasks和on_cleanup_tasks方法如下:

async def on_startup_tasks(self, app: web.Application) -> None:
		page_count = 4
		await asyncio.create_task(self._launch())
		app["browser"] = self.browser
		tasks = [asyncio.create_task(self.launch_tab()) for _ in range(page_count-1)]
		await asyncio.gather(*tasks)
		queue = asyncio.Queue(maxsize=page_count+1)
		for i in await self.browser.pages():
				await queue.put(i)
		app["pages_queue"] = queue
		app["screenshot_lock"] = asyncio.Lock()

async def on_cleanup_tasks(self, app: web.Application) -> None:
		await self.browser.close()

page_count为初始化的标签页数,这种常量一般定义到配置文件里,这里我图方便就不写配置文件了。

首先初始化所有的标签页放到队列里,然后存放在app这个对象里,这个对象可以在RenderHtmlView类里通过self.request.app访问到, 到时候就能控制使用哪个标签页来访问链接

我还初始化了一个协程锁,后面在RenderPngView类里截图的时候会用到,因为多标签不能同时截图,需要加锁。

超时停止页面继续加载

async def _goto(self, page: Optional[Page], options: AjaxPostData) -> Dict:
		try:
				await page.goto(options.url, 
						waitUntil=options.wait_util, timeout=options.timeout*1000)
		except PPTimeoutError:
				#await page.evaluate('() => window.stop()')
				await page._client.send("Page.stopLoading")
		finally:
				page.remove_all_listeners("request")

有时间页面明明加载出来了,但还在转圈,因为某个图片或css等资源访问不到,强制停止加载也不会影响到网页的内容。

Page.stopLoading和window.stop()都可以停止页面继续加载,忘了之前为什么选择前者了

定义请求参数

class HtmlPostData(BaseModel):
    url: str
    timeout: float = 30
    wait_util: str = "domcontentloaded"
    wait: float = 0   
    js_name: str = "" 
    filters: List[str] = [] 
    images: bool = 0  
    forbidden_content_types: List[str] = ["image", "media"]
    cache: bool = 1 
    cookie: bool = 0 
    text: bool = 1 
		headers: bool = 1
  • url
    : 访问的链接
  • timeout
    : 超时时间
  • wait_util
    : 页面加载完成的标识,一般都是
    domcontentloaded
    ,只有截图的时候会选择
    networkidle2
    ,让网页加载全一点。更多的选项的选项请看:
    Puppeteer waitUntil Options
  • wait
    : 页面加载完成后等待的时间,有时候还得等页面的某个元素加载完成
  • js_name
    : 预留的参数,用于在页面访问前加载js,目前就只有一个js(
    stealth.min.js
    )用于去浏览器特征
  • filters
    : 过滤的请求列表, 支持正则。比如有些css请求你不想让他加载
  • images
    : 是否加载图片
  • forbidden_content_types
    : 禁止加载的资源类型,默认是图片和视频。所有的类型见:
    resourcetype
  • cache
    : 是否启用缓存
  • cookie
    : 是否在返回结果里包含cookie
  • text
    : 是否在返回结果里包含html
  • headers
    : 是否在返回结果里包含headers

图片的参数

class PngPostData(HtmlPostData):
    render_all: int = 0
    text: bool = 0
    images: bool = 1
    forbidden_content_types: List[str] = []
    wait_util: str = "networkidle2"

参数和html的基本一样,增加了一个render_all用于是否截取整个页面。截图的时候一般是需要加载图片的,所以就启用了图片加载

怎么使用

多个标签同时采集

默认是启动了四个标签页,这四个标签页可以同时访问不同链接。如果标签页过多可能会影响性能,不过开了二三十个应该没什么问题

请求例子如下:

import sys
import asyncio
import aiohttp

if sys.platform == 'win32':
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

async def get_sign(session, delay):
    url = f"http://www.httpbin.org/delay/{delay}"
    api = f'http://127.0.0.1:8080/render.html?url={url}'
    async with session.get(api) as resp:
        data = await resp.json()
        print(url, data.get("status"))
        return data

async def main():
    headers = {
        "Content-Type": "application/json",
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
    }
    loop = asyncio.get_event_loop()
    t = loop.time()
    async with aiohttp.ClientSession(headers=headers) as session:
        tasks = [asyncio.create_task(get_sign(session, i)) for i in range(1, 5)]
        await asyncio.gather(*tasks)
    print("耗时: ", loop.time()-t)

        
if __name__ == "__main__":
    asyncio.run(main())

http://www.httpbin.org/delay
后面跟的数字是多少,网站就会多少秒后返回。所以如果同步运行的话至少需要1+2+3+4秒,而多标签页异步运行的话至少需要4秒

结果如图,四个链接只用了4秒多点:

file

拦截指定ajax请求的响应

import json
import sys
import asyncio
import aiohttp

if sys.platform == 'win32':
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

async def get_sign(session, url):
    api = f'http://127.0.0.1:8080/render.json'
    data = {
        "url": url,
        "xhr": "/api/", # 拦截接口包含/api/的响应并返回
        "cache": 0,
        "filters": [".png", ".jpg"]
    }
    async with session.post(api, data=json.dumps(data)) as resp:
        data = await resp.json()
        print(url, data)
        return data

async def main():
    urls = ["https://spa1.scrape.center/"]
    headers = {
        "Content-Type": "application/json",
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
    }
    loop = asyncio.get_event_loop()
    t = loop.time()
    async with aiohttp.ClientSession(headers=headers) as session:
        tasks = [asyncio.create_task(get_sign(session, url)) for url in urls]
        await asyncio.gather(*tasks)
    print(loop.time()-t)

        
if __name__ == "__main__":
    asyncio.run(main())

请求
https://spa1.scrape.center/
这个网站并获取ajax链接中包含
/api/
的接口响应数据,结果如图:

file

请求一个网站用时21秒,这是因为网站一直在转圈,其实要的数据已经加载完成了,可能是一些图标或者css还在请求。

超时强制返回

加上timeout参数后,即使页面未加载完成也会强制停止并返回数据。如果这个时候已经拦截到了ajax请求会返回ajax响应内容,不然就是返回空

不过好像因为有缓存,现在时间不到1秒就返回了

file

截图

import json
import sys
import asyncio
import base64
import aiohttp

if sys.platform == 'win32':
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

async def get_sign(session, url, name):
    api = f'http://127.0.0.1:8080/render.png'
    data = {
        "url": url,
        #"render_all": 1,
        "images": 1,
        "cache": 1,
        "wait": 1 
    }
    async with session.post(api, data=json.dumps(data)) as resp:
        data = await resp.json()
        if data.get('image'):
            image_bytes = base64.b64decode(data["image"])
            with open(name, 'wb') as f:
                f.write(image_bytes)
            print(url, name, len(image_bytes))
        return data

async def main():
    urls = [
        "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=44004473_102_oem_dg&wd=%E5%9B%BE%E7%89%87&rn=50",
        "https://www.toutiao.com/article/7145668657396564518/",
        "https://new.qq.com/rain/a/NEW2022092100053400",
        "https://new.qq.com/rain/a/DSG2022092100053300"
    ]
    headers = {
        "Content-Type": "application/json",
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
    }
    loop = asyncio.get_event_loop()
    t = loop.time()
    async with aiohttp.ClientSession(headers=headers) as session:
        tasks = [asyncio.create_task(get_sign(session, url, f"{n}.png")) for n,url in enumerate(urls)]
        await asyncio.gather(*tasks)
    print(loop.time()-t)


if __name__ == "__main__":
    asyncio.run(main())

集成到scrapy

import json
import logging
from scrapy.exceptions import NotConfigured

logger = logging.getLogger(__name__)

class BrowserMiddleware(object):
    def __init__(self, browser_base_url: str):
        self.browser_base_url = browser_base_url
        self.logger = logger
        
    @classmethod
    def from_crawler(cls, crawler):
        s = crawler.settings
        browser_base_url = s.get('PYPPETEER_CLUSTER_URL')
        if not browser_base_url:
            raise NotConfigured
        o = cls(browser_base_url)
        return o
    
    def process_request(self, request, spider):
        if "browser_options" not in request.meta or request.method != "GET":
            return
        browser_options = request.meta["browser_options"]
        url = request.url
        browser_options["url"] = url
        uri = browser_options.get('browser_uri', "/render.html")
        browser_url = self.browser_base_url.rstrip('/') + '/' + uri.lstrip('/')
        new_request = request.replace(
            url=browser_url,
            method='POST',
            body=json.dumps(browser_options)
        )
        new_request.meta["ori_url"] = url
        return new_request

    def process_response(self, request, response, spider):
        if "browser_options" not in request.meta or "ori_url" not in request.meta:
            return response
        try:
            datas = json.loads(response.text)
        except json.decoder.JSONDecodeError:
            return response.replace(url=url, status=500)
        datas = self.deal_datas(datas)
        url = request.meta["ori_url"]
        new_response = response.replace(url=url, **datas)
        return new_response
    
    def deal_datas(self, datas: dict) -> dict:
        status = datas["status"]
        text: str = datas.get('text') or datas.get('content')
        headers = datas.get('headers')
        response = {
            "status": status,
            "headers": headers,
            "body": text.encode()
        }
        return response            

开始想用aiohttp来请求,后面想了下,其实都要替换请求和响应,为什么不直接用scrapy的下载器

完整源代码

现在还只是个半成品玩具,还没有用于实际生产中,集群打包也没做。有兴趣的话可以自己完善一下

如果感兴趣的人比较多,后面也会系统的完善一下,打包成docker和发布第三方库到pypi

github:
https://github.com/kanadeblisst00/browser_cluster

事务的并发操作可能出现的问题

中文 英文 描述
脏读 Dirty Reads 事务2读到了事务1未提交的事务,事务1随后回滚,但事务2读到了事务1的“中间数据”。 在Read Uncommitted隔离级别下会发生,其它级别不会。
(update&read)
image
丢失更新 Lost Updates 两个事务对同一个行分别进行更新,其中一个更新覆盖了另一个,导致丢失了一个更新。 在Read Committed的隔离级别下仍能发生,Repeatable Read能够避免它发生。
为什么官方文档里没有提到Lost Updates这个现象?
官方文档:Transaction Isolation Levels
(read&update)
image
不可重复读 Non-repeatable Reads 事务2在事务1的两次读取之间更新了数据导致事务1两次读到不一样的数据。 在Repeatable Read隔离级别下解决,和Lost Update一样,本质都是因为在此隔离级别下S锁持有到事务结束使其它事务无法在本事务执行过程中更新数据。
(read&update)
image
幻读 Phantom Reads 当一个事务执行一个query两次, 但得到不同行数的结果集。 幻读和不可重复读不一样地方在于,解决不可重复读问题时针对的是已有数据,因此可以持有它们的S锁,使其它事务请求X锁时等待;而幻读是新插入的数据。
(insert\delete)
image

事务

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。

  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。

我一直纳闷这个“一致”是什么意思,然后在#引用.1.这本书里看到了解释:数据库在某一时刻的状态反应的是真实世界在这个时刻的数据,真实世界被抽象成数据,然后真实世界不停运行,数据库中的数据就像真实世界的一个切面,只不过真实世界是连续的,而数据库的数据是真实世界某一个时刻的状态,是离散的。随着真实世界的运行,当数据库中的数据从一个时刻到下一个时刻且数据都是“正确的”的时候,就称数据库从一个一致性状态到了另一个一致性状态,保证这种“一致性”的手段就是“数据库约束”,它对数据的正确性进行校验。不过,这种校验是片面的,数据的正确性也取决于业务逻辑(后台代码),但在数据库层面能做的就是“数据库约束”。

  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。

  • 持久性(Durability):即便发生断电等意外情况,已被提交的事务对数据库的修改应该永久保存在数据库中(硬盘上)。这点一般通过日志来保证。

事务的隔离级别

隔离级别影响锁模式的表现。

image

Read Uncommitted

  • 不要求SELECT时请求S锁,因此它不会阻塞X锁或者被X锁阻塞,因此可以发生脏读。

  • The intention of this isolation level is for systems primarily focused on reporting and business intelligence, not online transaction processing.

  • 别用。

Read Committed

  • 要求SELECT时请求S锁,因此它会阻塞X锁或者被X锁阻塞,因此可以避免脏读。

  • 它是SQL Server默认的隔离级别。

  • 由于S锁不会持有到事务结束,而是在SELECT完成后就释放了, 因此可能发生幻读和不可重复读。


    • 幻读:INSERT, DELETE

    • 不可重复读:UPDATE

Repeatable Read

  • 要求SELECT时请求S锁,并持续到事务结束,因此避免了事务过程中其它事务对数据的修改,因此可以避免幻读和不可重复读。

  • 可重复读:在当前的事务中可再次读取(R),读取的结果没有被修改(CUD)。

Serializable

  • 最高级别的隔离级别

  • 相比直接在所需的行上加锁,可串行化隔离级别在所需的行和下一行(索引顺序)上获得一个range lock。因此它可以避免插入新的数据,从而避免幻读现象。

image

  • 上图是SQLite中的页的结构,但DBMS设计思路是类似的。在一个页中,存储着多个Key,这些Key按顺序排列在一个数组中,所谓的“下一个”就是这个索引的下一个。

  • 如果key存在


    • 如果next key存在:
      • 在non-unique index的情况下,在当前请求的key和next key上范围锁。
      • 在unique index的情况下,理论上在key上获取S锁就足够,但SQL Server仍会获取范围锁
    • 如果next key不存在:taken on the ‘infinity’ value.
  • 如果key不存在


    • 如果next key存在:taken on the next key.
    • 如果next key不存在: taken on the ‘infinity’ value.
  • 在无限大上的范围锁会锁定 >= 当前key的范围

  • BETWEEN 在请求的key上和next key上获取范围锁

  • WHERE 在请求的key及前后获取范围锁

create table foo (c1 int)

go

insert into foo values (1)

insert into foo values (2)

insert into foo values (3)

insert into foo values (4)

insert into foo values (5)

create unique clustered index foo_ci on foo(c1)

set tran isolation level serializable

begin tran

select * from foo where c1 between 2 and 4 

SELECT dtl.request_session_id,
 dtl.resource_database_id,
 dtl.resource_associated_entity_id,
 dtl.resource_type,
 dtl.resource_description,
 dtl.request_mode,
 dtl.request_status
FROM sys.dm_tran_locks AS dtl
WHERE dtl.request_session_id = @@SPID;

image

引用

  1. 《SQL Server 2017 Query Performance Tuning 5th Edition》
  2. dotnettutorials: sql-server-concurrent-transactions
  3. stackoverflow: what are range locks
  4. techcommunity.microsoft: SQL Server Range Lock
  5. Microsoft: Transaction Isolation Levels

本文分享自华为云社区《
深入理解Java中的Reader类:一步步剖析
》,作者:bug菌。

前言

在Java开发过程中,我们经常需要读取文件中的数据,而数据的读取需要一个合适的类进行处理。Java的IO包提供了许多类用于数据的读取和写入,其中Reader便是其中之一。本文将对Java中的Reader进行详细介绍,并分析其优缺点及应用场景。

摘要

本文将从以下几个方面对Java中的
Reader
类进行详细介绍:

  1. Reader类的概述
  2. Reader类代码的解析
  3. Reader类的应用场景案例
  4. Reader类的优缺点分析
  5. Reader类的方法介绍及源代码分析
  6. Reader类的测试用例
  7. 全文小结和总结
  8. 附源码
  9. 建议

本文通过对Java中的Reader进行详细讲解,旨在帮助开发者更好地掌握Reader的使用方法。

Reader类

概述

Reader类是Java中用于读取字符流的抽象类。它是所有字符输入流的超类,提供了字符输入流读取时的基本功能。Reader类主要由三个类实现,分别是InputStreamReader、FileReader和CharArrayReader。

源代码解析

Reader
类是一个抽象类,它的源代码定义如下:

public abstract classReader implements Readable, Closeable {
...
}

其中,Reader实现了两个接口:
Readable

Closeable

Readable
接口中只定义了一个方法:

public interfaceReadable {intread(CharBuffer cb) throws IOException;
}


Closeable
接口中也只定义了一个方法:

public interfaceCloseable extends AutoCloseable {voidclose() throws IOException;
}

这两个接口的作用分别是提供读取字符和关闭资源的方法。

应用场景案例

Reader类通常用于读取文本文件中的数据。比如我们经常使用的BufferedReader就是Reader类的一个子类,用于逐行读取文本文件中的数据。除此之外,Reader还可用于读取网络数据、读取控制台输入等场景。

下面是几个 使用Reader 类的应用场景案例,同学们仅供参考:

1. 读取文本文件

使用 FileReader 类来读取文本文件很常见。例如可以使用
FileReader

BufferedReader
组合来读取一个文本文件并逐行输出:

    //1. 读取文本文件
    public static voidtestReadFile(){
FileReader fileReader;
BufferedReader bufferedReader;
try{
fileReader
= new FileReader("./template/fileTest.txt");
bufferedReader
= newBufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.
out.println(line);
}
fileReader.close();
bufferedReader.close();
}
catch(IOException e) {
e.printStackTrace();
}
}

通过上述案例,我们本地演示,结果可见如下:

2. 读取网络资源

可以使用 InputStreamReader 和 URL 类来读取网络资源,例如:

    //2. 读取网络资源
    public static voidtestReadURL() throws IOException {
URL url
= new URL("https://www.baidu.com/");
URLConnection conn
=url.openConnection();
InputStream
is =conn.getInputStream();
InputStreamReader isr
= new InputStreamReader(is);
BufferedReader br
= newBufferedReader(isr);

String line;
while ((line = br.readLine()) != null) {
System.
out.println(line);
}

br.close();
isr.close();
is.close();
}
public static voidmain(String[] args) throws IOException {
testReadURL();
}

通过上述案例,我们本地演示,结果可见如下:

3. 读取字符串

可以使用 StringReader 类来将一个字符串转换为字符流,例如:

    //3. 读取字符串
    public static voidtestReadStr() throws IOException {
String str
= "Hello, World!!!";
StringReader stringReader
= newStringReader(str);intdata;while ((data = stringReader.read()) != -1) {
System.
out.print((char) data);
}
stringReader.close();
}
public static voidmain(String[] args) throws IOException {
testReadStr();
}

通过上述案例,我们本地演示,结果可见如下:

通过介绍及演示以上三个比较常见的 Java 中运用 Reader 类的应用场景案例,通过使用 Reader 类的子类,可以方便地读取各种类型的字符流数据。如果你还有更多贴切生活或工作中的案例,欢迎评论区交流呀,独乐乐不如众乐乐。

利弊分析

优点

  1. Reader
    类支持字符流的读取,可以准确地读取文本文件中的数据。
  2. Reader
    类能够自动处理字符编码问题,在读取文件时能够自动转换编码方式。
  3. 可以通过
    Reader
    类的各个子类实现不同的功能,使用灵活。

缺点

  1. Reader
    类读取数据的速度较慢,不适合读取二进制数据。
  2. Reader
    类不能随机访问文件中的数据,只能逐行读取,读取大文件时效率较低。
  3. Reader
    类的使用较为繁琐,需要通过缓冲区等方式来提高读取速度和效率。

类代码方法介绍

构造方法

protected Reader()

Reader类的默认构造方法。

方法

public int read() throws IOException

用处:读取单个字符,并返回该字符的ASCII码,如果到达流的末尾,返回-1。

public int read(char[] cbuf) throws IOException

用处:读取字符数组,返回读取的字符个数。

public int read(char[] cbuf, int offset, int length) throws IOException

用处:读取指定长度的字符数组,返回读取的字符个数。

public long skip(long n) throws IOException

用处:跳过n个字符(包括空格),返回实际跳过的字符数。

public boolean ready() throws IOException

用处:判断是否可以从流中读取字符,如果可以读取返回true。

public boolean markSupported()

用处:判断此流是否支持mark()操作。如果支持,则返回true,否则返回false。

public void mark(int readAheadLimit) throws IOException

用处:设置mark位置,并将输入流中的指针指向mark位置。如果该流不支持mark()操作,则抛出IOException异常。

public void reset() throws IOException

用处:将输入流中的指针重新指向mark位置。如果该流不支持reset()操作,则抛出IOException异常。

abstract public void close() throws IOException

用处:关闭该流并释放与之关联的所有资源。

测试用例

以下是一个使用Reader类读取文件的测试用例:

测试代码演示

package com.example.javase.io.reader;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
/**
* @author bug菌
* @version 1.0
* @date 2023/10/19 10:34
*/ public classReaderTest {public static voidmain(String[] args) throws IOException {
File file
= new File("./template/fileTest.txt");
Reader reader
= newFileReader(file);char[] buffer = new char[1024];intlen;while ((len = reader.read(buffer)) != -1) {
System.
out.println(new String(buffer, 0, len));
}
reader.close();
}
}

测试结果展示

根据如上测试用例,我们来执行下main主函数进行测试读取文件的字符数据,结果展示如下截图:

通过控制台输出的内容与原文本内容进行对比,可得该测试用例运用Reader类正常读取文件内容,

代码解析

如上测试代码使用了Reader 类从文件中读取字符数据。如下来对该代码进行步骤解析,以帮助同学们加速理解。

首先,我们先创建一个 File 对象,指定要读取的文件路径,然后使用
FileReader
类将该文件读取到内存中,并返回
Reader
对象。然后使用
char[]
数组作为缓冲区,将数据从
Reader
中读取到缓冲区,并使用
String
类将缓冲区数据转化为字符串输出到控制台,直到所有数据都被读取完毕。最后,关闭 Reader 对象释放相关资源。整个读取过程非常简单,你们学会了吗?

全文小结

本文对Java中的
Reader
类进行了详细介绍,包括其简介、源代码解析、应用场景案例、优缺点分析、方法介绍及测试用例。通过本文的学习,我们可以更好地掌握
Reader
的使用方法,并在开发中合理使用
Reader
类。

总结

Reader
类是Java中一个用于读取字符流的抽象类,它具有读取文本数据、自动处理字符编码等优点,并可通过其子类实现不同的功能。但是,Reader类读取数据的速度较慢,不适合读取二进制数据,而且不能随机访问文件中的数据。在使用
Reader
类时,要注意使用缓冲区等方式来提高读取速度和效率。最后,要注意关闭资源,避免资源泄漏问题的发生。

附录源码

如上涉及所有源码均已上传同步在
「Gitee」
,提供给同学们一对一参考学习,辅助你更迅速的掌握。

点击关注,第一时间了解华为云新鲜技术~