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小時內與您取得聯系。