HTMLAgilityPack 是一个开源的.NET库,旨在帮助开发人员处理和操作HTML文档。它提供了解析HTML文档、查询DOM元素以及修改HTML内容的功能。HTMLAgilityPack 基于XPath和LINQ查询,使得开发者能够以类似于操作XML文档的方式来操作HTML文档。这使得从复杂的HTML结构中提取所需数据变得轻而易举。
以下是一些常用的HTMLAgilityPack方法和属性,以及它们的用途:
我们使用 HttpClient 发送一个 GET 请求到指定的 HTTPS URL,并且读取返回的响应内容。
如果出现 HTTP 状态码 403 (Forbidden) 错误表示您的请求被服务器拒绝,通常是因为服务器认为您没有权限访问该资源。
private async void btnGetTitle_Click(object sender, EventArgs e)
{
HtmlAgilityPack.HtmlDocument doc=new HtmlAgilityPack.HtmlDocument();
string htmlContent="";
using (HttpClient httpClient=new HttpClient())
{
try
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3");
HttpResponseMessage response=await httpClient.GetAsync("https://www.baidu.com");
//检查 HTTP 响应的状态码是否表示成功
response.EnsureSuccessStatusCode();
//读取内容
byte[] bytes=await response.Content.ReadAsByteArrayAsync();
htmlContent=Encoding.UTF8.GetString(bytes);
}
catch (HttpRequestException ex)
{
}
}
doc.LoadHtml(htmlContent);
HtmlNode titleNode=doc.DocumentNode.SelectSingleNode("//title");
if (titleNode !=null)
{
string title=titleNode.InnerText;
MessageBox.Show($"页面标题:{title}");
}
}
/// <summary>
/// 通过url取得html内容
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private async Task<string> GetHtml(string url)
{
string htmlContent="";
using (HttpClient httpClient=new HttpClient())
{
try
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3");
HttpResponseMessage response=await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
//读取内容
byte[] bytes=await response.Content.ReadAsByteArrayAsync();
htmlContent=Encoding.UTF8.GetString(bytes);
}
catch (HttpRequestException ex)
{
}
}
return htmlContent;
}
private async void btnGetLinks_Click(object sender, EventArgs e)
{
HtmlAgilityPack.HtmlDocument doc=new HtmlAgilityPack.HtmlDocument();
string htmlContent=await GetHtml("https://www.baidu.com");
doc.LoadHtml(htmlContent);
HtmlNodeCollection linkNodes=doc.DocumentNode.SelectNodes("//a[@href]");
if (linkNodes !=null)
{
foreach (HtmlNode linkNode in linkNodes)
{
string link=linkNode.GetAttributeValue("href", "");
lstLink.Items.Add(link);
}
}
}
private async void btnGetSpecialLink_Click(object sender, EventArgs e)
{
HtmlAgilityPack.HtmlDocument doc=new HtmlAgilityPack.HtmlDocument();
string htmlContent=await GetHtml("https://news.baidu.com/");
doc.LoadHtml(htmlContent);
HtmlNodeCollection linkNodes=doc.DocumentNode.SelectNodes("//*[@id=\"pane-news\"]/ul/li[@class=\"bold-item\"]/a");
if (linkNodes !=null)
{
foreach (HtmlNode linkNode in linkNodes)
{
string link=linkNode.GetAttributeValue("href", "");
string title=linkNode.InnerText;
lnkSpecialLink.Items.Add(title + " " + link);
}
}
}
快速找到节点path
SP.NET Core中,严格来讲只有一个Asp.net Core MVC一个框架,Asp.net Core MVC既支持基于视图的MVC开发,也支持Web API和Razor Pages开发。
在VS2022中,一定选择ASP.NET Core Web应用(模型-视图-控制器)项目模板
项目中wwwroot文件夹包含图片、JS、CSS等静态文件,Models、Views、Controllers是与MVC相关的文件夹,appsetting.json是默认配置文件,program.cs为入口代码
MVC模式包括:模型、视图和控制器三部分,视图负责展示内容,也就是html网页;控制器负责处理用户的请求和为视图准备数据;模型负责在视图和控制器之前传递数据。
MVC约定:
MVC中,控制器由Controller类实现,视图一般为*.cshtml文件,模型则是C#中的普通类。
控制器的名字一般以Controller结尾,并放在Controllers文件夹下
视图一般放到Views/控制器名称文件夹下
模型则放在model文件夹下
工作过程
浏览器提交的请求会被封装到模型类的对象中,然后传递给控制器,控制器对其进行处理后将结果放到模型类的对象中再传给视图,视图则解析模型对象渲染成HTML输出给浏览器。
案例:
public record Person(string Name, bool IsVIP, DateTime CreatedTime);
public class TestController : Controller
{ //控制器类中被外界直接访问的方法叫做操作方法
public IActionResult Demo1(int a) //返回类型为IActionResult
{
var model=new Person("Zack", true, new DateTime(1999, 9, 9));
return View(model);//将model对象传递给与操作方法同名的View
}
}
@model MVC项目1.Models.Person
<div>姓名:@Model.Name</div>
<div>@(Model.IsVIP?"VIP":"普通会员")</div>
<div>注册时间:@Model.CreatedTime</div>
@model MVC项目1.Models.Person:视图文件是接收Person类数据的强类型视图
以@开头的代码后面跟C#语句,这种代码叫做服务器端代码
我们在访问.../Test/Demo1地址时,框架会寻找名为TestController类中的Demo1方法来处理用户的请求,运行该操作方法,则会将view渲染成HTML输出给浏览器
MVC开发的系统一般是在浏览器中查看,而目前访问服务器的客户端已不仅仅是浏览器了,很多客户端和服务器之间一般都是使用JSON结构来传递数据,所以需要将数据进行结构化,执行结构化操作的服务叫做Web API。
ASP.NET Core Web API与MVC的项目目录结构相似,但是没有Views和Models文件夹,因为Web API直接返回的是结构化数据,不需要提供展示数据的视图。
样板代码WeatherForecastController,继承自ControllerBase类,MVC中Controller类同样是继承自ControllerBase,Controller类中多了View和MVC中视图相关的代码。
[ApiController] //Web API都要加
[Route("[controller]")] //设定路由,[controller]代表控制器的名字,在这里是WeatherForecast
//表示对/WeatherForecast路径的请求由WeatherForecastController来处理
public class WeatherForecastController : ControllerBase
{
//当对/WeatherForecast路径发送Get请求时由该方法Get处理
[HttpGet(Name="GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{...
}
}
Post Put等方法
先增加一个类SaveNoteRequest,然后在WeatherForecastController中增加一个SaveNote方法,并设定[HttpPost]
public record SaveNoteRequest(string Title, string Content);
[HttpPost]
public string SaveNote(SaveNoteRequest req)
{
string filename=$"{req.Title}.txt";
System.IO.File.WriteAllText(filename, req.Content);
return filename;
}
可以在自带Swagger页面中进行调试查看结构数据,注意只有勾选[启用OpenAPI支持]才会生成一个接口定义浏览界面
话说,人靠衣装,佛靠金装。大街上的小姐姐都喜欢把自己打扮得美美的,让你忍不住多看几眼,这就是装饰的作用。
装饰器是最新的 ECMA 中的一个提案,是一种与类(class)相关的语法,用来注释或修改类和类方法。装饰器在 Python 和 Java 等语言中也被大量使用。装饰器是实现 AOP(面向切面)编程的一种重要方式。
下面是一个使用装饰器的简单例子,这个 @readonly 可以将 count 属性设置为只读。可以看出来,装饰器大大提高了代码的简洁性和可读性。
class Person {
@readonly count=0;
}
复制代码
由于浏览器还未支持装饰器,为了让大家能够正常看到效果,这里我使用 Parcel 进行了一下简单的配置,可以去 clone 这个仓库后再来运行本文涉及到的所有例子,仓库地址:learn es6
本文涉及到 Object.defineProperty、高阶函数等知识,如果之前没有了解过相关概念,建议先了解后再来阅读本文。
在开始讲解装饰器之前,先从经典的装饰器模式说起。装饰器模式是一种结构型设计模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构,是作为对现有类的一个包装。
一般来说,在代码设计中,我们应当遵循「多用组合,少用继承」的原则。通过装饰器模式动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
下班回去和朋友愉快地开黑,当我正在用亚索「面对疾风吧」的时候,突然想到,如果让我设计亚索英雄,我该怎么实现呢?
我灵光一闪,那肯定会先设计一个英雄的类。
class Hero {
attack() {}
}
复制代码
然后,再实现一个 Yasuo 的类来继承这个 Hero 类。
class Yasuo extends Hero {
attack() {
console.log("斩钢闪");
}
}
复制代码
我还在想这个问题的时候,队友已经打了大龙,我的亚索身上就出现了大龙 buff 的印记。我突然想到,那该怎么给英雄增加大龙 buff 呢?那增加个大龙 buff 的属性不行吗?
当然不太行,要知道,英雄联盟里面的大龙 buff 是会增加收益的。
嗯,聪明的我已经想到办法了,再继承一次不就好了吗?
class BaronYasuo extends Yasuo {}
复制代码
厉害了,但是如果亚索身上还有其他 buff 呢?毕竟 LOL 里面是有红 buff、蓝 buff、大龙 buff 等等存在,那岂不是有多少种就要增加多少个类吗?
可以换种思路来思考这个问题,如果把 buff 当做我们身上的衣服。在不同的季节,我们会换上不同的衣服,到了冬天,甚至还会叠加多件衣服。当 buff 消失了,就相当于把这件衣服脱了下来。如下图所示:
衣服对人来说起到装饰的作用,buff 对于亚索来说也只是增强效果。那么,你是不是有思路了呢? 没错,可以创建 Buff 类,传入英雄类后获得一个新的增强后的英雄类。
class RedBuff extends Buff {
constructor(hero) {
this.hero=hero;
}
// 红buff造成额外伤害
extraDamage() {
}
attack() {
return this.hero.attack() + this.extraDamage();
}
}
class BlueBuff extends Buff {
constructor(hero) {
this.hero=hero;
}
// 技能CD(减10%)
CDR() {
return this.hero.CDR() * 0.9;
}
}
class BaronBuff extends Buff {
constructor(hero) {
this.hero=hero;
}
// 回城速度缩短一半
backSpeed() {
return this.hero.backSpeed * 0.5;
}
}
复制代码
定义好所有的 buff 类之后,就可以直接套用到英雄身上,这样看起来是不是清爽了很多呢?这种写法看起来很像函数组合。
const yasuo=new Yasuo();
const redYasuo=new RedBuff(yasuo); // 红buff亚索
const blueYasuo=new BlueBuff(yasuo); // 蓝buff亚索
const redBlueYasuo=new BlueBuff(redYasuo); // 红蓝buff亚索
复制代码
decorator(装饰器)是 ES7 中的一个提案,目前处于 stage-2 阶段,提案地址:JavaScript Decorators。
装饰器与之前讲过的函数组合(compose)以及高阶函数很相似。装饰器使用 @ 作为标识符,被放置在被装饰代码前面。在其他语言中,早就已经有了比较成熟的装饰器方案。
先来看一下 Python 中的一个装饰器的例子:
def auth(func):
def inner(request,*args,**kwargs):
v=request.COOKIES.get('user')
if not v:
return redirect('/login')
return func(request, *args,**kwargs)
return inner
@auth
def index(request):
v=request.COOKIES.get("user")
return render(request,"index.html",{"current_user":v})
复制代码
这个 auth 装饰器是通过检查 cookie 来判断用户是否登录的。auth 函数是一个高阶函数,它接收了一个 func 函数作为参数,返回了一个新的 inner 函数。
在 inner 函数中进行 cookie 的检查,由此来判断是跳回登录页面还是继续执行 func 函数。
在所有需要权限验证的函数上,都可以使用这个 auth 装饰器,很简洁明了且无侵入。
JavaScript 中的装饰器和 Python 的装饰器类似,依赖于 Object.defineProperty,一般是用来装饰类、类属性、类方法。
使用装饰器可以做到不直接修改代码,就实现某些功能,做到真正的面向切面编程。这在一定程度上和 Proxy 很相似,但使用起来比 Proxy 会更加简洁。
注意:装饰器目前还处于 stage-2,意味着语法之后也许会有变动。装饰器用于函数、对象等等已经有一些规划,请看:Future built-in decorators
装饰类的时候,装饰器方法一般会接收一个目标类作为参数。下面是一个给目标类增加静态属性 test 的例子:
const decoratorClass=(targetClass)=> {
targetClass.test='123'
}
@decoratorClass
class Test {}
Test.test; // '123'
复制代码
除了可以修改类本身,还可以通过修改原型,给实例增加新属性。下面是给目标类增加 speak 方法的例子:
const withSpeak=(targetClass)=> {
const prototype=targetClass.prototype;
prototype.speak=function() {
console.log('I can speak ', this.language);
}
}
@withSpeak
class Student {
constructor(language) {
this.language=language;
}
}
const student1=new Student('Chinese');
const student2=new Student('English');
student1.speak(); // I can speak Chinese
student2.speak(); // I can speak Englist
复制代码
利用高阶函数的属性,还可以给装饰器传参,通过参数来判断对类进行什么处理。
const withLanguage=(language)=> (targetClass)=> {
targetClass.prototype.language=language;
}
@withLanguage('Chinese')
class Student {
}
const student=new Student();
student.language; // 'Chinese'
复制代码
如果你经常编写 react-redux 的代码,那么也会遇到需要将 store 中的数据映射到组件中的情况。connect 是一个高阶组件,它接收了两个函数 mapStateToProps 和 mapDispatchToProps 以及一个组件 App,最终返回了一个增强版的组件。
class App extends React.Component {
}
connect(mapStateToProps, mapDispatchToProps)(App)
复制代码
有了装饰器之后,connect 的写法可以变得更加优雅。
@connect(mapStateToProps, mapDispatchToProps)
class App extends React.Component {
}
复制代码
类属性装饰器可以用在类的属性、方法、get/set 函数中,一般会接收三个参数:
使用类属性装饰器可以做到很多有意思的事情,比如最开始举的那个 readonly 的例子:
function readonly(target, name, descriptor) {
descriptor.writable=false;
return descriptor;
}
class Person {
@readonly name='person'
}
const person=new Person();
person.name='tom';
复制代码
还可以用来统计一个函数的执行时间,以便于后期做一些性能优化。
function time(target, name, descriptor) {
const func=descriptor.value;
if (typeof func==='function') {
descriptor.value=function(...args) {
console.time();
const results=func.apply(this, args);
console.timeEnd();
return results;
}
}
}
class Person {
@time
say() {
console.log('hello')
}
}
const person=new Person();
person.say();
复制代码
在 react 知名的状态管理库 mobx 中,也通过装饰器来将类属性置为可观察属性,以此来实现响应式编程。
import {
observable,
action,
autorun
} from 'mobx'
class Store {
@observable count=1;
@action
changeCount(count) {
this.count=count;
}
}
const store=new Store();
autorun(()=> {
console.log('count is ', store.count);
})
store.changeCount(10); // 修改 count 的值,会引起 autorun 中的函数自动执行。
复制代码
如果你想要使用多个装饰器,那么该怎么办呢?装饰器是可以叠加的,根据离被装饰类/属性的距离来依次执行。
class Person {
@time
@log
say() {}
}
复制代码
除此之外,在装饰器的提案中,还出现了一种组合了多种装饰器的装饰器例子。目前还没见到被使用。
通过使用 decorator 来声明一个组合装饰器 xyz,这个装饰器组合了多种装饰器。
decorator @xyz(arg, arg2 {
@foo @bar(arg) @baz(arg2)
}
@xyz(1, 2) class C { }
复制代码
和下面这种写法是一样的。
@foo @bar(1) @baz(2)
class C { }
复制代码
在实现 JavaScript 多重继承的时候,可以使用 mixin 的方式,这里结合装饰器甚至还能更进一步简化 mixin 的使用。
mixin 方法将会接收一个父类列表,用其装饰目标类。我们理想中的用法应该是这样:
@mixin(Parent1, Parent2, Parent3)
class Child {}
复制代码
和之前实现多重继承的时候实现原理一致,只需要拷贝父类的原型属性和实例属性就可以实现了。
这里创建了一个新的 Mixin 类,来将 mixins 和 targetClass 上面的所有属性都拷贝过去。
const mixin=(...mixins)=> (targetClass)=> {
mixins=[targetClass, ...mixins];
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if (key !=='constructor'
&& key !=='prototype'
&& key !=='name'
) {
let desc=Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
class Mixin {
constructor(...args) {
for (let mixin of mixins) {
copyProperties(this, new mixin(...args)); // 拷贝实例属性
}
}
}
for (let mixin of mixins) {
copyProperties(Mixin, mixin); // 拷贝静态属性
copyProperties(Mixin.prototype, mixin.prototype); // 拷贝原型属性
}
return Mixin;
}
export default mixin
复制代码
我们来测试一下这个 mixin 方法是否能够正常工作吧。
class Parent1 {
p1() {
console.log('this is parent1')
}
}
class Parent2 {
p2() {
console.log('this is parent2')
}
}
class Parent3 {
p3() {
console.log('this is parent3')
}
}
@mixin(Parent1, Parent2, Parent3)
class Child {
c1=()=> {
console.log('this is child')
}
}
const child=new Child();
console.log(child);
复制代码
最终在浏览器中打印出来的 child 对象是这样的,证明了这个 mixin 是可以正常工作的。
注意:这里的 Child 类就是前面的 Mixin 类。
也许你会问,为什么还要多创建一个多余的 Mixin 类呢?为什么不能直接修改 targetClass 的 constructor 呢?前面不是讲过 Proxy 可以拦截 constructor 吗?
恭喜你,你已经想到了 Proxy 的一种使用场景。没错,这里用 Proxy 的确会更加优雅。
const mixin=(...mixins)=> (targetClass)=> {
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !=='constructor'
&& key !=='prototype'
&& key !=='name'
) {
let desc=Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
for (let mixin of mixins) {
copyProperties(targetClass, mixin); // 拷贝静态属性
copyProperties(targetClass.prototype, mixin.prototype); // 拷贝原型属性
}
// 拦截 construct 方法,进行实例属性的拷贝
return new Proxy(targetClass, {
construct(target, args) {
const obj=new target(...args);
for (let mixin of mixins) {
copyProperties(obj, new mixin()); // 拷贝实例属性
}
return obj;
}
});
}
复制代码
以往我们在频繁触发的场景下,为了优化性能,经常会使用到节流函数。下面以 React 组件绑定滚动事件为例子:
class App extends React.Component {
componentDidMount() {
this.handleScroll=_.throttle(this.scroll, 500);
window.addEveneListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEveneListener('scroll', this.handleScroll);
}
scroll() {}
}
复制代码
在组件中绑定事件需要注意应当在组件销毁的时候进行解绑。而由于节流函数返回了一个新的匿名函数,所以为了之后能够有效解绑,不得不将这个匿名函数存起来,以便于之后使用。
但是在有了装饰器之后,我们就不必在每个绑定事件的地方都手动设置 throttle 方法,只需要在 scroll 函数添加一个 throttle 的装饰器就行了。
const throttle=(time)=> {
let prev=new Date();
return (target, name, descriptor)=> {
const func=descriptor.value;
if (typeof func==='function') {
descriptor.value=function(...args) {
const now=new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev=new Date();
}
}
}
}
}
复制代码
使用起来比原来要简洁很多。
class App extends React.Component {
componentDidMount() {
window.addEveneListener('scroll', this.scroll);
}
componentWillUnmount() {
window.removeEveneListener('scroll', this.scroll);
}
@throttle(50)
scroll() {}
}
复制代码
而实现防抖(debounce)函数装饰器和节流函数类似,这里也不再多说。
const debounce=(time)=> {
let timer;
return (target, name, descriptor)=> {
const func=descriptor.value;
if (typeof func==='function') {
descriptor.value=function(...args) {
if(timer) clearTimeout(timer)
timer=setTimeout(()=> {
fn.apply(this, args)
}, wait)
}
}
}
}
复制代码
如果对节流和防抖函数比较感兴趣,那么可以去阅读一下这篇文章:函数节流与函数防抖
通过类属性装饰器来对类的属性进行类型的校验。
const validate=(type)=> (target, name)=> {
if (typeof target[name] !==type) {
throw new Error(`attribute ${name} must be ${type} type`)
}
}
class Form {
@validate('string')
static name=111 // Error: attribute name must be ${type} type
}
复制代码
如果你觉得对属性一个个手动去校验太过麻烦,也可以通过编写校验规则,来对整个类进行校验。
const rules={
name: 'string',
password: 'string',
age: 'number'
}
const validator=rules=> targetClass=> {
return new Proxy(targetClass, {
construct(target, args) {
const obj=new target(...args);
for (let [name, type] of Object.entries(rules)) {
if (typeof obj[name] !==type) {
throw new Error(`${name} must be ${type}`)
}
}
return obj;
}
})
}
@validator(rules)
class Person {
name='tom'
password='123'
age='21'
}
const person=new Person();
复制代码
core-decorators 是一个封装了常用装饰器的 JS 库,它归纳了下面这些装饰器(只列举了部分)。
装饰器虽然还属于不稳定的语法,但在很多框架中都已经广泛使用,例如 Angular、Nestjs 等等,和 Java 中的注解用法非常相似。 装饰器在 TypeScript 中结合反射后还有一些更高级的应用,下篇文章会进行深入讲解。
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
*请认真填写需求信息,我们会在24小时内与您取得联系。