作者归档:分享园地

使用Net将HTML简历导出为PDF格式

现在有许多将HTML导出PDF的第三方包,这里介绍使用的是Select.HtmlToPdf.NetCore

使用Select.HtmlToPdf.NetCore

  1. 整体思路是将cshtml内容读出来,然后再转为Pdf文档
  2. 读取cshtml内容有两种方法,第一种使用第三方包 RazorEngine.NetCore,第二种使用官方方法进行读取。(注意两种方法的cshtml内容略有不同)

效果图展示

在线演示地址

我把所有的源代码都上传到了我的个人Github,有需要的请自取:https://github.com/WeiMing0803/ExportPdf

首先使用ChatGPT生成个人简历信息

代码部分

HomeController.cs :

public async Task<IActionResult> ToPdf()
{
    PdfDocument pdfDocument = new PdfDocument();
    HtmlToPdf converter = new HtmlToPdf();//实例化一个html到pdf转换器对象
    converter.Options.PdfPageOrientation = PdfPageOrientation.Portrait;//设置页面方向
    converter.Options.PdfPageSize = PdfPageSize.A4;//设置页面大小
    converter.Options.MarginTop = 10;//设置页边距
    converter.Options.MarginBottom = 10;
    converter.Options.MarginLeft = 10;
    converter.Options.MarginRight = 10;

    PdfReportModel model = new PdfReportModel { Name = "彭于晏", Email = "pengyuyan@outlook.com" };
    //string htmlResult = readByEngineRazor(model);//第一种方法,使用RazorEngine.NetCore读取Cshtml文件
    string htmlResult = await readCshtml(model);//第二种方法

    if (!string.IsNullOrEmpty(htmlResult))
    {
        pdfDocument = converter.ConvertHtmlString(htmlResult);
    }

    string savePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $@"ExportPDF\{DateTime.Now.ToString("yyyyMMdd")}");
    Directory.CreateDirectory(savePath);
    string filename = Path.Combine(savePath, $"{DateTime.Now.ToString("yyyyMMddHHmmssffff")}.pdf");
    pdfDocument.Save(filename);

    byte[] bytes = System.IO.File.ReadAllBytes(filename);
    return File(bytes, "application/pdf", Path.GetFileName(filename));
}

 private string readByEngineRazor(PdfReportModel model)
{
    string template = System.IO.File.ReadAllText("Views/Report/PdfReport.cshtml");
    string htmlResult = Engine.Razor.RunCompile(template, "PdfReport", typeof(PdfReportModel), model);
    return htmlResult;
}

private async Task<string> readCshtml(PdfReportModel model)
{
    string htmlResult = await _viewRenderService.RenderToStringAsync("Report/PdfReport", model);
    return htmlResult;
}

TemplateGadgetProvider.cs :

public class TemplateGadgetProvider
{
    public static TemplateGadgetProvider _instance;
    public static TemplateGadgetProvider Instance
    {
        get
        {
            if (_instance == null)
                _instance = new TemplateGadgetProvider();
            return _instance;
        }
    }

    public string Load(string virtualPath)
    {
        return File.ReadAllText(virtualPath);
    }
}

pdfReport.css :
Css样式文件:点击查看详细内容

html {
    font-family: 'Open Sans', sans-serif;
    background: whitesmoke;
}

a {
    text-decoration: none;
    color: black;
}

hr {
    background: grey;
}

#container {
    position: relative;
    display: flex;
}

#profile {
    flex: 15%;
    display: block;
    position: relative;
    margin: 5% 2% 0 10%;
    width: 100%;
    height: 100%;
}

#info-cards {
    flex: 55%;
    display: block;
    margin-top: 5%;
    margin-right: 10%;
    width: 100%;
    height: 100%;
}

#image {
    position: relative;
    overflow: hidden;
}

#image,
#profile-photo {
    position: relative;
    width: 80px;
    height: 80px;
    border-radius: 10px;
}

    #image > a {
        position: absolute;
        top: 0;
        left: 0;
        background: rgba(0, 0, 0, 0.5) !important;
        height: 100%;
        width: 100%;
        display: none;
    }

        #image > a > i {
            -webkit-text-stroke: 1px #ffffffdd;
            padding: 40%;
        }

    #image:hover a {
        display: block;
    }

#name {
    font-size: 23px !important;
    line-height: 20px !important;
}

#about,
.card > ul > li {
    padding: 0 0 0 15px;
    position: relative;
    display: inline-block;
    width: 100%;
}

#about {
    font-size: 20px !important;
    padding: 0 !important;
}

    #name,
    #about > p {
        font-weight: bolder;
        font-family: 'Open Sans', sans-serif;
    }

#email {
    font-size: 15px !important;
    font-weight: bold !important;
    font-family: 'Cutive Mono', monospace;
}

#college,
#email,
#year-graduation,
#education,
#more-about,
#telephone,
#fax {
    color: #555;
    font-size: 13.5px;
}

strong,
span {
    color: black;
    font-size: 16px;
}

#social-links,
#about {
    display: inline-block;
}

#social-links {
    margin-bottom: 12px;
}

    #social-links a {
        margin: 0 10px;
    }

#edit-intro {
    display: block;
    color: #097bbf;
    font-family: 'Nunito', sans-serif;
}

.fab {
    font-size: 1.1em;
}

.fab,
.fas {
    color: whitesmoke;
}

#about > a {
    top: 4px;
    right: 8px;
}

.edit {
    top: 19px;
    right: 10px;
}

#about > a,
.edit {
    position: absolute;
    font-size: 15px !important;
}

.stroke-transparent {
    -webkit-text-stroke: 1px #000;
    -webkit-text-fill-color: transparent;
}

.blue {
    color: #097bbf !important;
    font-size: 13px;
}

.stroke-transparent-blue {
    -webkit-text-stroke: 1px #097bbf;
    -webkit-text-fill-color: transparent;
}

.card {
    box-shadow: 0 3px 10px 0 rgba(0, 0, 0, .1);
    overflow-x: hidden;
    margin-bottom: 30px;
    padding: 15px 30px 30px 30px;
    background-color: #fff;
}

    .card > p {
        color: #0e141e;
        font-weight: bolder;
        font-size: 18px;
        line-height: 2;
    }

        .card > p > i {
            font-size: 18px;
        }

    .card > a {
        font-weight: 400;
        font-size: 15px;
        margin: 0;
        margin-left: 25px;
        padding: 0;
        border: 0;
        height: auto;
        background: transparent;
        color: #097bbf;
        outline: none;
        cursor: pointer;
    }

    .card > ul {
        list-style-type: none;
    }

.tags {
    font-size: 17px;
    font-weight: bolder;
}

    .tags ~ a {
        display: none !important;
    }

    .tags span {
        font-size: 14px;
        font-weight: normal;
        color: #0e141e;
    }

        .tags span span {
            color: #738f93;
        }

@media screen and (max-width:1090px) {
    #profile {
        margin-left: 5%;
    }
}

@media screen and (max-width:850px) {
    #container {
        display: block;
    }

    #profile {
        width: 90%;
    }

    .card {
        margin: 0 5%;
        margin-bottom: 30px;
    }
}

PdfReport.cshtml :

使用RazorEngine.NetCore需要修改下面两处地方

  1. 删除 @model PdfReportModel
  2. @Html.Raw(@style) 修改为 @@Raw(@style)

视图文件:点击查看详细内容

@using exportPdf.common
@model PdfReportModel   

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    @{
        string style = TemplateGadgetProvider.Instance.Load(@"wwwroot\css\pdfReport.css");
    }
    <style>@Html.Raw(@style)</style>
</head>

<body>
    <div id="inner-nav"></div>
    <div id="container">
        <div id="profile">
            <div id="image">
                <img id="profile-photo" src="https://img2023.cnblogs.com/blog/233608/202303/233608-20230308165653594-2049775608.jpg" alt="Profile-Image">
                <a href="#"><i class="fas fa-pen stroke-transparent"></i></a>
            </div>
            <p id="name">@Model.Name<br><span id="email">@Model.Email</span></p>
            <p id="designation">前端开发工程师<br><span id="college">天将降大任于斯人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为也,所以动心忍性,增益其所不能。——《孟子》 </span></p>
            <div id="social-links"><a href="#"><i class="fab fa-facebook-f stroke-transparent"></i></a><a><i
                        class="fab fa-twitter stroke-transparent"></i></a><a><i
                        class="fab fa-linkedin-in stroke-transparent"></i></a><a><i
                        class="fab fa-github stroke-transparent"></i></a></div>
            <a id="edit-intro" href="#"><i class="fas fa-pen-alt blue"></i>&nbsp;&nbsp;</a>
            <hr width="100%">
            <div id="about">
                <p style="display:inline;">个人详情</p>
                <a href="#"><i class="fas fa-pen stroke-transparent-blue"></i></a>
            </div>
            <p id="year-graduation">预计毕业年份<br><strong>2023年6月</strong></p>
            <p id="education">学历<br><strong>湖南大学 本科</strong></p>
            <p id="more-about">专业<br><strong> 计算机科学与技术专业</strong></p>
            <p id="telephone">电话<br><strong>0532-2271351</strong></p>
            <p id="fax">传真<br><strong>+91-532-25453441</strong></p>
        </div>
        <div id="info-cards">
            <div class="card">
                <p><i class="fas fa-briefcase stroke-transparent"></i>&nbsp;&nbsp;&nbsp;专业技能</p>
                <ul>
                    <li>
                        <p class="tags">1. 熟练掌握HTML、CSS、JavaScript等前端基础技术</p>
                    </li>
                    <li>
                        <p class="tags">2. 熟悉jQuery、Bootstrap等常用前端框架和库</p>
                    </li>
                    <li>
                        <p class="tags">3. 了解Node.js、Express等后端开发技术</p>
                    </li>
                    <li>
                        <p class="tags">4. 掌握Git、Webpack等常用开发工具</p>
                    </li>
                    <li>
                        <p class="tags">5. 具备良好的编码风格和文档习惯</p>
                    </li>
                </ul>
            </div>
            <div class="card">
                <p><i class="fas fa-briefcase stroke-transparent"></i>&nbsp;&nbsp;&nbsp;工作检验</p>
                <ul>
                    <li>
                        <p class="tags">1. 依帆网站首页制作(个人项目)<br>
    - 使用HTML、CSS、JavaScript实现了一个响应式的网站首页<br>
    - 使用Bootstrap进行布局和样式美化,使用jQuery实现轮播图和导航栏效果<br>
    - 使用Webpack进行打包和优化,使用Git进行版本控制和部署</p>
                    </li>
                    <li>
                        <p class="tags">2. 艺风网站后台管理系统(实习项目)<br>
    - 参与了一个基于Node.js和Express的后台管理系统的开发<br>
    - 负责前端页面的编写,使用EJS模板引擎渲染数据<br>
    - 使用Ajax和Fetch进行数据交互,使用Element UI组件库提升用户体验<br>
    - 遵循MVC架构,使用Mongoose操作MongoDB数据库</p>
                    </li>
                </ul>
            </div>
            <div class="card">
                <p><i class="fas fa-graduation-cap stroke-transparent"></i>&nbsp;&nbsp;&nbsp;自我评价</p>
                <ul>
                    <li>
                        <p class="tags">具备较强的学习能力和逻辑思维能力,喜欢接触新技术和新知识</p>
                    </li>
                    <li>
                        <p class="tags">具备良好的沟通能力和团队协作能力,能够积极配合团队完成任务</p>
                    </li>
                    <li>
                        <p class="tags">具备一定的创新能力和解决问题能力,能够针对不同需求提出合理方案</p>
                    </li>
                </ul>
                <a href="#">+ Add new</a>
            </div>
        </div>
    </div>
</body>
</html>

ViewRenderService :

public class ViewRenderService
{
    private readonly IRazorViewEngine _razorViewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;

    public ViewRenderService(IRazorViewEngine razorViewEngine,
        ITempDataProvider tempDataProvider,
        IServiceProvider serviceProvider)
    {
        _razorViewEngine = razorViewEngine;
        _tempDataProvider = tempDataProvider;
        _serviceProvider = serviceProvider;
    }

    public async Task<string> RenderToStringAsync(string viewName, object model)
    {
        var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
        var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

        using (var sw = new StringWriter())
        {
            var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);

            if (viewResult.View == null)
            {
                throw new ArgumentNullException($"{viewName} does not match any available view");
            }

            var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
            {
                Model = model
            };

            var viewContext = new ViewContext(
                actionContext,
                viewResult.View,
                viewDictionary,
                new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                sw,
                new HtmlHelperOptions()
            );

            await viewResult.View.RenderAsync(viewContext);
            return sw.ToString();
        }
    }
}

Program.cs :

builder.Services.AddTransient<ViewRenderService>();

以上就是使用Select.HtmlToPdf.NetCore将HTML导出为PDF的全部内容!

作者:百宝门-后端组-明维

我们为什么要阅读webpack源码


相信很多人都有这个疑问,为什么要阅读源码,仅仅只是一个打包工具,会用不就行了,一些配置项在官网,或者谷歌查一查不就好了吗,诚然在大部分的时候是这样的,但这样在深入时也会遇到以下几种问题。

  1. webpack 配置繁琐,具有 100 多个内置插件,200 多个钩子函数,在保持灵活配置的同时,也把问题抛给了开发者。如不同的配置项会不会对同一个功能产生影响,引用 Plugin 的先后顺序会不会影响打包结果?这些问题,不看源码是无法真正清晰的。
  2. plugin 也就是插件,是 webpack 的支柱功能。开发者可以自己使用钩子函数写出插件,来丰富 webpack 的生态,也可以在自己或公司的项目中引用自己开发的插件,来去解决实际的工程问题,不去探究源码,无法理解 webpack 插件的运行,也无法写出高质量的插件。
  3. 从前端整体来看,现代前端的生态与打包工具高度相关,webpack 作为其中的佼佼者,了解源码,也就是在了解前端的生态圈。

Tapable浅析

首先我们要先明白什么是 Tapable,这个小型库是 webpack 的一个核心工具。在 webpack 的编译过程中,本质上通过 Tapable 实现了在编译过程中的一种发布订阅者模式的插件机制。它提供了一系列事件的发布订阅 API ,通过 Tapable 可以注册事件,从而在不同时机去触发注册的事件进行执行。

下面将会有一个模拟 webpack 注册插件的例子来尝试帮助理解。

compiler.js

const { SyncHook, AsyncParallelHook }  = require('tapable');

class Compiler {
  constructor(options) {
    this.hooks = {
      testSyncHook: new SyncHook(['name', 'age']),
      testAsyncHook: new AsyncParallelHook(['name', 'age'])
    }

    let plugins = options.plugins;

    plugins.forEach(plugin => {
      plugin.apply(this);
    });
  }

  run() {
    this.testSyncHook('ggg', 25);
    this.testAsyncHook('hhh', 24);
  }

  testSyncHook(name, age) {
    this.hooks.testSyncHook.call(name, age);
  }

  testAsyncHook(name, age) {
    this.hooks.testAsyncHook.callAsync(name, age);
  }
}

module.exports = Compiler;

index.js

const Compiler = require('./complier');
const MockWebpackPlugin = require('./mock-webpack-plugin');

const complier = new Compiler({
  plugins: [
    new MockWebpackPlugin(),
  ]
});

complier.run();

mock-webpack-plugin.js

class MockWebpackPlugin {

  apply(compiler) {

    compiler.hooks.testSyncHook.tap('MockWebpackPlugin', (name, age) => {
      console.log('同步事件', name, age);
    })

    compiler.hooks.testAsyncHook.tapAsync('MockWebpackPlugin', (name, age) => {
      setTimeout(() => {
        console.log('异步事件', name, age)
      }, 3000)
    })
  }
}

module.exports = MockWebpackPlugin;

我相信有些小伙伴看到上述代码,就已经明白了大概的逻辑,我们只需要抓住发布订阅这两个词,在代码中呈现的就是 tap 和 call,如果是异步钩子,使用 tapAsync, tapPromise 注册(发布),就要用 callAsync, promise(注意这里的 promise 是 Tapable 钩子实例方法,不要跟 Promise API 搞混) 触发(订阅)。

发布

    compiler.hooks.testSyncHook.tap('MockWebpackPlugin', (name, age) => {
      console.log('同步事件', name, age);
    })

    compiler.hooks.testAsyncHook.tapAsync('MockWebpackPlugin', (name, age) => {
      setTimeout(() => {
        console.log('异步事件', name, age)
      }, 3000)
    })

这里可以看到使用 tab 和 tabAsync 进行注册,在什么时机注册的呢,在 Compiler 类的初始化时期,也就是在通过 new 命令生成对象实例的时候,下面的代码已经在 constructor 中被调用并执行了,当然这个时候并没有像函数一样被调用,打印出来姓名和年龄,这时我们只需要先知道,它们已经被注册了。

### 订阅 ###

  run() {
    this.testSyncHook('ggg', 25);
    this.testAsyncHook('hhh', 24);
  }

  testSyncHook(name, age) {
    this.hooks.testSyncHook.call(name, age);
  }

  testAsyncHook(name, age) {
    this.hooks.testAsyncHook.callAsync(name, age);
  }

通过 compiler.run() 命令将会执行下面两个函数,使用 call 和 callAsync 订阅。这个时候就会执行 console.log 来打印姓名和年龄了,所以说此时我们就能明白 webpack 中 compiler 和 compilation 中的钩子函数是以触发的时期进行区分,归根结底,是注册的钩子在 webpack 不同的编译时期被触发。

注意事项

这里要注意在初始化 Tapable Hook 的同时,要加上参数,传入参数的数量需要与实例化时传递给钩子类构造函数的数组长度保持一致。

    this.hooks = {
      testSyncHook: new SyncHook(['name', 'age']),
      testAsyncHook: new AsyncParallelHook(['name', 'age'])
    }

这里并非要严格的传入 [‘name’, ‘age’],你也可以取其它的名字,如 [‘fff’, ‘ggg],但是为了语义化,还是要进行规范,如下方代码,截取自源码中的 lib/Compiler.js 片段,它们在初始化中也是严格按照了这个规范。

    /** @type {AsyncSeriesHook<[Compiler]>} */
    beforeRun: new AsyncSeriesHook(["compiler"]),
    /** @type {AsyncSeriesHook<[Compiler]>} */
    run: new AsyncSeriesHook(["compiler"]),
    /** @type {AsyncSeriesHook<[Compilation]>} */
    emit: new AsyncSeriesHook(["compilation"]),

更具体的可以查看这篇文章 走进 Tapable – 掘金 (juejin.cn)

如何调试

想调试 webpack 源码,一般有两种方式,一种是 clone 调试,一种是 npm 包调试,笔者这里选择通过 clone 调试,运行 webpack 也有两种方式,一是通过 webpack-cli 输入命令启动,另外一种如下,引入 webapck,使用 webpack.run() 启动。

准备工作

首先可以用 https 从 github 上克隆 webpack 源码。

    git clone https://github.com/webpack/webpack
    npm install

之后可以在根目录创建一个名为 source 的文件夹,source 文件夹目录如下

-- webpack
    -- source 
        -- src 
             -- foo.js
             -- main.js
        -- index.html
        -- index.js
        -- webpack.config.js 

index.js

const webpack = require('../lib/index.js');
const config = require('./webpack.config.js');

const complier = webpack(config);
complier.run((err, stats) => {
  if (err) {
    console.error(err);
  } else {
    console.log(stats);
  }
})

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: './src/main.js',
  output: {
      path: path.join(__dirname, './dist'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
        exclude: /node_modules/,
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Test Webpack',
      template: './index.html',
      filename: 'template.html'
    })
  ]
}

引用 html-webpack-plugin 和 babel-loader 主要是想更清晰看到在构建过程中 webpack 会如何处理引入的 plugin 和 loader。

main.js

import foo from './foo.js';
import { isEmpty } from 'lodash';

foo();

const obj = {};
console.log(isEmpty(obj));
console.log('main.js');

foo.js

export default function foo() {
  console.log('foo');
}

文件创建好了,这里使用 Vscode 进行调试, 打开 JavaScript 调试终端。

image.png

源码阅读

按照下面命令,启动 webpack

    cd source
    node index.js

这里为了更加清晰, 可以打上一个断点。如在 lib/webpack.js 中,将断点打在 158 行,查看是如何生成的 compiler 实例。

image.png

这里需要点击单步调试,这样才能进入 create 函数中,一步步调试可以看到,首先会对传入的 options 进行校验, 如果不符合规范,将会抛出错误,由于这里的 options 是一个对象,将会进入到 createCompiler 函数内。

image.png

在这个函数内将会创造 Compiler 实例,以及注册引入的插件和内置插件。

image.png

笔者将会一步步的讲解这个函数都做了什么事,如

applyWebpackOptionsBaseDefaults:给没设置的基本配置加上默认值。

new Compiler:生成 compiler 实例,初始化一些钩子和参数。

NodeEnvironmentPlugin:主要是对文件模块进行了封装和优化,感兴趣的读者可以打断点,详细去查看。

接下来要做的事情就是注册钩子,如上文中引入了 html-webpack-plugin, 这里将会调用 HtmlWebpackplugin 实例的 apply 函数,这样就能明白为什么以 class 类的方式,写插件,为什么里面一定要加上 apply。紧接着创建完 compiler 实例后,正如官网上描述的,关于 compiler.hooks.environment 的订阅时期,在编译器准备环境时调用,时机就在配置文件中初始化插件之后。我们就能知其然,也能知所以然了。

image.png

再往下,

new WebpackOptionsApply().process(options, compiler):注册了内部插件,如 DllPlugin, HotModuleReplacementPlugin 等。

小技巧分享

这里简单分享了笔者看源码的步骤,然后还有两个技巧分享。

一是由于 webpack 运用了大量回调函数,一步步打断点是很难看的清楚的,可直接在 Vscode 中全局搜索 compiler.hooks.xxx 和 compilation.hooks.xxx, 去看 tap 中回调函数的执行。

二是可在 Vscode 调试中的 watch 模块,添加上 compiler 和 compilation,这样也是更方便观察回调函数的执行。如

image.png

总结

webpack 中的细节很是繁多,里面有大量的异常处理,在看的时候要有重点的看,有选择的看,如果你要看 make 阶段所做的事情, 可以重点去看如何生成模块,模块分为几种,如何递归处理依赖,如何使用 loader 解析文件等。笔者认为看源码还有一个好处,那就是让你对这些知名开源库没有畏惧心理,它们也是用 js 一行行写的,里面会有一些代码片段,可能写的也没有那么优美,我们在阅读代码的同时,说不定也能成为代码贡献者,能够在简历上留下浓墨重彩的一笔。

作者:百宝门-前端组-闫磊刚

C#神器”BlockingCollection”类实现C#神仙操作

前言

如果你想玩转C# 里面多线程,工厂模式,生产者/消费者,队列等高级操作,就可以和我一起探索这个强大的线程安全提供阻塞和限制功能的C#神器类

BlockingCollection简单介绍

微软介绍地址:https://learn.microsoft.com/zh-cn/dotnet/standard/collections/thread-safe/blockingcollection-overview
BlockingCollection 是一个线程安全集合类,可提供以下功能:

  1. 实现制造者-使用者模式
  2. 通过多线程并发添加和获取项
  3. 可选最大容量
  4. 集合为空或已满时通过插入和移除操作进行阻塞
  5. 插入和移除“尝试”操作不发生阻塞,或在指定时间段内发生阻塞
  6. 封装实现 IProducerConsumerCollection 的任何集合类型
  7. 使用取消标记执行取消操作
  8. 支持使用 foreach(在 Visual Basic 中,使用 For Each)的两种枚举:1. 只读枚举,2. 在枚举项时将项移除的枚举

起手式

BlockingCollectionblockingCollection = new(1);

  • new 操作符里面的数字是实现了可选最大容量,超出就线程阻塞了,程序一直卡在哪里

先来个开胃菜 => 三句代码实现线程阻塞

BlockingCollection<int> blockingCollection = new(1);
blockingCollection.Add(1);
blockingCollection.Add(2);

说明:因为限制队列只能插入一条,第一条没有消费掉,所以一直卡在插入第二条程序不会往下继续运行实现了集合为空或已满时通过插入和移除操作进行阻塞

正式开始前先分享一些多线程的知识点

Task类简单介绍

Task 表面上是Thread但却是对ThreadPool的封装,控制和扩展性很强,对线程的延续,阻塞,取消,超时,比传统的Thread和ThreadPool强

Queue类简单介绍

队列(Queue)代表了一个先进先出的对象集合。当您需要对各项进行先进先出的访问时,则使用队列。当您在列表中添加一项,称为入队,当您从列表中移除一项时,称为出队

接下来进入实际使用场景

场景一: 生产者=> 消费者

建议代码还是要动手实现一下,不然体会不到一边生产数据,同时还能取数据的神仙操作

int count = 0 ;
BlockingCollection<string> blockingCollection = new(1);
//生产者
Task.Factory.StartNew(() =>
{
 while (true)
 {
    blockingCollection.Add("String: " + count);
    count++;
    if (count > 10)
    {
     blockingCollection.CompleteAdding();
    }
 }
});

//消费者
Task.Factory.StartNew(() =>
{
 foreach (var element in blockingCollection.GetConsumingEnumerable())
 {
  Thread.Sleep(1000);
  ("Work: " + element).Dump();//Dump 为工具Linq的功能
 }
});

上面的代码中这个方法GetConsumingEnumerable很重要,它可以在BlockingCollection集合有数据的时候取数据,没有的话停止取,可以达到监测的效果

这个案例实现了如下功能:

  1. 多线程并发添加和获取项
  2. 生产者和消费者模式
  3. 使用取消标记执行取消操作(让生产者知道我们已经不需要你工作了)

生产者/消费者输出结果

Work: String: 0
Work: String: 1
Work: String: 2
Work: String: 3
Work: String: 4
Work: String: 5
Work: String: 6
Work: String: 7
Work: String: 8
Work: String: 9
Work: String: 10

场景二: 实现队列FIFO(先进先出),LIFO(先进后出)

 //先进先出(FIFO)
 BlockingCollection<int> bc = new(new ConcurrentQueue<int>());
 bc.Add(1);
 bc.Add(2);
 bc.CompleteAdding();

 //先进后出(LIFO)
 BlockingCollection<int> bc2 = new(new ConcurrentStack<int>());
 bc2.Add(1);
 bc2.Add(2);
 bc2.CompleteAdding();

 bc.Take().Dump("bc1:");
 bc2.Take().Dump("bc2:");

队列输出结果

bc :1 
bc2: 2

这个简单的案例是想介绍一下其实:BlockingCollection也可以实现队列的功能

以上就是本期的全部内容啦谢谢大家看到这里

作者 => 百宝门瞿佑明

多人协同开发,git workflow 提高团队协作能力

—-by:刘坚 百宝门-前端组

👧🏻集美们,还在为工作多年而只会使用简单的git指令被质疑是假简历而烦恼吗😑. Don’t worry!

|| 今天给带来大家一个git攻略指南😘.成为大手子,🥰参与开源大项目指日可待👩🏻‍💻👩🏻‍💻👩🏻‍💻. 😭.

Git

这是git的官方文档直通车🚃:

GitHub.com Help Documentation

容我随手截图:

可能对于英语成绩不好的小姐妹来说,文档是相当的不友好.

头晕了,幻痛了(读书的回忆全都回来了).

全是英语,宝宝难受,人生艰难😑😮‍💨.

但,这些都是小问题!

我们只要轻轻的滑动一下鼠标,关注这块就可以了🙈:

左边就是日常的git指令设置SSH之类的老生常谈的操作.

然后看右边的About pull request这就是需要介绍的.

你只需要了解四条最常规的git指令就ok😋.

// 添加文件到暂存区
git add .

// 设置提交的说明
git commit -m "feat: xxx"

// 拉去远程仓库代码
git pull origin <branch name>

// 推送远程仓库代码
git push origin <branch name>

git workflow

一种基于gitflow的工作方式,这种工作方式主要用于: 管理新功能😎,发布新特性📢,以及维护👨🏻‍🔧👩🏻‍🔧等.

大部分人都使用github都只是单纯的用它来储存代码🤪,但是,github上⭐上万的repo.参与代码的贡献者们有上千上万🤨他们该如何管理各种代码冲突🤪和版本发布🤪呢?

这便是gitflow: 根据不同的工作特性来创建不同的分支进行定义:

  1. master 主分支
  2. feature 新分支
  3. hotfix 热补丁分支
  4. fix 修复bug分支
  5. 等..

那么,所有基于master衍生出来的分支该如何管理呢❓❓❓

PR便是管理这些gitflow的屠龙宝刀❗❗❗

Pull Request

官网介绍:

简而言之🚴🏻‍♂️: 当你在自己的feature(fix.hotfix)分支或者派生仓库上完成功能开发,并入主分支(稳定版本)或者主仓库的一个可视化请求.

现在,找一个项目开启一次PR完成流程🧐.

找项目

👨🏻随随便便在网上找一个大型开源项目🧑🏻‍🔧然后准备动手⚠️,准备动手⚠️,准备动手⚠️

百度的深度学习的repo. 现在对他发一个最简单的PR.

贴上地址:

https://github.com/PaddlePaddle/Paddle.

修改错别字🙈

fork repo

出现如下界面:

然后,随便找个错别字更正🙉:

push到自己fork的仓库,然后创建PR,如下图所示:

创建成功:

在创建成功之后,大型项目仓库都有分支保护代码审查(CodeReview) .

等待审查通过,成功你就是 大型开源项目的代码贡献者了🌈.

用AngleSharp & LINQPad抓取分析博客园排行榜

AngleSharp简单介绍

  1. AngleSharp 是一个 .NET 库
  2. 使您能够解析基于尖括号的超文本,如HTML、SVG、MathML、XML
  3. AngleSharp的一个重要方面是CSS也可以解析
  4. 同时还是开源,免费的

Github: https://github.com/AngleSharp/AngleSharp
使用文档: https://anglesharp.github.io/

开发工具的推荐LINQPad

介绍:一个小巧,打开秒速,随时能写C#,不至于灵感快速流失的小工具
下载地址:https://www.linqpad.net/
有免费版,基本功能已经够用。我们公司买了它的Premium版。

AngleSharp 代码实操

实操前一些分享一些C#的知识点

如何快速发送网络请求获取到数据呢?

可以用如下:
1、HttpWebRequest
2、WebClient
3、HttpClient
4、RestSharp
5、Flurl

本期重点用HttpClient来实现

起手式

引用NuGet包: Install-Package AngleSharp

使用场景案例

  1. 获取博客园排行榜的Html并且解析
IConfiguration config = Configuration.Default.WithDefaultLoader();
string address = "https://www.cnblogs.com/aggsite/SideRight";
IBrowsingContext context = BrowsingContext.New(config);
IDocument document = await context.OpenAsync(address);
IHtmlCollection<IElement> side_right = document.QuerySelectorAll("div");

side_right.Select(m => new { 
        title = m.QuerySelector(".card-title a")?.TextContent,
        url = m.QuerySelectorAll("ul li").Select(x => x.TextContent)
       })
       .Where(x => x.title != null)
       .Dump();

通过上面代码快速就能分析且快速抓取博客园的排行榜,简单,快速,高效 代码少,有没有觉得Linq语法糖配合请求一些框架的强大呢,朋友们

效果图

既然都能抓取数据了,接下来就是爬虫最重要的分析啦

2.分析博客园每天什么时候发博客看的人数最多,点赞的人数最多,星期几发文章多,哪个大佬发文章多

获取数据的方法

通过HttpClient加上Linq加上AngleSharp实现请求获取Hmtl => 保存Json => 分析Json 生成有价值的图表

public void GetData()
{
 var http = new HttpClient();
 var parser = new HtmlParser();

 File.WriteAllText(@"C:\Users\QYM\Desktop\OfficFile\BlogData.json", JsonConvert.SerializeObject(Enumerable.Range(1, 200)
     .AsParallel()
     .AsOrdered()
     .SelectMany(page =>
     {
      var content = new StringContent(JsonConvert.SerializeObject(new
      {
       CategoryId = "808",
       CategoryType = "SiteHome",
       ItemListActionName = "AggSitePostList",
       PageIndex = $"{page}",
       ParentCategoryId = "0",
       TotalPostCount = "4000"
      }), Encoding.UTF8, "application/json");

      var resp = http.PostAsync("https://www.cnblogs.com/AggSite/AggSitePostList", content).Result;

      var document = parser.ParseDocument(resp.Content.ReadAsStringAsync().GetAwaiter().GetResult());


      return document?.QuerySelectorAll("article").Select(pageContext =>
      {
       return new
       {
        Url = pageContext.QuerySelector(".post-item-text a").GetAttribute("href").Trim(),
        Title = pageContext.QuerySelector(".post-item-text a").TextContent.Trim(),
        Context = pageContext.QuerySelector(".post-item-text p").TextContent.Trim(),
        Name = pageContext.QuerySelector("footer a").TextContent.Trim(),
        DateTime = DateTime.Parse(pageContext.QuerySelector("footer .post-meta-item").TextContent),
        LookOK = pageContext.QuerySelector("footer .post-meta-item+a span").TextContent.Trim(),
        LookPerson = pageContext.QuerySelector("footer .post-meta-item+a+a+a span").TextContent.Trim()
       };
      });
     }), Newtonsoft.Json.Formatting.Indented));
}

效果图

获取博客园200页数据

读取数据并且调用LinqPad 自带的Chart图表方法进行分析

public void ReadData()
{
 var data = JsonConvert.DeserializeObject<List<BlogJsonData>>(File.ReadAllText(@"C:\Users\QYM\Desktop\OfficFile\BlogData.json"));

 Util.Chart(data
 .GroupBy(x => x.DateTime.Hour)
 .Select(x => new { Hour = x.Key, ViewCount = 1.0 * x.Sum(v => v.LookPerson) })
 .OrderByDescending(x => x.Hour),
 x => x.Hour,
 y => y.ViewCount, Util.SeriesType.Bar).Dump("时间段观看人数最多");

 Util.Chart(data
 .GroupBy(x => x.DateTime.Hour)
 .Select(x => new { Hour = x.Key, ViewCount = 1.0 * x.Sum(v => v.LookOk) })
 .OrderByDescending(x => x.Hour),
 x => x.Hour,
 y => y.ViewCount, Util.SeriesType.Bar).Dump("时间段点赞人数最多");

 Util.Chart(data
  .GroupBy(x => x.DateTime.DayOfWeek)
  .Select(x => new { WeekDay = x.Key, ArticleCount = x.Count() })
  .OrderBy(x => x.WeekDay),
 x => x.WeekDay.ToString(),
 y => y.ArticleCount, Util.SeriesType.Bar).Dump("星期几发文章最多");

 Util.Chart(data
  .GroupBy(x => x.Name)
  .Select(x => new { UserName = x.Key, ArticleCount = x.Count() })
  .OrderByDescending(x => x.ArticleCount)
  .Take(9),
 x => x.UserName,
 y => y.ArticleCount,  Util.SeriesType.Bar).Dump("哪个大佬发文章比较多");

}

效果图

源文件Json

分析数据的实体

public class BlogJsonData
{
 public string Url { get; set; }
 public string Title { get; set; }
 public string Context { get; set; }
 public string Name { get; set; }
 public DateTime DateTime { get; set; }
 public int LookOk { get; set; }
 public int LookPerson {get;set;}
}

接下来就是见证奇迹的时候,通过分析抓取到html,保存成Json分析出一些意想不到的图表

效果图

  1. 时间段观看人数最多?

看来博客园一般查看人数最多的是9点->10点,说明哈哈哈,果然大家早上都是喜欢关注编程的大事呀

  1. 时间段点赞人数最多?

果然早起的鸟儿有虫吃,如果想要博客点赞高,那就必须早上九点 -> 10点 抓住阅读高峰期,菜鸟收获高赞,得到很多人的认可

  1. 星期几发文章最多?

看来星期一到星期五中发博客最多的星期一和星期二,最不想上班的两天最适合用来灵感创作写文字

  1. 哪个大佬发文章比较多?

目测近期京东云开发者对博客园贡献很大,看了一下,质量都是很高的文章,极力推荐