.NET 权限工作流框架 TOP 榜 - 小码编匠 - 博客园

mikel阅读(120)

来源: .NET 权限工作流框架 TOP 榜 – 小码编匠 – 博客园

前言

.NET权限管理及快速开发框架、最好用的权限工作流系统。

基于经典领域驱动设计的权限管理及快速开发框架,源于Martin Fowler企业级应用开发思想及最新技术组合(SQLSugar、EF、Quartz、AutoFac、WebAPI、Swagger、Mock、NUnit、Vue2/3、Element-ui/plus、IdentityServer等)。已成功在docker/jenkins中实施。

核心模块包括:组织机构、角色用户、权限授权、表单设计、工作流等。

它的架构精良易于扩展,是中小企业的首选。

版本说明

1、主分支main运行环境默认为.NET SDK 6.0,支持.NET未来版本,需要.NET SDK 4.0/4.5开发环境的同学请查看本项目4.0分支,已停止维护。

2、目前OpenAuth.Net以全部开源的方式向大众开放,对于有经验的开发者,官方文档足以满足日常开发。为了能让项目走的更远,特推出基于vue2 + element-ui /vue3 + element-plus的单页面应用程序,即企业版/高级版OpenAuth.Pro

开源地址:http://demo.openauth.net.cn:1802

3、该版本是一套后端基于OpenAuth.WebAPI接口,前端基于vue-element-admin,采用VUE全家桶(VUE+VUEX+VUE-ROUTER)单页面SPA开发的管理后台。

预览地址:http://demo.openauth.net.cn:1803

另外 企业版包含一套基于有赞Vant+Vue3的移动端界面。

预览地址:http://demo.openauth.net.cn:1804

核心看点

  • 同时支持EntityFramework、SQLSugar两款最流行的ORM框架
  • 符合国情的RBAC权限体系。超强的自定义权限控制功能,可灵活配置用户、角色可访问的数据权限。
  • 完整的字段权限控制,可以控制字段可见及API是否返回字段值
  • 可拖拽的表单设计。详情:可拖拽表单
  • 可视化流程设计
  • 全网最好用的打印解决方案。详情:智能打印
  • 基于Quartz.Net的定时任务控制,可随时启/停,可视化配置Cron表达式功能
  • 基于CodeSmith的代码生成功能,可快速生成带有头/明细结构的页面
  • 支持SQLServer、mysql、Oracle、PostgreSql数据库,理论上支持所有数据库
  • 集成IdentityServer4,实现基于OAuth2的登录体系
  • 建立三方对接规范,已有系统可以无缝对接流程引擎
  • 前端采用 vue + layui + element-ui + ztree + gooflow + leipiformdesign
  • 后端采用 .NET Core +EF core+ autofac + quartz +IdentityServer4 + nunit + swagger
  • 设计工具 PowerDesigner + Enterprise Architect

项目截图

流程中心

表单设计

数据权限

仓储中心

项目经验

教科书级的分层思想,哪怕苛刻的你阅读的是大神级精典大作(如:《企业应用架构模式》《重构与模式》《ASP.NET设计模式》等),你也可以参考本项目。不信?有图为证,Resharper自动生成的项目引用关系,毫无PS痕迹!

官方地址

  • 网站:http://www.openauth.net.cn
  • 文档:http://doc.openauth.net.cn
  • 项目:https://gitee.com/dotnetchina/OpenAuth.Net

如果觉得这篇文章对你有用,欢迎加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行交流心得,共同成长。

.NET 窗口/屏幕截图 - 唐宋元明清2188 - 博客园

mikel阅读(129)

来源: .NET 窗口/屏幕截图 – 唐宋元明清2188 – 博客园

图像采集源除了显示控件(上一篇《.NET 控件转图片》有介绍从界面控件转图片),更多的是窗口以及屏幕。

窗口截图最常用的方法是GDI,直接上Demo吧:

复制代码
 1         private void GdiCaptureButton_OnClick(object sender, RoutedEventArgs e)
 2         {
 3             var bitmap = CaptureScreen();
 4             CaptureImage.Source = ConvertBitmapToBitmapSource(bitmap);
 5         }
 6         /// <summary>
 7         /// 截图屏幕
 8         /// </summary>
 9         /// <returns></returns>
10         public static Bitmap CaptureScreen()
11         {
12             IntPtr desktopWindow = GetDesktopWindow();
13             //获取窗口位置大小
14             GetWindowRect(desktopWindow, out var lpRect);
15             return CaptureByGdi(desktopWindow, 0d, 0d, lpRect.Width, lpRect.Height);
16         }
17         private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap)
18         {
19             using MemoryStream memoryStream = new MemoryStream();
20             // 将 System.Drawing.Bitmap 保存到内存流中
21             bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
22             // 重置内存流的指针到开头
23             memoryStream.Seek(0, SeekOrigin.Begin);
24 
25             // 创建 BitmapImage 对象并从内存流中加载图像
26             BitmapImage bitmapImage = new BitmapImage();
27             bitmapImage.BeginInit();
28             bitmapImage.StreamSource = memoryStream;
29             bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
30             bitmapImage.EndInit();
31             // 确保内存流不会被回收
32             bitmapImage.Freeze();
33             return bitmapImage;
34         }
35         /// <summary>
36         /// 截图窗口/屏幕
37         /// </summary>
38         /// <param name="windowIntPtr">窗口句柄(窗口或者桌面)</param>
39         /// <param name="left">水平坐标</param>
40         /// <param name="top">竖直坐标</param>
41         /// <param name="width">宽度</param>
42         /// <param name="height">高度</param>
43         /// <returns></returns>
44         private static Bitmap CaptureByGdi(IntPtr windowIntPtr, double left, double top, double width, double height)
45         {
46             IntPtr windowDc = GetWindowDC(windowIntPtr);
47             IntPtr compatibleDc = CreateCompatibleDC(windowDc);
48             IntPtr compatibleBitmap = CreateCompatibleBitmap(windowDc, (int)width, (int)height);
49             IntPtr bitmapObj = SelectObject(compatibleDc, compatibleBitmap);
50             BitBlt(compatibleDc, 0, 0, (int)width, (int)height, windowDc, (int)left, (int)top, CopyPixelOperation.SourceCopy);
51             Bitmap bitmap = System.Drawing.Image.FromHbitmap(compatibleBitmap);
52             //释放
53             SelectObject(compatibleDc, bitmapObj);
54             DeleteObject(compatibleBitmap);
55             DeleteDC(compatibleDc);
56             ReleaseDC(windowIntPtr, windowDc);
57             return bitmap;
58         }
复制代码

根据user32.dll下拿到的桌面信息-句柄获取桌面窗口的设备上下文,再以设备上下文分别创建内存设备上下文、设备位图句柄

复制代码
 1 BOOL BitBlt(
 2     HDC   hdcDest,  // 目标设备上下文
 3     int   nXDest,   // 目标起始x坐标
 4     int   nYDest,   // 目标起始y坐标
 5     int   nWidth,   // 宽度(像素)
 6     int   nHeight,  // 高度(像素)
 7     HDC   hdcSrc,   // 源设备上下文
 8     int   nXSrc,    // 源起始x坐标
 9     int   nYSrc,    // 源起始y坐标
10     DWORD dwRop    // 操作码(如CopyPixelOperation.SourceCopy)
11 );
复制代码

图像位块传输BitBlt是最关键的函数,Gdi提供用于在设备上下文之间进行位图块的传输,从原设备上下文复现位图到创建的设备上下文

另外,与Bitblt差不多的还有StretchBlt,StretchBlt也是复制图像,但可以同时对图像进行拉伸或者缩小,需要缩略图可以用这个方法

然后以设备位图句柄输出一个位图System.Drawing.Bitmap,使用到的User32、Gdi32函数:

 View Code

还有一种比较简单的方法Graphics.CopyFromScreen,看看调用DEMO:

复制代码
 1         private void GraphicsCaptureButton_OnClick(object sender, RoutedEventArgs e)
 2         {
 3             var image = CaptureScreen1();
 4             CaptureImage.Source = ConvertBitmapToBitmapSource(image);
 5         }
 6         /// <summary>
 7         /// 截图屏幕
 8         /// </summary>
 9         /// <returns></returns>
10         public static Bitmap CaptureScreen1()
11         {
12             IntPtr desktopWindow = GetDesktopWindow();
13             //获取窗口位置大小
14             GetWindowRect(desktopWindow, out var lpRect);
15             return CaptureScreenByGraphics(0, 0, lpRect.Width, lpRect.Height);
16         }
17         /// <summary>
18         /// 截图屏幕
19         /// </summary>
20         /// <param name="x">x坐标</param>
21         /// <param name="y">y坐标</param>
22         /// <param name="width">截取的宽度</param>
23         /// <param name="height">截取的高度</param>
24         /// <returns></returns>
25         public static Bitmap CaptureScreenByGraphics(int x, int y, int width, int height)
26         {
27             var bitmap = new Bitmap(width, height);
28             using var graphics = Graphics.FromImage(bitmap);
29             graphics.CopyFromScreen(x, y, 0, 0, new System.Drawing.Size(width, height), CopyPixelOperation.SourceCopy);
30             return bitmap;
31         }
复制代码

Graphics.CopyFromScreen调用简单了很多,与GDI有什么区别?

Graphics.CopyFromScreen内部也是通过GDI.BitBlt来完成屏幕捕获的,封装了下提供更高级别、易胜的API。

测试了下,第一种方法Gdi32性能比Graphics.CopyFromScreen性能略微好一点,冷启动时更明显点,试了2次耗时大概少个10多ms。

所以对于一般应用场景,使用 Graphics.CopyFromScreen 就足够了,但如果你需要更高的控制权和性能优化,建议使用 Gdi32.BitBlt

kybs00/CaptureImageDemo (github.com)

C# 网络编程:.NET 开发者的核心技能 - 小码编匠 - 博客园

mikel阅读(127)

来源: C# 网络编程:.NET 开发者的核心技能 – 小码编匠 – 博客园

前言

数字化时代,网络编程已成为软件开发中不可或缺的一环,尤其对于 .NET 开发者而言,掌握 C# 中的网络编程技巧是迈向更高层次的必经之路。无论是构建高性能的 Web 应用,还是实现复杂的分布式系统,网络编程都是支撑这一切的基石。

本篇主要为 .NET 开发者提供一份全面而精炼的 C# 网络编程入门,从基础知识到高级话题,逐一剖析,帮助你建立起扎实的网络编程功底,让你在网络世界的编码之旅中游刃有余。

一、HTTP 请求

HTTP(Hypertext Transfer Protocol)是互联网上应用最为广泛的一种网络协议,主要用于从万维网服务器传输超文本到本地浏览器的传输协议。

在C#中,处理HTTP请求有多种方式,从传统的System.Net命名空间到现代的HttpClient类,每种方法都有其适用场景。

1、使用 HttpClient 发送HTTP请求

HttpClient是C#中推荐用于发送HTTP请求的类,它提供了异步的API,可以更好地处理长时间运行的操作,避免阻塞UI线程。

以下是一个简单的GET请求示例:

复制代码
using System;
using System.Net.Http;
using System.Threading.Tasks;

public class HttpClientExample
{
    public static async Task Main()
    {
        using var client = new HttpClient();
        var response = await client.GetAsync("https://api.example.com/data");
        
        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            Console.WriteLine(content);
        }
        else
        {
            Console.WriteLine($"Failed to retrieve data: {response.StatusCode}");
        }
    }
}
复制代码

 2、使用 WebClient 发送HTTP请求

尽管WebClient类仍然存在于.NET Framework中,但在.NET Core和后续版本中,它已被标记为过时,推荐使用HttpClient

不过,对于简单的同步请求,WebClient仍然可以使用:

复制代码
using System;
using System.IO;
using System.Net;

class WebClientExample
{
    public static void Main()
    {
        using (var client = new WebClient())
        {
            try
            {
                string result = client.DownloadString("https://api.example.com/info");
                Console.WriteLine(result);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
        }
    }
}
复制代码

3、使用 HttpRequestMessage 和 HttpMessageHandler

对于更复杂的HTTP请求,如需要自定义请求头或处理认证,可以使用HttpRequestMessageHttpMessageHandler

这种方式提供了更多的灵活性和控制:

复制代码
using System;
using System.Net.Http;
using System.Threading.Tasks;

class HttpRequestMessageExample
{
    public static async Task Main()
    {
        using var client = new HttpClient();
        var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/info");
        request.Headers.Add("Authorization", "Bearer your-access-token");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            Console.WriteLine(content);
        }
        else
        {
            Console.WriteLine($"Failed to retrieve data: {response.StatusCode}");
        }
    }
}
复制代码

4、注意事项

  • 安全性和性能: 使用HttpClient时,确保在一个应用程序的生命周期内重用同一个实例,而不是每次请求都创建新的实例。
  • 错误处理: 总是对HTTP请求的结果进行检查,处理可能发生的异常和非成功的HTTP状态码。
  • 超时和取消: 使用HttpClient时,可以通过CancellationToken来控制请求的超时和取消。

通过掌握这些知识点,能够在C#中有效地处理各种HTTP请求,从简单的GET请求到复杂的POST请求,包括身份验证和错误处理。

二、WebSocket 通信

WebSocket是一种在单个TCP连接上进行全双工通信的协议,它提供了比传统HTTP请求/响应模型更低的延迟和更高的效率,非常适合实时数据流、聊天应用、在线游戏等场景。在C#中,无论是服务器端还是客户端,都可以使用WebSocket进行通信。

1、客户端使用 WebSocket

在C#中,你可以使用System.Net.WebSockets命名空间下的ClientWebSocket类来创建WebSocket客户端。下面是一个简单的示例,展示了如何连接到WebSocket服务器并发送和接收消息:

复制代码
using System;
using System.IO.Pipelines;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// WebSocket客户端类,用于与WebSocket服务器建立连接和通信。
/// </summary>
public class WebSocketClient
{
    /// <summary>
    /// 客户端WebSocket实例。
    /// </summary>
    private readonly ClientWebSocket _webSocket = new ClientWebSocket();
    
    /// <summary>
    /// 用于取消操作的CancellationTokenSource。
    /// </summary>
    private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

    /// <summary>
    /// 连接到指定的WebSocket服务器。
    /// </summary>
    /// <param name="uri">WebSocket服务器的URI。</param>
    public async Task Connect(string uri)
    {
        // 使用提供的URI连接到WebSocket服务器
        await _webSocket.ConnectAsync(new Uri(uri), _cancellationTokenSource.Token);
    }

    /// <summary>
    /// 向WebSocket服务器发送消息。
    /// </summary>
    /// <param name="message">要发送的消息字符串。</param>
    public async Task SendMessage(string message)
    {
        // 将消息转换为UTF8编码的字节
        byte[] buffer = Encoding.UTF8.GetBytes(message);
        
        // 创建ArraySegment,封装要发送的字节缓冲区
        ArraySegment<byte> segment = new ArraySegment<byte>(buffer);
        
        // 发送消息到WebSocket服务器
        await _webSocket.SendAsync(segment, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
    }

    /// <summary>
    /// 接收WebSocket服务器发送的消息。
    /// </summary>
    /// <param name="onMessageReceived">接收到消息时调用的回调函数。</param>
    public async Task ReceiveMessage(Action<string> onMessageReceived)
    {
        // 当WebSocket连接处于打开状态时,持续接收消息
        while (_webSocket.State == WebSocketState.Open)
        {
            var buffer = new byte[1024];
            
            // 接收来自WebSocket服务器的数据
            var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _cancellationTokenSource.Token);
            
            // 如果接收到的类型为关闭,则关闭连接
            if (result.MessageType == WebSocketMessageType.Close)
            {
                await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
                break;
            }
            
            // 将接收到的字节转换为字符串,并通过回调函数处理
            var receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
            onMessageReceived(receivedMessage);
        }
    }

    /// <summary>
    /// 断开与WebSocket服务器的连接。
    /// </summary>
    public async Task Disconnect()
    {
        // 取消接收和发送操作
        _cancellationTokenSource.Cancel();
        
        // 关闭WebSocket连接
        await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
    }
}
复制代码

2、服务器端使用 WebSocket

在服务器端,可以使用ASP.NET Core中的Microsoft.AspNetCore.WebSockets来支持WebSocket。

下面是一个简单的WebSocket服务端点配置示例:

复制代码
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

public class Startup
{
    /// <summary>
    /// 配置服务容器。
    /// </summary>
    /// <param name="services">服务集合。</param>
    public void ConfigureServices(IServiceCollection services)
    {
        // 添加控制器服务
        services.AddControllers();
    }

    /// <summary>
    /// 配置应用管道。
    /// </summary>
    /// <param name="app">应用构建器。</param>
    /// <param name="env">主机环境。</param>
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // 在开发环境中启用异常页面
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // 启用路由
        app.UseRouting();

        // 启用WebSocket中间件
        app.UseWebSockets();

        // 配置端点处理器
        app.UseEndpoints(endpoints =>
        {
            // 映射默认的GET请求处理器
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello World!");
            });

            // 映射WebSocket请求处理器
            endpoints.Map("/ws", async context =>
            {
                // 检查当前请求是否为WebSocket请求
                if (context.WebSockets.IsWebSocketRequest)
                {
                    // 接受WebSocket连接
                    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();

                    // 持续监听WebSocket消息
                    while (true)
                    {
                        // 准备接收缓冲区
                        var buffer = new byte[1024 * 4];
                        
                        // 接收WebSocket消息
                        var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

                        // 如果收到的类型为关闭消息,则关闭连接
                        if (result.MessageType == WebSocketMessageType.Close)
                        {
                            await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
                            break;
                        }

                        // 解码接收到的消息
                        var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
                        Console.WriteLine($"Received: {message}");

                        // 回复消息给客户端
                        await webSocket.SendAsync(
                            new ArraySegment<byte>(Encoding.UTF8.GetBytes($"Echo: {message}")),
                            result.MessageType,
                            result.EndOfMessage,
                            CancellationToken.None);
                    }
                }
                else
                {
                    // 如果不是WebSocket请求,则返回400错误
                    context.Response.StatusCode = 400;
                }
            });
        });
    }
}
复制代码

在上面的服务器端代码中,首先启用了WebSocket中间件,然后映射了一个/ws端点来处理WebSocket连接。

当收到连接请求时,我们接受连接并进入循环,监听客户端发送的消息,然后简单地回传一个回显消息。

3、说明

WebSocket为C#开发者提供了强大的实时通信能力,无论是构建复杂的实时数据流应用还是简单的聊天室,WebSocket都是一个值得考虑的选择。通过掌握客户端和服务器端的实现细节,可以充分利用WebSocket的优势,创建高性能和低延迟的实时应用。

三、 Socket 编程

Socket编程是计算机网络通信中的基础概念,它提供了在不同计算机之间发送和接收数据的能力。

在C#中,Socket编程主要通过System.Net.Sockets命名空间下的Socket类来实现。Socket可以用于创建TCP/IP和UDP两种主要类型的网络连接,分别对应于流式套接字(Stream Sockets)和数据报套接字(Datagram Sockets)。

1、Socket 基础

Socket地址族:指定网络协议的类型,如AddressFamily.InterNetwork用于IPv4。

Socket类型:SocketType.Stream用于TCP,SocketType.Dgram用于UDP。

Socket协议:ProtocolType.TcpProtocolType.Udp,分别用于TCP和UDP。

2、TCP Socket 客户端

TCP Socket客户端通常用于建立持久的连接,并通过流的方式发送和接收数据。

以下是一个简单的TCP客户端示例:

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public class TcpClientExample
{
    public static void Main()
    {
        try
        {
            // 创建一个新的Socket实例
            using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                // 连接到服务器
                IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
                IPEndPoint remoteEP = new IPEndPoint(ipAddress, 11000);
                socket.Connect(remoteEP);

                // 发送数据
                string message = "Hello Server!";
                byte[] data = Encoding.ASCII.GetBytes(message);
                socket.Send(data);

                // 接收服务器响应
                data = new byte[1024];
                int bytes = socket.Receive(data);
                Console.WriteLine("Received: {0}", Encoding.ASCII.GetString(data, 0, bytes));
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e.ToString());
        }
    }
}
复制代码

3、TCP Socket 服务器

TCP Socket服务器负责监听客户端的连接请求,并处理来自客户端的数据。

以下是一个简单的TCP服务器示例:

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public class TcpServerExample
{
    public static void Main()
    {
        try
        {
            // 创建一个新的Socket实例
            using (Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                // 绑定到本地端口
                IPAddress ipAddress = IPAddress.Any;
                IPEndPoint localEP = new IPEndPoint(ipAddress, 11000);
                listener.Bind(localEP);

                // 监听连接
                listener.Listen(10);

                // 接受客户端连接
                Console.WriteLine("Waiting for a connection...");
                Socket handler = listener.Accept();

                // 接收数据
                byte[] data = new byte[1024];
                int bytes = handler.Receive(data);
                Console.WriteLine("Text received: {0}", Encoding.ASCII.GetString(data, 0, bytes));

                // 发送响应
                string response = "Hello Client!";
                byte[] responseData = Encoding.ASCII.GetBytes(response);
                handler.Send(responseData);
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e.ToString());
        }
    }
}
复制代码

4、UDP Socket

UDP Socket用于无连接的、不可靠的网络通信,通常用于实时数据传输,如视频流或游戏。

以下是一个简单的UDP客户端和服务器示例:

UDP客户端

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public class UdpClientExample
{
    public static void Main()
    {
        try
        {
            // 创建一个新的Socket实例
            using (UdpClient client = new UdpClient())
            {
                // 发送数据
                string message = "Hello UDP Server!";
                byte[] data = Encoding.ASCII.GetBytes(message);
                IPEndPoint server = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 11000);
                client.Send(data, data.Length, server);

                // 接收服务器响应
                data = client.Receive(ref server);
                Console.WriteLine("Received: {0}", Encoding.ASCII.GetString(data));
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e.ToString());
        }
    }
}
复制代码

UDP服务器

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public class UdpServerExample
{
    public static void Main()
    {
        try
        {
            // 创建一个新的Socket实例
            using (UdpClient listener = new UdpClient(11000))
            {
                // 接收数据
                IPEndPoint client = new IPEndPoint(IPAddress.Any, 0);
                byte[] data = listener.Receive(ref client);
                Console.WriteLine("Text received: {0}", Encoding.ASCII.GetString(data));

                // 发送响应
                string response = "Hello UDP Client!";
                byte[] responseData = Encoding.ASCII.GetBytes(response);
                listener.Send(responseData, responseData.Length, client);
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e.ToString());
        }
    }
}
复制代码

以上示例展示了如何使用C#中的Socket类来实现TCP和UDP的客户端与服务器通信。

在实际应用中,可能还需要处理并发连接、错误处理和资源管理等问题。

此外,对于TCP通信,考虑到性能和资源使用,通常建议使用异步编程模型。

四、C# 网络安全

C# 中进行网络编程时,网络安全是一个至关重要的方面,涉及数据传输的保密性、完整性和可用性。以下是一些关键的网络安全知识点,它们对于构建安全的网络应用程序至关重要:

1、SSL/TLS 加密

在C#中使用HttpClient时,可以通过HttpClientHandler类来配置SSL/TLS相关的选项,确保HTTPS请求的安全性。

下面是一个示例,演示了如何使用HttpClientHandler来配置SSL/TLS设置:

复制代码
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // 创建 HttpClientHandler 实例
        var handler = new HttpClientHandler();

        // 配置 SSL/TLS 设置
        // 设置检查服务器证书的委托
        handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

        // 设置是否自动重定向
        handler.AllowAutoRedirect = true;

        // 设置代理
        // handler.UseProxy = true;
        // handler.Proxy = new WebProxy("http://proxy.example.com:8080");

        // 创建 HttpClient 实例
        using var httpClient = new HttpClient(handler);

        // 设置请求头部
        httpClient.DefaultRequestHeaders.Accept.Clear();
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        // 发送 HTTPS 请求
        var response = await httpClient.GetAsync("https://api.example.com/data");

        // 检查响应状态
        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            Console.WriteLine(content);
        }
        else
        {
            Console.WriteLine($"Failed to retrieve data: {response.StatusCode}");
        }
    }
}
复制代码

解释

  • ServerCertificateCustomValidationCallback:此属性允许你指定一个委托,用来验证服务器的SSL证书。在这个示例中,我们使用了HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,它会接受任何证书,这在测试环境中可能有用,但强烈建议在生产环境中使用更严格的证书验证逻辑。
  • AllowAutoRedirect:此属性控制是否允许HttpClient自动处理重定向。默认情况下,它是开启的。
  • UseProxy 和 Proxy:如果需要通过代理服务器发送请求,可以配置这两个属性。
  • DefaultRequestHeaders:用于设置请求的默认头部,如Accept,以指定期望的响应格式。

注意事项:

  • 实际应用中,不建议使用DangerousAcceptAnyServerCertificateValidator,因为它绕过了正常的证书验证,可能使应用程序暴露于中间人攻击。在生产环境中,应该实现自己的证书验证逻辑,确保只接受有效和可信的证书。
  • 此外,如果应用程序需要处理特定的SSL/TLS协议版本或加密套件,也可以通过SslProtocols属性进一步定制HttpClientHandler的SSL/TLS设置。
  • 例如,可以将其设置为SslProtocols.Tls12SslProtocols.Tls13,以限制使用的协议版本。

2、密码安全存储

在C#中安全地存储密码是一个至关重要的实践,尤其是当涉及到用户账户和敏感信息时。为了保护密码不被泄露或破解,应避免以明文形式存储密码,而是采用加密或哈希的方式。

以下是一些推荐的实践:

  • 使用哈希函数

使用安全的哈希函数,如SHA-256或SHA-512,可以将密码转换为一个固定长度的摘要。但是,简单的哈希容易受到彩虹表攻击,因此需要加入盐值(salt)。

示例代码:

复制代码
using System;
using System.Security.Cryptography;
using System.Text;

public static class PasswordHasher
{
    public static string HashPassword(string password, byte[] salt)
    {
        using (var sha256 = SHA256.Create())
        {
            var passwordSalted = Encoding.UTF8.GetBytes(password + Encoding.UTF8.GetString(salt));
            var hash = sha256.ComputeHash(passwordSalted);
            return Convert.ToBase64String(hash);
        }
    }

    public static byte[] GenerateSalt()
    {
        using (var rng = new RNGCryptoServiceProvider())
        {
            var salt = new byte[32];
            rng.GetBytes(salt);
            return salt;
        }
    }
}

// 使用示例
byte[] salt = PasswordHasher.GenerateSalt();
string hashedPassword = PasswordHasher.HashPassword("password123", salt);
复制代码
  • 使用加盐哈希

在哈希密码之前,先将随机生成的盐值与密码结合。这可以防止彩虹表攻击和暴力破解。

  • 使用慢速哈希函数

使用像PBKDF2、bcrypt、scrypt或Argon2这样的慢速哈希函数,可以显著增加破解难度,因为它们设计时考虑了防止暴力破解。

示例代码:

复制代码
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

public static class PasswordHasher
{
    public static string HashPasswordUsingBcrypt(string password)
    {
        using (var bcrypt = new Rfc2898DeriveBytes(password, 16, 10000)) // 16 bytes of salt, 10000 iterations
        {
            return Convert.ToBase64String(bcrypt.GetBytes(24)); // 24 bytes of hash
        }
    }
}

// 使用示例
string hashedPassword = PasswordHasher.HashPasswordUsingBcrypt("password123");
复制代码
  • 存储哈希和盐值

在数据库中,除了存储哈希后的密码,还应存储用于该密码的盐值,以便在验证时使用相同的盐值重新计算哈希。

  • 验证密码

在用户登录时,从数据库中检索哈希和盐值,使用相同的哈希函数和盐值对输入的密码进行哈希,然后与存储的哈希值进行比较。

示例代码:

public static bool VerifyPassword(string inputPassword, string storedHash, byte[] storedSalt)
{
    string hashOfInput = PasswordHasher.HashPassword(inputPassword, storedSalt);
    return hashOfInput == storedHash;
}
  • 不要存储密码重置问题的答案

密码重置问题的答案应该像密码一样被安全地处理,避免以明文形式存储。

ASP.NET Core提供了内置的密码哈希和验证方法,使用这些框架通常比手动实现更安全。总之,安全地存储密码涉及到使用强哈希算法、加盐、适当的迭代次数和存储机制。同时,保持对最新安全实践的关注,并定期更新代码以应对新的威胁。

3、防止SQL注入

使用参数化查询或ORM工具等,防止SQL注入攻击。

string query = "SELECT * FROM SystemUser WHERE Username = @username";
SqlCommand command = new SqlCommand(query, connection);
command.Parameters.AddWithValue("@username", inputUsername);

4、防止跨站脚本攻击(XSS)

对用户输入进行合适的编码和验证,防止恶意脚本注入。

string userContent = "<script>alert('XSS');</script>";
string encodedContent = HttpUtility.HtmlEncode(userContent);

5、防止跨站请求伪造(CSRF)

ASP.NET MVC可以使用Anti-Forgery Token等机制来防止CSRF攻击。

@Html.AntiForgeryToken()

6、身份验证和授权

使用更高级的身份验证机制,如JWT(JSON Web Token),并在应用中实施合适的授权策略。

[Authorize]
public ActionResult SecureAction()
{
    // 安全操作
}

7、判断文件安全

在C#中,判断一个文件是否”安全”可以从多个角度考量,这通常涉及到文件的来源、内容、权限以及是否包含潜在的恶意代码等。

下面我会介绍几种可能的方法来检查文件的安全性:

  • 检查文件的来源

确保文件是从可信的源下载或获取的。在Web应用程序中,可以使用Content-Disposition响应头来检查文件是否作为附件提供,以及文件名是否符合预期。

  • 验证文件的类型和扩展名

通过检查文件的扩展名或MIME类型来确定文件类型是否符合预期,例如,如果期望图片文件,那么只接受.jpg.png等扩展名。

复制代码
private bool IsFileSafeByExtension(string filePath)
{
    string[] allowedExtensions = { ".jpg", ".png", ".gif" };
    string extension = Path.GetExtension(filePath).ToLower();
    return allowedExtensions.Contains(extension);
}
复制代码
  • 检查文件的内容

使用文件签名或魔法数字来验证文件的实际类型与声明的类型是否一致,防止扩展名欺骗。

复制代码
private bool IsFileSafeByContent(string filePath)
{
    byte[] magicNumbers = File.ReadAllBytes(filePath);
    if (magicNumbers.Length >= 2 && magicNumbers[0] == 0xFF && magicNumbers[1] == 0xD8) // JPEG
    {
        return true;
    }
    // Add checks for other formats...
    return false;
}
复制代码
  • 扫描病毒和恶意软件

使用反病毒软件或在线API来检查文件是否含有病毒或恶意软件,VirusTotal 提供了API来检查文件是否含有病毒,https://www.virustotal.com/ 具体示例如下

复制代码
using System;  
using System.Net.Http;  
using System.Threading.Tasks;  
using Newtonsoft.Json; // 需要安装Newtonsoft.Json NuGet包  
  
class Program  
{  
    static async Task Main(string[] args)  
    {  
        string apiKey = "API密钥";  
        string fileUrl = "文件ID";  
  
        string url = $"https://www.virustotal.com/vtapi/v3/files/{fileUrl}/report";  
        HttpClient client = new HttpClient();  
        client.DefaultRequestHeaders.Add("x-apikey", apiKey);  
  
        HttpResponseMessage response = await client.GetAsync(url);  
  
        if (response.IsSuccessStatusCode)  
        {  
            string responseBody = await response.Content.ReadAsStringAsync();  
            dynamic report = JsonConvert.DeserializeObject(responseBody);  
  
            if (report.positives > 0)  
            {  
                Console.WriteLine("文件含有病毒或恶意软件。");  
            }  
            else  
            {  
                Console.WriteLine("文件安全。");  
            }  
        }  
        else  
        {  
            Console.WriteLine("API请求失败。");  
        }  
    }  
}
复制代码
  • 检查文件权限

确保文件具有正确的权限,以防止未经授权的访问。

复制代码
private bool IsFileSafeByPermissions(string filePath)
{
    var fileInfo = new FileInfo(filePath);
    var security = fileInfo.GetAccessControl();
    // Check permissions here...
    return true; // Placeholder logic
}
复制代码
  • 文件大小检查

限制文件的大小,避免消耗过多的磁盘空间或内存。

private bool IsFileSafeBySize(string filePath, long maxSizeInBytes)
{
    var fileInfo = new FileInfo(filePath);
    return fileInfo.Length <= maxSizeInBytes;
}
  • 内容安全策略(CSP)

在Web应用中,使用CSP来限制加载的资源类型和来源,防止XSS等攻击。

  • 综合检查函数示例
复制代码
private bool IsFileSafe(string filePath)
{
    return IsFileSafeByExtension(filePath) &&
           IsFileSafeByContent(filePath) &&
           IsFileSafeFromVirus(filePath) &&
           IsFileSafeByPermissions(filePath) &&
           IsFileSafeBySize(filePath, 1024 * 1024); // Limit to 1MB
}
复制代码

请注意,上述代码片段仅作为示例,实际应用中可能需要调整和补充具体的实现细节,例如引入实际的病毒扫描库或API,以及更复杂的权限和内容检查逻辑。

安全检查是多层面的,需要结合具体的应用场景和需求进行综合考量。

8、安全的Cookie处理

Cookies是Web开发中用于存储用户信息的一种常用机制,它们可以在客户端浏览器中保存小量的数据,以便服务器可以跟踪用户的偏好设置、登录状态等信息。然而,如果Cookie处理不当,可能会引发严重的安全问题,如数据泄露、会话劫持(Session Hijacking)和跨站脚本攻击(XSS)。因此,确保Cookie的安全处理至关重要。

以下是处理Cookie时应当遵循的一些最佳实践:

  • 使用HTTPS:传输Cookie时,务必使用HTTPS加密连接。HTTPS可以防止中间人攻击(Man-in-the-Middle Attack),保护Cookie数据免受窃听。
  • 设置HttpOnly标志:将Cookie标记为HttpOnly可以阻止JavaScript脚本访问Cookie,从而降低跨站脚本攻击(XSS)的风险。
  • 设置Secure标志:当Cookie被标记为Secure时,它们只会在HTTPS连接下发送,确保数据在传输过程中的安全性。
  • 限制Cookie的有效路径和域:通过设置Cookie的Path和Domain属性,可以控制哪些页面可以访问特定的Cookie,减少攻击面。
  • 使用SameSite属性:SameSite属性可以控制Cookie是否随跨站点请求发送,减少跨站请求伪造(CSRF)攻击的可能性。可以选择Strict、Lax或None三种模式之一。
  • 设置合理的过期时间:为Cookie设定一个适当的过期时间,可以避免永久性Cookie带来的安全风险,同时也便于清理不再需要的用户信息。
  • 定期审查和更新Cookie策略:定期检查Cookie的使用情况,确保所有Cookie设置符合最新的安全标准和隐私法规。

通过遵循这些最佳实践,可以大大增强应用程序的安全性,保护用户数据免受恶意攻击。在Web开发中,安全的Cookie处理不仅是技术要求,也是对用户隐私和数据安全的责任体现。

复制代码
using System;
using System.Web;

public class CookieHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        // 创建一个新的Cookie对象
        HttpCookie cookie = new HttpCookie("UserSession");

        // 设置Cookie值
        cookie.Value = "123456"; // 假设这是用户的唯一标识符

        // 设置Cookie的过期时间
        cookie.Expires = DateTime.Now.AddDays(1); // 设置Cookie在一天后过期

        // 设置HttpOnly属性以增加安全性
        cookie.HttpOnly = true;

        // 如果你的网站支持HTTPS,设置Secure属性
        if (context.Request.IsSecureConnection)
            cookie.Secure = true;

        // 添加Cookie到响应中
        context.Response.AppendCookie(cookie);
    }

    public bool IsReusable
    {
        get { return false; }
    }
}
复制代码

在.NET Core或.NET 6+中,使用不同的API来处理Cookie,例如Microsoft.AspNetCore.Http命名空间下的IResponseCookies接口。

五、总结

通过文章的全面介绍 C# 网络编程,相信对这一块内容有了了解和理解。从简单的 HTTP 请求到复杂的套接字通信,从异步编程模型到安全协议的应用,每一步都为我们构建现代网络应用奠定了坚实的基。在实际项目中,根据需求深入学习和实践这些知识点,将有助于提升.NET开发者在网络编程领域的能力。持续学习和实践是成为优秀 .NET 开发者的不二法门。

如果觉得这篇文章对你有用,欢迎加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行交流心得,共同成长。

我们的前端开发逆天了!1 小时搞定了新网站,还跟我说 “不要钱” - 程序员鱼皮 - 博客园

mikel阅读(127)

来源: 我们的前端开发逆天了!1 小时搞定了新网站,还跟我说 “不要钱” – 程序员鱼皮 – 博客园

大家好,我是程序员鱼皮。前段时间我们上线了一个新软件 剪切助手 ,并且针对该项目做了一个官网:

很多同学表示官网很好看,还好奇是怎么做的,其实这个网站的背后还有个有趣的小故事。。。

鱼皮:我们要做个官网,能下载应用就行,一周时间怎么样?

我们的前端开发 – 多喝热水同学:一周?太小瞧我了吧,1 小时给你搞定!

鱼皮:唔嘈,你很勇哦?

本来以为他是开玩笑的,没想到,1 小时后,他真的给我看了网站效果,而且比预期的好太多了。我的评价是:逆天

他给我解释道:其实我用了一个新框架,基本不用自己写代码,而且还可以白票平台来免费部署网站~

鱼皮:不错不错,回头给我的读者们也分享一下!

于是,就有了下面这篇文章:

对于前端同学来说,用的最多的 Web 框架无非就是 React/Vue/Angular 这三大件了,那本文将带你了解一个新的 Web 框架 Astro,并手把手带你使用 Astro 搭建一个属于自己的站点,用过的都说真香!

关于技术栈的选择

假如现在有这样一个需求,公司需要你去做一个官网落地页,没什么别的要求,界面美观且能介绍公司就行,你会怎么选技术栈?

如果是以前,我可能会挑一个自己熟悉的语言去快速开发,但是现在你问我选什么技术栈,我可能会选择 Astro,为什么?且听我娓娓道来~

首先如果是自己花时间去开发的话,我们需要搭建网站的整体布局,如导航栏、logo、页脚等等,还需要考虑移动端的适配、网站 SEO 优化等等…

我不知道你们会不会觉得有点烦?反正我是有点烦了,且自己做出来的可能还没那么好看…

image

如果布局、适配、SEO 这些都配好了,只需要改改文字的话那该多好!

image

没错,依靠 Astro 强大的主题生态就可以帮助我们快速完成这些事情!像我们公司的产品 剪切助手(https://jianqiezhushou.com) 的官网就是用 Astro 搭建的,如下:

image

image

还是很好看的有木有,且移动端的响应式适配、SEO 通通都搞定,一举多得!

接下来我们就简单了解一下 Astro 这个框架,然后使用 Astro 快速生成一个自己的站点!

Astro 框架介绍

关于 Astro 的介绍,官方文档(https://docs.astro.build/zh-cn/concepts/why-astro)给出了很明确的定位:“最适合构建像博客、营销网站、电子商务网站这样的以内容驱动的网站的 Web 框架”

image

它默认就支持服务端渲染,且支持 React、Preact、Svelte、Vue、Solid、Lit、HTMX、Web 组件,这意味着你可以用任意框架的写法来编写 Astro,这一特性在 Astro 中被称为 “群岛”。

快速拥有一个 Astro 应用

这里我们不会从零到一的去介绍 Astro 的写法,感兴趣的同学可以简单从官网过一遍入门指南(https://docs.astro.build/zh-cn/getting-started),我们要做的就是依靠 Astro 强大的主题模板,实现只需要改改文字、写一写 Markdown 就能轻松搭建一个漂亮的博客网站!

1)选择主题模板

进入 Astro 官方模板网站(https://astro.build/themes),挑选一个自己心仪的模板,如下:

image

我选择的模板是 https://astro.build/themes/details/astro-boilerplate/ ,我们进入到这个模板的详情页,可以看到有两个按钮,如下:

image

第一个是源码,第二个是在线效果的演示。

我们点击 Get Started 获取项目的源代码。

2)查看模板的 README 文档

通过 README 文档我们可以了解到如下信息

  1. 怎么去启动这个项目?
  2. 怎么构建发布?

如下图:

image

那么接下来我们就按照 README 中所描述的步骤来操作,第一步我们先把项目拉取到本地,执行如下命令:

git clone --depth=1 https://github.com/ixartz/Astro-boilerplate

image

在编辑器中打开这个项目,并安装项目依赖,如下:

image

安装依赖完成后启动项目,项目启动后我们访问 http://localhost:4321 ,如下图:

image

ok,现在我们就得到了一个最原始的模板,和之前的预览效果是一致的,如下:

image

3)更换项目中的个人信息

现在我们要做的就是把里面的文字换成自己的信息,没有的信息我们可以删掉,这里可以通过查看 index.astro 文件来了解整个网站的大致布局,从而找到我们要调整的组件,如下:

image

如果你不知道怎么调整也可以用另一种更简单的方法,直接搜索内容关键词,来找到我们要修改的内容,如下:

image

下面是我调整后的效果,如下:

image

看起来也不赖,主打一个简约风格。

4)如何发文

这是一个博客站,所以还需要找到发文入口,我们找到 posts 文件夹,在此文件夹下编写 markdown 文件即可,配置按已有的格式修改,如下:

image

ok,接下来我们就尝试一下发一篇文章,在 posts 文件夹下新建一个 md 格式的文件,往里面写入一些内容,如下:

image

可以看到,我们编写的 markdown 已经被解析为了文章~

至此网站的搭建已经结束了,剩下的就是自己在上面添加内容。

现在网站我们有了,还需要让别人能够访问你的网站,一般到这一步我们需要服务器,域名等等,如果没有的话怎么办?

白票!!将白票贯彻到底!!!

image

我们可以白票的第三方服务有:

1)GitHub Pages

2)Netlify

3)Cloudflare

4)Vercel

等等…

这里我们就以 Netlify 为例,其他的大家感兴趣可以自行去了解。

部署

1)创建仓库

首先我们需要一个能够存放代码的地方,我们去 GitHub 创建一个代码仓库,并上传代码,如下:

image

2)将仓库关联到 Netlify

进入到 Netlify登录页(https://app.netlify.com/login),这里因为我们的代码放在了 GitHub,所以我们选择使用 GitHub 进行登录,如下:

image

选择导入已有的项目,如下:

image

从 GitHub 导入,如下:

image

找到我们博客所在的代码仓库,如下:

image

点击仓库我们会进入到部署配置页,如下:

image

一些关键的配置说明都列出来了,按照要求配置即可,没有特别说明的目前不需要关注,点击部署后等待几分钟即可完成部署,如下:

image

现在我们访问 https://codereshui.netlify.app 就能看到部署的网站了,且后续有新的内容变更的时候(比如发文),网站会自动构建并发布!

妥妥的一条龙服务!!!好了,这篇文章就肝到这里,大家也可以把自己的网站搞起来了~

mysql 使用show tables表存在,但是select时却提示表不存在!_show tables 能查到表 select 查不到表-CSDN博客

mikel阅读(250)

来源: mysql 使用show tables表存在,但是select时却提示表不存在!_show tables 能查到表 select 查不到表-CSDN博客

这个问题困扰了很久,情况是某张表被损坏。

直接使用select查询时报错:

ERROR 1146 (42S02): Last_Error: Error ‘Table ‘xxxxxx’ doesn’t exist’ Error ” ERROR 1146 (42S02): Table
咦!怎么表无缘无故就不见了???

使用 show tables; 命令却发现表是存在的,瞬间懵逼了。无论发生什么情况,肯定是有原因的,哈哈哈。

最终解决方案是,不需要停mySQL服务,直接通过mySQL的my.conf查看到datadir目录,进入目录后,找到指定出问题的表关联文件:

1、tableName.frm
2、tableName.ibd
直接删除,至此才把出问题的表完全删除掉。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/yangjiechao945/article/details/96458466

报错 General error: 1366 Incorrect string value: ‘\xF0\x9F\x8D\x83‘ for column ‘per_name‘ at row 1-CSDN博客

mikel阅读(126)

解决办法:将数据库此字段设置为 utf8mb4_general_ci 即可。

来源: 报错 General error: 1366 Incorrect string value: ‘\xF0\x9F\x8D\x83‘ for column ‘per_name‘ at row 1-CSDN博客

插入数据报错:
SQLSTATE[HY000]: General error: 1366 Incorrect string value: ‘\xF0\x9F\x8D\x83’ for column ‘per_name’ at row 1。

产生错误原因是,入库字段设置的字节无法满足要求。一般文字字节在1-3之间,但是有些生僻字,例如产生此报错的文字是四个字节就无法入库而报错。

解决办法:将数据库此字段设置为 utf8mb4_general_ci 即可。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/lwaimj/article/details/110536257

C# 开发技巧 轻松监控方法执行耗时 - 小码编匠 - 博客园

mikel阅读(151)

来源: C# 开发技巧 轻松监控方法执行耗时 – 小码编匠 – 博客园

MethodTimer.Fody 是一个功能强大的库,可以用于测量 .NET 应用程序中的方法的执行时间。允许你在不修改代码的情况下,自动地测量和记录方法的执行时间。

这个工具是基于.NET的 weaving 技术,通过修改IL(Intermediate Language,中间语言)代码来插入计时逻辑,从而在方法调用前后记录时间戳,进而计算出方法的执行时间。

它使用 Fody 插件框架可以无缝集成到项目中,所以向代码中添加性能测量功能变得非常容易。

使用方法

1、安装NuGet包

在Visual Studio中,打开NuGet包管理器,搜索并安装MethodTimer.Fody或者使用命令方式

PM> Install-Package Fody
PM> Install-Package MethodTimer.Fody

具体操作如下图所示:

2、使用 Time 特性

复制代码
using MethodTimer;

namespace DemoConsole
{
    internal class Program
    {
        /// <summary>
        /// 程序入口
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            // 调用示例方法
            new Program().DoSomething();

            Console.WriteLine("测试方法执行结束!!!");

            Console.ReadKey();
        }

        /// <summary>
        /// 示例方法
        /// </summary>
        [Time]
        public void DoSomething()
        {
            Console.WriteLine("测试方法执行时间!!!");
        }
    }
}
复制代码

Fody是一个.NET的weaving框架,需要确保项目已经启用了Fody,并且在项目属性的”Fody”标签页中添加了MethodTimer模块。

3、执行效果

启动运行程序,可以在输出窗口查看方法的执行耗时,具体如下图所示:

4、其他说明

Time 特性不仅可以加在方法上还可以直接添加到 Class 上,具体如下代码所示:

复制代码
using MethodTimer;

namespace ConsoleApp3
{
    [Time]
    internal class Program
    {
        /// <summary>
        /// 程序入口
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            // 调用示例方法
            new Program().DoSomething();

            new Program().ToDoSomething();

            Console.WriteLine("方法执行结束!!!");

            Console.ReadKey();
        }

        /// <summary>
        /// 示例方法1
        /// </summary>
      
        public void DoSomething()
        {
            Console.WriteLine("001——测试执行时间方法!!!");
           
        }
        /// <summary>
        /// 示例方法2
        /// </summary>

        public void ToDoSomething()
        {
            Console.WriteLine("002——测试执行时间方法!!!");

        }
    }
}
复制代码

运行程序后,可以输出类中每个方法的执行时间。

实际上,在代码中添加了 Time 特性以后,Fody 会自动生成下面的代码

复制代码
 public class MyClass
 {
        [Time]
        public void DoSomething()
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();

            // 原始方法体
            System.Threading.Thread.Sleep(1000); // 模拟工作

            stopwatch.Stop();

            // 输出或记录执行时间
            Console.WriteLine($"执行时间:{stopwatch.Elapsed.TotalMilliseconds} ms");
        }
 }
复制代码

5、拦截记录

如果想手动处理日志记录,可以定义一个静态类来拦截日志记录,方法的示例,具体如下代码所示

复制代码
public static class MethodTimeLogger
{
    public static void Log(MethodBase methodBase, TimeSpan elapsed, string message)
    {
        //Do some logging here
    }
}
复制代码

生成后的代码

复制代码
public class MyClass
{
    public void MyMethod()
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            Console.WriteLine("Hello");
        }
        finally
        {
            stopwatch.Stop();
            MethodTimeLogger.Log(methodof(MyClass.MyMethod), stopwatch.Elapsed);
        }
    }
}
复制代码

MethodTimer.Fody是一个非常有用的工具,尤其在性能调优阶段,可以帮助你快速识别出哪些方法是性能瓶颈,从而针对性地进行优化。

主要特点

1、非侵入式

MethodTimer.Fody不需要在源代码中添加额外的计时代码,只需要在项目中添加相应的NuGet包,并在项目属性中做一些配置,就可以自动地为方法添加计时功能。

2、灵活的配置

你可以选择性地对某些方法进行计时,或者排除不想被计时的方法。这通常通过方法的特性或者类的命名空间来进行配置。

3、输出结果多样化

MethodTimer.Fody可以将计时结果输出到不同的地方,如控制台、日志文件或者通过事件追踪(ETW)等方式,这取决于你的配置。

4、性能影响小

尽管MethodTimer.Fody在方法中插入了计时逻辑,但它被设计得尽可能地对性能影响最小,通过精心优化的IL代码插入策略来实现这一点。

总结

MethodTimer.Fody 是一个强大的工具,提供了简便的方式来监控 C# 方法的执行时间,特别适用于需要快速诊断性能问题的场合。

通过其灵活的配置和非侵入性的特性,它可以无缝地融入现有的开发流程中,帮助我们团队提高应用的性能和响应速度。

这个工具特别适合在开发和测试阶段快速识别性能瓶颈,而无需在代码中显式地添加计时代码,可以保持源代码的整齐性和可维护性。

开源地址

https://github.com/Fody/MethodTimer

 

如果觉得这篇文章对你有用,欢迎加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行交流心得,共同成长。

 

【弯曲矫正】智能文字识别技术-弯曲矫正概述 - 合合技术团队 - 博客园

mikel阅读(150)

来源: 【弯曲矫正】智能文字识别技术-弯曲矫正概述 – 合合技术团队 – 博客园

一、背景

电子文档由于更容易存档、编辑、签名和共享,越来越多的文档需电子化,随着高质量摄像头在手机等移动设备上的普及,利用移动设备对文档进行数字化采集已经非常普遍。通过图像校正与图像质量提升,移动设备采集的文档图像质量甚至可以与专用的文档扫描仪相当。然而,文档总是由于纸张几何形状和捕获条件不受控制而形变。这阻碍了形变图像的信息提取,降低可读性,对数据增强和下游任务如OCR识别、版面分析与还原等任务增加难度。

二、方法概述

为解决文档弯曲矫正问题,学术界已有多种方案。

一类是利用多目相机,结构光或者激光雷达等设备对文档进行扫描,获得文档表面的3D结构信息,进而对文档校正展平。这类方法一般可以得到比较好的校正效果,但依赖专用设备的特点限制了其使用场景。

还有一类是利用显式的几何模型以适应形变文档曲面,这类方法完全依靠图像信息以及文档形变的先验知识对图像进行校正。这类方法一般需要进行文字行或者表格线的检测,并假设曲面符合特定的几何约束,如曲面是柱面。这类方法可以在普通的移动设备上实现,但是其校正效果受文字行检测准确度的限制,对文档版式比较敏感,无法处理存在大量图表的文档,且误检的文字行有可能会对校正造成严重干扰。

还有一类基于优化的方法,利用损失函数缓慢迭代优化以获得形变矫正结果,但时间较长不适合实时应用。

最近,数据驱动的方法已经流行起来。 这些方法训练一个形变矫正神经网络,学习形变场,从而得到类似扫描的结果。 这样的网络可以实现实时矫正。Das等人使用 CNN 检测文档的折痕并进行分割文件分成多个块进行矫正。 Xing等人 应用CNN估计文档变形和相机姿态以进行校正。 Ramanna等人通过利用 pix2pixhd 网络去除文档的卷曲和几何失真。 然而,这些方法仅适用于简单变形和单调背景。

Ma等人提出了一个堆叠的 U-Net,它经过训练端到端预测翘曲的前向映射。 由于生成的数据集与真实世界的图像有很大不同,[15] 对其进行了训练在真实世界的图像上测试时,数据集的泛化能力较差。Das等人认为当合成训练数据集仅使用 2D 变形进行训练时,弯曲矫正模型并不总是表现良好,因此他们创建了一个 Doc3D 数据集,该数据集具有多种类型的像素级文档图像偏移场,同时使用真实世界文档和渲染软件。

同时,提出了一种去扭曲网络和细化网络来校正文档图像的几何和阴影。李等人 在 3D 空间中生成训练数据集,并使用渲染引擎获得更精细、更逼真的失真文档图像细节。他们提出了基于图像块(patch)的学习方法,并通过在梯度域中的处理将patch结果拼接到校正后的文档中,以及用于去除阴影的光照校正网络。与之前的方法相比,这些文献更关心生成的训练数据集和真实世界测试数据集之间的差异,并专注于生成更真实的训练数据集以提高真实世界图像的泛化能力。尽管这些结果令人惊叹,但深度神经网络的学习和表达能力并未得到充分探索。

三、合合方案

我们将弯曲矫正问题定义如下:

其中u 是形变场S(Source)是弯曲图T(Target)是平整图。一个理想的空间变换(spatial transformation)需要有两个衡量标准,及相似度和正则项,一方面我们期望弯曲样本变换后与目标(平整样本)越相似越好,图像相似性有很多种标准,常见的有相关系数(Correlation Coefficient, CC)、归一化的相关系数(NCC)、互信息(Mutual Information, MI)均方误差(MSE)等。

另一方面,我们也希望这个变换是空间上平滑且连续的,这样能保证变换遵循物理模型,存在连续可逆的变换,使得我们的变换在数据合成等方面有更广泛的应用。

和相似度损失函数类似,正则项在网络里也有多种实现方式,一种是通过对位移场直接进行空间梯度惩罚,一种则是通过对速度场进行约束后再通过积分层得到最终形变场,还有一种则是在训练过程中通过循环损失函数来实现。

形变矫正网络可以是encoder-decoder类似结构,由于惩罚项如果直接施加在位移场上,大位移场景模型的矫正能力就会降低,有方案通过多次迭代矫正过程位移场来实现大形变。

我们则参考配准中的流模型(fluid model),用速度场来建模形变场,并通过积分层来实现最终的形变场。事实上,位移场也可以被视作是轨迹固定的流场(直线)。 对于不同的正则项, 在大部分情况下,直线轨迹并不是最优解。直线轨迹得到的正则项的值很多情况下会更大点。 作为对比,引入速度场在这种情形下实现了更多的自由度。

如果你对这一块感到困惑,可以想象连接世界地图上两个地方的最短路径, 大部分情况下都不是直线 [Ref]。速度场求解可转换为如下问题,其中L是对速度场施加的正则项。

 

空间变换网络一开始提出时只是简单用作仿射变换等,后来采用了采样网格的方式使得它功能更加强大。对于大小为[W, H]的二维图像来说,其位移场大小为[W, H, 2]。位移场表示每个像素在各个方向(x,y轴)的位移。空间变换网络会根据位移场生成一个归一化后的采样网格,然后用该网络对图像进行采样,就得到了矫正后的图像。

 


 

  1. Shaodi You, et al. 2017. Multiview Rectification of Folded Documents. IEEE Transactions on Pattern Analysis and Machine Intelligence.
  2. Taeho Kil, et al. 2017. Robust Document Image Dewarping Method Using Text-Lines and Line Segments. In Proceedings of the International Conference on Document Analysis and Recognition. IEEE, 865;870.
  3. Beom Su Kim, et al. 2015. Document Dewarping via Text-Line Based Optimization. Pattern Recognition 48, 11 (2015), 3600–3614.
  4. Sagnik Das, et al. 2019. DewarpNet:Single-image Document Unwarping with Stacked 3D and 2D Regression Networks. In Proceedings of the International Conference on Computer Vision.
  5. Hao Feng, et al. 2021. DocTr:Document Image Transformer for Geometric Unwarping and Illumination Correction. In Proceedings of the ACM International Conference on Multimedia.
  6. Guo-Wang Xie, Fei Yin, Xu-Yao Zhang, and Cheng-Lin Liu. 2020. Dewarping Document Image by Displacement Flow Estimation with Fully Convolutional Network. In Document Analysis Systems. Springer, 131–144.
  7. Gaofeng Meng, et al. 2015. Extraction of Virtual Baselines from Distorted Document Images Using Curvilinear Projection. In Proceedings of the International Conference on Computer Vision.
  8. Vincent Arsigny, et al. 2005. A log-Euclidean framework for statistics on diffeomorphisms. In International Conference on Medical Image Computing and Computer-Assisted Intervention, pages 924–931. Springer.
  9. John Ashburner. 2007. A fast diffeomorphic image registration algorithm. Neuroimage, 38(1):95–113.
  10. Beg, M.F., et al. 2005. Computing large deformation metric mappings via geodesic flows of diffeomorphisms. Journal of Computer Vision, 139–157.
  11. Brian Avants et al. 2004. Geodesic estimation for large deformation anatomical shape averaging and interpolation. Neuroimage, 23:S139–S150.
  12. Adrian V Dalca, et al. 2019. Unsupervised learning of probabilistic diffeomorphic registration for images and surfaces. Medical image analysis, 57:226–236.
  13. Zhengyang Shen, et al. 2019. Networks for joint affine and non-parametric image registration. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition, pages 4224–4233.
  14. fluid(流) 方法图像配准简介 – 知乎
  15. GitHub – uncbiag/registration: Image Registration

表格识别与内容提炼技术理解及研发趋势 - 合合技术团队 - 博客园

mikel阅读(146)

来源: 表格识别与内容提炼技术理解及研发趋势 – 合合技术团队 – 博客园

引言:

表格是各类文档中常见的对象,其结构化的组织形式方便人们进行信息理解和提取。表格的种类根据有无边框可以划分有线表、少线表、无线表。表格样式复杂多样,如存在背景填充、光照阴影、单元格行列合并等情况。大数据时代存在大量电子文档,应用表格识别技术能够减少表格处理时间,因此表格识别是文档理解领域的重要研究课题,也是合合信息这几年的技术突破点方向之一。

表格识别主要包括表格检测和表格结构识别两个子任务。

表格检测主要检测表格主体,即样本中表格区域。表格结构识别是对表格区域进行分析,提取表格中的数据与结构信息,得到行列分布与逻辑结构。未经特殊说明,以下表格识别专指表格结构识别。

 

研究现状与解决方案:

近年来,国内外专家学者对表格识别进行了大量研究,取得了丰富的研究成果。下面主要从传统图像处理方法和深度学习这两个方面做具体阐述。

传统方法

传统的表格识别工作是基于一些启发式的规则和图像处理方法,主要利用表格线或者文本块之间的空白分隔区域来确定单元格区域,通过腐蚀、膨胀,找连通区域,检测线段、直线,求交点,合并猜测框等。有自顶向下的方法(先检测表格区域,再不断对表格区域进行切割拆分得到单元格区域);还有自底向上的方法(先检测文本块,找到可能的表格线以及这些线的交点,确定单元格后还原出表格区域)

1、 OpenCV-检测并提取表格[1]

这是一种自顶向下的方法,先对图像进行二值化,然后使用霍夫变换,检测其中的直线,并在直线中,找到围成的一个矩形区域,最后将这块区域提取出来。

作者主要使用了四步操作
1、处理图像,灰度化,二值化。在灰度图的基础上运用adaptiveThreshold来达成自动阈值的二值化,取代Canny,这个算法在提取直线和文字比Canny有更好的效果
2、利用OpenCV里面的形态学函数,腐蚀erode膨胀dilate
3、交叉横纵线条,对点进行定位,通过bitwise_and函数获得横纵线条的交点,通过交点对表格区域进行提取
4、判断区域是否为表格。

虽然此方法可以相对完整的识别图片中的表格,但也存在几个问题:
1、图片倾斜不易识别
2、图片背景复杂会干扰识别
3、少线表情况,表格只有上下两条线的时候如何判断

 

2、pdfplumber解析表格[2]

pdfplumber是一款完全用python开发的pdf解析库,对于全线表,pdfminer能够实现较好的抽取效果,但对于少线表和无线表,效果就差了很多。

下面介绍pdfplumber中的表格抽取流程,这是一种自底向上的方法,首先找到可见的或猜测出不可见的候选表格线;然后根据候选表格线确定它们的交点;接着根据得到的交点,找到围成它们的最小单元格;最后把连通的单元格进行整合,生成检测出的表格对象。在表格生成的过程中,利用单元格的bbox坐标(四个角的坐标)判断单元格是否属于当前表格;同时对表格的左上角坐标进行排序,过滤掉小表格。

3、camelot表格抽取[3]

camelot是一个可以从可编辑的pdf文档中抽取表格的开源框架,与pdfplumber相比,其功能完备性差了点,除了表格抽取之外,并不能用它从pdf文档中解析出字符、单词、文本、线等较为低层次的对象。

 

camelot支持两种表格抽取模式:

 

一、lattice线框类表格抽取,步骤如下:
1、pdf转图像
2、图像处理算法检测出水平方向和竖直方向可能用于构成表格的直线
3、根据检测出的直线,生成可能表格的bounding box
4、确定表格各行、列的区域
5、根据各行、列的区域,水平、竖直方向的表格线以及页面文本内容,解析出表格结构,填充单元格内容,最终形成表格对象。

二、stream少线框类表格抽取,步骤如下:
1、通过pdfminer获取连续字符串
2、通过文本对齐的方式确定可能表格的bounding box
3、确定表格各行、列的区域
4、根据各行、列的区域以及页面上的文本字符串,解析表格结构,填充单元格内容,最终形成表格对象。

4、T-recs[4]

这是一种自底向上的方法,核心思想是对文本块区域进行聚类。

步骤如下:
1、从文本块中选择种子点
2、在种子点上下各一行分别去找与该种子点文本块之间是否水平方向有重合,如果有重合则将相应文本块和种子点块归到一起,并作为新的种子点
3、重复第1、2步,不断找与之水平方向有重合的文本块,直到所有的文本块都不能再合并下去。

此方法也存在诸多局限:
1、表头是跨单元格的,表头下面的文本块会被全部合并到一起
2、有时候上下几行文本确确实实是对齐的,但是和左右文本区域比较近,这种本不能分开的区域被错误的分开了
3、孤立的文本块会被切分成单独块。

因此,本方法后面大部分工作是针对这三种局限设定后处理规则,该方法认为表格之所以是表格是由文本块的分布决定的,而与分割带无关。加入后处理规则之后该方法方法具有较好的通用性,无论是对与PDF文档还是OCR的结果,都有比较好的效果。

深度学习方法

近年来人工智能技术飞速发展,研究人员将CV,NLP和图神经等成熟方法应用在表格识别任务中,取得很多不错的成果。深度学习表格识别主流方法包括语义分割,目标检测,序列预测和图神经等,下面我们对这些工作分别进行介绍。

1. 语义分割方法:

1.1 Rethinking Semantic Segmentation for Table Structure Recognition in Documents[5]

本文将表格结构的识别定义为语义分割问题,使用FCN网络框架,对表格的行和列分别进行预测。主要介绍了一种对预测结果进行切片的方法,降低了表格识别的复杂度。使用了FCN的Encoder和Decoder的结构,并加载了在ImageNet预训练好的模型。图片经过模型生成了与原图大小相同的特征,切片过程将特征按照行和列进行平均,将H*W*C(高*宽*Channel)的特征合并成了H*C和W*C大小特征;对这些特征进行卷积后,再进行复制,扩展为H*W*C的大小,再通过卷积层得到每个像素点的标签;最后进行后处理得到最终的结果。本文pipeline如图1所示。本文方法的局限在于本文所处理的表格对象中所有的单元格不存在跨行跨列,每行每列都从表格的最左侧和最上端开始,到最右侧和最下端结束。

图1

 

1.2 腾讯表格识别技术方案[6]

图像分割是对图像的每个像素点赋予标签,在表格识别任务中,每个像素可能属于横线、竖线、不可见横线、不可见竖线这几个标签。

解决方案流程如图2所示:
1、表格线标注:横向的线,竖向的线,横向的不可见线,竖向的不可见线。类别不互斥,考虑到单元格交点问题,即交点处的像素属于多个类别
2、几何分析提取连通区域,对连通区域拟合折线,合并形成框线;考虑图片弯曲、表格倾斜的情况,利用投影变换对原图矫正
3、调用ocr,识别文本内容,确定字符坐标
4、根据第二步的框线计算行列信息,判断单元格合并情况,得到每个单元格在途中的位置
5、根据单元格坐标和字符坐标,将字符嵌入到单元格,还原表格。

此方案专注于将页面拍照后进行表格识别,对于一般的表格效果还好,但现实场景太过纷杂,仍有很多问题亟待解决。

图2

2. 物体检测方法:

2.1 海康LGPMA方案[7]

此方案是ICDAR21比赛Table Recognition赛道的冠军,LGPMA将表格识别分为文本行检测、文字识别和表格结构识别三部分。文本检测模块是一个单行文本检测器,文字识别模块是一个基于attention 的识别器,这两部分用来获取表格图像中的文本信息。表格结构识别部分采用的是一种LGPMA的方案,基于Mask-RCNN同时出两个分割头,一个LPMA学习局部对齐边界,一个GPMA学习全局对齐边界,融合了自顶向下和自底向上两种思想。

如图3所示。在得到两路的soft mask之后,将LPMA和GPMA的对齐mask融合,之后对每个单元格边框进行精修。最后经过cell matching , empty cell searching 和 empty cell merging三个后处理步骤得到最终的表格结构。原方案采用较大的基础网络,训练推理对硬件及输出尺寸有一定约束,实际落地较为困难。

图3

2.2 角点表格检测法[8]

针对表格检测本文使用了“角点”来提升表格检测的精确度,对比基本模型在检测结果上能够得到进一步的检测与提升。

首先引入角点的概念,如图4所示:角点是表格四个顶点周围的一部份区域,这些区域大小相同,同一个表格的所有角点构成一个角点组,角点的检测与表格检测一样,可以使用目标检测模型来解决,作者使用Faster R-CNN模型,同时进行角点和表格的检测,检测结构如图5所示,使用角点组对对应的表格检测的横坐标进行校准,得到最终的表格区域。该方法与未加入角点的Faster R-CNN模型相比,结果有了较大的提升。

图4

图5

基于CenterNet的端到端表格识别方案[9]

目标检测识别往往是在图像上将目标用矩形框的形式框出,目标检测器都先穷举出潜在目标位置,然后对该位置进行分类,这种做法浪费时间,低效,还需要做额外的后处理。

CenterNet是将目标作为一个点(BBOX中心点),利用中心点回归其他目标属性,比如尺寸、位置、方向等。本文提出了一种基于CenterNet的表格识别方法,网络结构如图6所示,利用Cycle_Pairing模块和Pairing损失去学习相邻单元格的公共顶点信息,然后通过连结单元格获取一个完整的表格结构,最后使用相同的解析过程去获取行列信息。这篇文章解决户外场景图像的表格解析问题(TSP:table structure parsing),局限在于此方法仅适用于有线表格,无线表角点定义的歧义性使得本文方法不一定work。从本文的思路我们或许可以探索角点法在处理复杂场景的情况下是否比anchor-based的方法表现更优。

图6

3. 序列预测方法:

3.1 Latex标签序列预测[10]

基于图像的表格分析优势在于,它对表格类型具有鲁棒性,并不要求格式是页面扫描图像还是纯数字文档,它可用于多数文档类型,包括PDF、HTML、PowerPoint格式等。然而,非结构化数字文档中的表格数据,由于其结构和样式的复杂性及多样性,很难解析为结构化的机器可读格式。

在实践中,手工标注用于训练的数据集的成本和不灵活性是实际部署深度学习模型的关键瓶颈。本文是微软的一篇文章,利用互联网中存在大量的Word和Latex源文档,对这些在线文档应用一些弱监督来标注表格,创建TableBank数据集。对于word文档,可以修改内部的office xml代码,指定每个表格的边界线;对于latex文档,可以修改tex代码,代码已识别表格的边界框。表格检测使用基于不同配置的Faster R-CNN的架构,表格结构识别模型基于image-to-text的编码器-解码器架构。

本文的局限在于版式多样对表格分析任务的准确率具有负面影响,模型泛化能力差,某一领域的模型应用到其他领域效果不好,在TableBank数据集上的建模和学习具有很大改进空间。

3.2 HTML标签序列预测[11]

类似地,IBM公司开发并发布了数据集PubTabNet,此数据集中自动为每个表图像加上关于表的结构和每个单元格内的文本信息(HTML格式),如图7(a)所示。

作者等人提出了一种端到端的表格识别方案,是一种基于注意力的编码器-解码器(EDD)架构,它是由编码器、结构解码器和单元格解码器组成,可以将表格图像转化成HTML代码。编码器获得表格图像的视觉特征,两个独立的结构解码器⼀个输出表格结构,⼀个输出单元格内容。

图7(b)所示为EDD架构,不需要复杂的后处理即可得到表格结果。但end2end的方案在中文场景的落地还有很长的路要走,另外缺少表格物理结构的信息,EDD方法的纠错空间不多。同时,本文的另一个贡献是提出了一种新的基于树编辑距离的图像表格识别评价指标TEDS,将表格建模为树形结构,该指标比之前的基于precision、reacll、F1 score的评价指标更为规范。

图7(a)

图7(b)

3.3 TableMaster解决方案[12]

平安产险提出的TableMaster方案是ICDAR21比赛Table Recognition赛道的亚军,本方案基于文本识别算法Master[13]。TableMASTER算法采用多任务学习的模式,同时进行表格结构序列预测以及单元格位置回归,最后通过后处理匹配算法,融合表格结构序列和单元格文本内容,得到HTML代码。整个解决方案可以分为4个部分:表格结构序列识别,表格文本行检测,表格文本行识别,以及单元格与表格结构序列匹配。

表格结构序列识别部分,使用改进的文本识别算法MASTER,它与原生的MASTER在结构上的区别如图8所示。他们的特征提取器在结构上是大体一致的,采用的都是改进后的ResNet网络,TableMaster在解码阶段,经过一个TransformerLayer后,会分为两条分支。一条分支进行表格结构序列的监督学习,另一条分支进行表格中单元位置回归的监督学习。

表格文本行检测部分,采用的是经典的文本检测算法PSENet[14],表格文本行识别部分,采用的文本识别算法MASTER。

在单元格与表格结构序列匹配部分,作者团队定义了三种匹配规则,分别是Center Point Rule,IOU Rule,以及Distance Rule。这三种匹配规则的优先级顺序为Center Point Rule>IOU Rule>Distance Rule。

序列预测的表格识别结果受限于序列长度和类别数量,同时transformer结构推理速度有优化空间。

图8

4. 图卷积神经网络方法:

4.1 Rethinking Table Recognition using Graph Neural Networks[15]

近年来,越来越多的学者将深度学习技术应用到图数据领域,本文作者将表格结构识别问题描述为一个与图神经网络兼容的图问题,利用图神经网络解决这一问题。将每一个文本区域作为一个顶点,使用矩阵描述的三个图(单元格、行共享矩阵、列共享矩阵)定义为真值,若顶点共享一行,则对应的文本区域属于同一行,这些顶点视作彼此相邻。

模型分为四个流程:特征提取、信息交互、随机采样和分类,过程如图9所示。利用CNN提取视觉特征,输出端将ocr得到的顶点位置映射到特征图上,将视觉特征与位置特征结合形成聚集特征。得到所有的顶点特征后进行特征交互融合,得到每个顶点的结构特征。在分类部分使用DenseNet分别对定点对进行是否同行、同列、同单元格的结构关系分类。

图9

4.2 GFTE: Graph-based Financial Table Extraction[16]

本文[16]主要是为了主要解决金融类的、中文表格识别问题,提出一种新的基于图卷积神经网络的模型GFTE作为表格识别数据集(本文自己发布的中文数据集FinTab)中的基线方法。GFTE通过融合图像特征、位置特征和文本特征,能够对表格进行精确的边缘检测,取得较好的效果。

GFTE可以分为以下几个步骤:
1、构建基本事实,包括表格区域的图像、文本内容、文本位置和结构标签
2、基于单元格构造一个无向图G=<V, R, C>
3、使用GFTE来预测相邻关系,包括垂直和水平关系。

作者将表格中的每个单元格视作节点,每个节点包含三种类型的信息:文本内容、绝对位置和图像,节点与它的邻域理解成边,那么表格结构完全可以用一个图来表示,图结构如图10所示。

图11是GFTE的基本结构,首先将绝对位置转换成相对位置,然后使用这些位置生成图。然后将纯文本嵌入预先定义好的特征空间,使用LSTM获取语义特征。接着将位置特征与文本特征进行拼接,将他们传入两层的图卷积网络(GCN)。最后,利用节点的相对位置,计算网格,利用网格中国输入的像素位置计算输出,得到某一节点在某一点的图像特征。获得这三种不同的特征后,将生成图的一条边上的两个节点配对,并与三种不同的特征集合在一起,使用MLP预测这两个节点处于同行还是同列。

图神经网络通过合理建模,将表格中的不同特征进行融合,并取得了不错的效果,这种表格结构识别方法,为未来的工作提供了一些可能的发展道路。

图10

图11

————

结语:

目前,表格识别任务逐渐受到工业界和学术界的关注。表格识别对象从PDF文档,到文档图片,再到自然场景图片,复杂场景表格识越来越被重视。随着人工智能应用的成熟,表格识别方法已从传统图像算法转向到深度学习算法。目标检测、语义分割、序列预测、图神经网络等是目前表格识别的主要技术方向,它们各有优劣。作为文档理解的重要一环,一套在复杂场景中鲁棒性很强的表格识别方案仍有待探索。

对于表格识别及相关内容提炼,合合信息认为传统的表格识别方法设计起来较为复杂,对于版面布局分析和表格结构的提取,图像处理的方法依赖各种阈值和参数的选择,难以满足现实生活中复杂多样的表格场景,鲁棒性也相对较差。目前业界常用的表格识别方法多为深度学习算法,比较依赖算法工程师对于神经网络的精心设计,可以不用依赖阈值和参数,鲁棒性较强。

从公司角度来讲,算法落地的关键因素大致可分为计算力、数据、算法模型、业务场景等。计算力是人工智能技术的生产力,数据的价值在于给予解决某一场景问题足够的特征,算法模型是项目落地的承载体,业务场景是落地的关键。在技术落地中,应当一切以业务场景为核心,以最少的数据,最简单的模型,最少的计算力解决最实际的问题,达到最好的效果。对于表格识别这一技术来讲,一款推理速度快且在少样本情况下表现优异的表格识别算法会备受青睐。


参考文献:

[1] OpenCV-检测并提取表格:https://blog.csdn.net/yomo127/article/details/52045146

[2] pdfplumber解析表格:pdfplumber是怎么做表格抽取的(三) – 知乎

[3] camelot表格抽取:camelot是怎么做表格抽取的(三)—— 非线框类表格抽取 – 知乎

[4] T-recs:T. Kieninger and A. Dengel, “The T-Recs table recognition and analysis system,” in Document Analysis Systems: Theory and Practice, pp. 255–270, 1999.

[5] Siddiqui S A , Khan P I , Dengel A , et al. Rethinking Semantic Segmentation for Table Structure Recognition in Documents[C]// 2019 International Conference on Document Analysis and Recognition (ICDAR). 2019.

[6] 走进AI时代的文档识别技术之表格图像识别 – 云+社区 – 腾讯云 (tencent.com)

[7] LGPMA: Complicated Table Structure Recognition with Local and Global Pyramid Mask Alignment (arxiv.org)

[8] Sun N , Zhu Y , Hu X . Faster R-CNN Based Table Detection Combining Corner Locating[C]// 2019 International Conference on Document Analysis and Recognition (ICDAR). 2019.

[9] Long R , Wang W , Xue N , et al. Parsing Table Structures in the Wild. 2021.

[10] Zhong X , Shafieibavani E , Yepes A J . Image-based table recognition: data, model, and evaluation[J]. 2019.

[11] Li M , Cui L , Huang S , et al. TableBank: Table Benchmark for Image-based Table Detection and Recognition[J]. 2019.

[12] Ye J , Qi X , He Y , et al. PingAn-VCGroup’s Solution for ICDAR 2021 Competition on Scientific Literature Parsing Task B: Table Recognition to HTML[J]. 2021.

[13] Lu N , Yu W , Qi X , et al. MASTER: Multi-Aspect Non-local Network for Scene Text Recognition[J]. 2019.

[14]Li X , Wang W , Hou W , et al. Shape Robust Text Detection with Progressive Scale Expansion Network[J]. 2018.

[15] Qasim S R , Mahmood H , F Shafait. Rethinking Table Recognition using Graph Neural Networks. 2019.

[16] Li Y , Huang Z , Yan J , et al. GFTE: Graph-based Financial Table Extraction[J]. 2020.

表格识别与内容提炼技术理解及研发趋势 - 合合技术团队 - 博客园

mikel阅读(136)

来源: 表格识别与内容提炼技术理解及研发趋势 – 合合技术团队 – 博客园

引言:

表格是各类文档中常见的对象,其结构化的组织形式方便人们进行信息理解和提取。表格的种类根据有无边框可以划分有线表、少线表、无线表。表格样式复杂多样,如存在背景填充、光照阴影、单元格行列合并等情况。大数据时代存在大量电子文档,应用表格识别技术能够减少表格处理时间,因此表格识别是文档理解领域的重要研究课题,也是合合信息这几年的技术突破点方向之一。

表格识别主要包括表格检测和表格结构识别两个子任务。

表格检测主要检测表格主体,即样本中表格区域。表格结构识别是对表格区域进行分析,提取表格中的数据与结构信息,得到行列分布与逻辑结构。未经特殊说明,以下表格识别专指表格结构识别。

 

研究现状与解决方案:

近年来,国内外专家学者对表格识别进行了大量研究,取得了丰富的研究成果。下面主要从传统图像处理方法和深度学习这两个方面做具体阐述。

传统方法

传统的表格识别工作是基于一些启发式的规则和图像处理方法,主要利用表格线或者文本块之间的空白分隔区域来确定单元格区域,通过腐蚀、膨胀,找连通区域,检测线段、直线,求交点,合并猜测框等。有自顶向下的方法(先检测表格区域,再不断对表格区域进行切割拆分得到单元格区域);还有自底向上的方法(先检测文本块,找到可能的表格线以及这些线的交点,确定单元格后还原出表格区域)

1、 OpenCV-检测并提取表格[1]

这是一种自顶向下的方法,先对图像进行二值化,然后使用霍夫变换,检测其中的直线,并在直线中,找到围成的一个矩形区域,最后将这块区域提取出来。

作者主要使用了四步操作
1、处理图像,灰度化,二值化。在灰度图的基础上运用adaptiveThreshold来达成自动阈值的二值化,取代Canny,这个算法在提取直线和文字比Canny有更好的效果
2、利用OpenCV里面的形态学函数,腐蚀erode膨胀dilate
3、交叉横纵线条,对点进行定位,通过bitwise_and函数获得横纵线条的交点,通过交点对表格区域进行提取
4、判断区域是否为表格。

虽然此方法可以相对完整的识别图片中的表格,但也存在几个问题:
1、图片倾斜不易识别
2、图片背景复杂会干扰识别
3、少线表情况,表格只有上下两条线的时候如何判断

 

2、pdfplumber解析表格[2]

pdfplumber是一款完全用python开发的pdf解析库,对于全线表,pdfminer能够实现较好的抽取效果,但对于少线表和无线表,效果就差了很多。

下面介绍pdfplumber中的表格抽取流程,这是一种自底向上的方法,首先找到可见的或猜测出不可见的候选表格线;然后根据候选表格线确定它们的交点;接着根据得到的交点,找到围成它们的最小单元格;最后把连通的单元格进行整合,生成检测出的表格对象。在表格生成的过程中,利用单元格的bbox坐标(四个角的坐标)判断单元格是否属于当前表格;同时对表格的左上角坐标进行排序,过滤掉小表格。

3、camelot表格抽取[3]

camelot是一个可以从可编辑的pdf文档中抽取表格的开源框架,与pdfplumber相比,其功能完备性差了点,除了表格抽取之外,并不能用它从pdf文档中解析出字符、单词、文本、线等较为低层次的对象。

 

camelot支持两种表格抽取模式:

 

一、lattice线框类表格抽取,步骤如下:
1、pdf转图像
2、图像处理算法检测出水平方向和竖直方向可能用于构成表格的直线
3、根据检测出的直线,生成可能表格的bounding box
4、确定表格各行、列的区域
5、根据各行、列的区域,水平、竖直方向的表格线以及页面文本内容,解析出表格结构,填充单元格内容,最终形成表格对象。

二、stream少线框类表格抽取,步骤如下:
1、通过pdfminer获取连续字符串
2、通过文本对齐的方式确定可能表格的bounding box
3、确定表格各行、列的区域
4、根据各行、列的区域以及页面上的文本字符串,解析表格结构,填充单元格内容,最终形成表格对象。

4、T-recs[4]

这是一种自底向上的方法,核心思想是对文本块区域进行聚类。

步骤如下:
1、从文本块中选择种子点
2、在种子点上下各一行分别去找与该种子点文本块之间是否水平方向有重合,如果有重合则将相应文本块和种子点块归到一起,并作为新的种子点
3、重复第1、2步,不断找与之水平方向有重合的文本块,直到所有的文本块都不能再合并下去。

此方法也存在诸多局限:
1、表头是跨单元格的,表头下面的文本块会被全部合并到一起
2、有时候上下几行文本确确实实是对齐的,但是和左右文本区域比较近,这种本不能分开的区域被错误的分开了
3、孤立的文本块会被切分成单独块。

因此,本方法后面大部分工作是针对这三种局限设定后处理规则,该方法认为表格之所以是表格是由文本块的分布决定的,而与分割带无关。加入后处理规则之后该方法方法具有较好的通用性,无论是对与PDF文档还是OCR的结果,都有比较好的效果。

深度学习方法

近年来人工智能技术飞速发展,研究人员将CV,NLP和图神经等成熟方法应用在表格识别任务中,取得很多不错的成果。深度学习表格识别主流方法包括语义分割,目标检测,序列预测和图神经等,下面我们对这些工作分别进行介绍。

1. 语义分割方法:

1.1 Rethinking Semantic Segmentation for Table Structure Recognition in Documents[5]

本文将表格结构的识别定义为语义分割问题,使用FCN网络框架,对表格的行和列分别进行预测。主要介绍了一种对预测结果进行切片的方法,降低了表格识别的复杂度。使用了FCN的Encoder和Decoder的结构,并加载了在ImageNet预训练好的模型。图片经过模型生成了与原图大小相同的特征,切片过程将特征按照行和列进行平均,将H*W*C(高*宽*Channel)的特征合并成了H*C和W*C大小特征;对这些特征进行卷积后,再进行复制,扩展为H*W*C的大小,再通过卷积层得到每个像素点的标签;最后进行后处理得到最终的结果。本文pipeline如图1所示。本文方法的局限在于本文所处理的表格对象中所有的单元格不存在跨行跨列,每行每列都从表格的最左侧和最上端开始,到最右侧和最下端结束。

图1

 

1.2 腾讯表格识别技术方案[6]

图像分割是对图像的每个像素点赋予标签,在表格识别任务中,每个像素可能属于横线、竖线、不可见横线、不可见竖线这几个标签。

解决方案流程如图2所示:
1、表格线标注:横向的线,竖向的线,横向的不可见线,竖向的不可见线。类别不互斥,考虑到单元格交点问题,即交点处的像素属于多个类别
2、几何分析提取连通区域,对连通区域拟合折线,合并形成框线;考虑图片弯曲、表格倾斜的情况,利用投影变换对原图矫正
3、调用ocr,识别文本内容,确定字符坐标
4、根据第二步的框线计算行列信息,判断单元格合并情况,得到每个单元格在途中的位置
5、根据单元格坐标和字符坐标,将字符嵌入到单元格,还原表格。

此方案专注于将页面拍照后进行表格识别,对于一般的表格效果还好,但现实场景太过纷杂,仍有很多问题亟待解决。

图2

2. 物体检测方法:

2.1 海康LGPMA方案[7]

此方案是ICDAR21比赛Table Recognition赛道的冠军,LGPMA将表格识别分为文本行检测、文字识别和表格结构识别三部分。文本检测模块是一个单行文本检测器,文字识别模块是一个基于attention 的识别器,这两部分用来获取表格图像中的文本信息。表格结构识别部分采用的是一种LGPMA的方案,基于Mask-RCNN同时出两个分割头,一个LPMA学习局部对齐边界,一个GPMA学习全局对齐边界,融合了自顶向下和自底向上两种思想。

如图3所示。在得到两路的soft mask之后,将LPMA和GPMA的对齐mask融合,之后对每个单元格边框进行精修。最后经过cell matching , empty cell searching 和 empty cell merging三个后处理步骤得到最终的表格结构。原方案采用较大的基础网络,训练推理对硬件及输出尺寸有一定约束,实际落地较为困难。

图3

2.2 角点表格检测法[8]

针对表格检测本文使用了“角点”来提升表格检测的精确度,对比基本模型在检测结果上能够得到进一步的检测与提升。

首先引入角点的概念,如图4所示:角点是表格四个顶点周围的一部份区域,这些区域大小相同,同一个表格的所有角点构成一个角点组,角点的检测与表格检测一样,可以使用目标检测模型来解决,作者使用Faster R-CNN模型,同时进行角点和表格的检测,检测结构如图5所示,使用角点组对对应的表格检测的横坐标进行校准,得到最终的表格区域。该方法与未加入角点的Faster R-CNN模型相比,结果有了较大的提升。

图4

图5

基于CenterNet的端到端表格识别方案[9]

目标检测识别往往是在图像上将目标用矩形框的形式框出,目标检测器都先穷举出潜在目标位置,然后对该位置进行分类,这种做法浪费时间,低效,还需要做额外的后处理。

CenterNet是将目标作为一个点(BBOX中心点),利用中心点回归其他目标属性,比如尺寸、位置、方向等。本文提出了一种基于CenterNet的表格识别方法,网络结构如图6所示,利用Cycle_Pairing模块和Pairing损失去学习相邻单元格的公共顶点信息,然后通过连结单元格获取一个完整的表格结构,最后使用相同的解析过程去获取行列信息。这篇文章解决户外场景图像的表格解析问题(TSP:table structure parsing),局限在于此方法仅适用于有线表格,无线表角点定义的歧义性使得本文方法不一定work。从本文的思路我们或许可以探索角点法在处理复杂场景的情况下是否比anchor-based的方法表现更优。

图6

3. 序列预测方法:

3.1 Latex标签序列预测[10]

基于图像的表格分析优势在于,它对表格类型具有鲁棒性,并不要求格式是页面扫描图像还是纯数字文档,它可用于多数文档类型,包括PDF、HTML、PowerPoint格式等。然而,非结构化数字文档中的表格数据,由于其结构和样式的复杂性及多样性,很难解析为结构化的机器可读格式。

在实践中,手工标注用于训练的数据集的成本和不灵活性是实际部署深度学习模型的关键瓶颈。本文是微软的一篇文章,利用互联网中存在大量的Word和Latex源文档,对这些在线文档应用一些弱监督来标注表格,创建TableBank数据集。对于word文档,可以修改内部的office xml代码,指定每个表格的边界线;对于latex文档,可以修改tex代码,代码已识别表格的边界框。表格检测使用基于不同配置的Faster R-CNN的架构,表格结构识别模型基于image-to-text的编码器-解码器架构。

本文的局限在于版式多样对表格分析任务的准确率具有负面影响,模型泛化能力差,某一领域的模型应用到其他领域效果不好,在TableBank数据集上的建模和学习具有很大改进空间。

3.2 HTML标签序列预测[11]

类似地,IBM公司开发并发布了数据集PubTabNet,此数据集中自动为每个表图像加上关于表的结构和每个单元格内的文本信息(HTML格式),如图7(a)所示。

作者等人提出了一种端到端的表格识别方案,是一种基于注意力的编码器-解码器(EDD)架构,它是由编码器、结构解码器和单元格解码器组成,可以将表格图像转化成HTML代码。编码器获得表格图像的视觉特征,两个独立的结构解码器⼀个输出表格结构,⼀个输出单元格内容。

图7(b)所示为EDD架构,不需要复杂的后处理即可得到表格结果。但end2end的方案在中文场景的落地还有很长的路要走,另外缺少表格物理结构的信息,EDD方法的纠错空间不多。同时,本文的另一个贡献是提出了一种新的基于树编辑距离的图像表格识别评价指标TEDS,将表格建模为树形结构,该指标比之前的基于precision、reacll、F1 score的评价指标更为规范。

图7(a)

图7(b)

3.3 TableMaster解决方案[12]

平安产险提出的TableMaster方案是ICDAR21比赛Table Recognition赛道的亚军,本方案基于文本识别算法Master[13]。TableMASTER算法采用多任务学习的模式,同时进行表格结构序列预测以及单元格位置回归,最后通过后处理匹配算法,融合表格结构序列和单元格文本内容,得到HTML代码。整个解决方案可以分为4个部分:表格结构序列识别,表格文本行检测,表格文本行识别,以及单元格与表格结构序列匹配。

表格结构序列识别部分,使用改进的文本识别算法MASTER,它与原生的MASTER在结构上的区别如图8所示。他们的特征提取器在结构上是大体一致的,采用的都是改进后的ResNet网络,TableMaster在解码阶段,经过一个TransformerLayer后,会分为两条分支。一条分支进行表格结构序列的监督学习,另一条分支进行表格中单元位置回归的监督学习。

表格文本行检测部分,采用的是经典的文本检测算法PSENet[14],表格文本行识别部分,采用的文本识别算法MASTER。

在单元格与表格结构序列匹配部分,作者团队定义了三种匹配规则,分别是Center Point Rule,IOU Rule,以及Distance Rule。这三种匹配规则的优先级顺序为Center Point Rule>IOU Rule>Distance Rule。

序列预测的表格识别结果受限于序列长度和类别数量,同时transformer结构推理速度有优化空间。

图8

4. 图卷积神经网络方法:

4.1 Rethinking Table Recognition using Graph Neural Networks[15]

近年来,越来越多的学者将深度学习技术应用到图数据领域,本文作者将表格结构识别问题描述为一个与图神经网络兼容的图问题,利用图神经网络解决这一问题。将每一个文本区域作为一个顶点,使用矩阵描述的三个图(单元格、行共享矩阵、列共享矩阵)定义为真值,若顶点共享一行,则对应的文本区域属于同一行,这些顶点视作彼此相邻。

模型分为四个流程:特征提取、信息交互、随机采样和分类,过程如图9所示。利用CNN提取视觉特征,输出端将ocr得到的顶点位置映射到特征图上,将视觉特征与位置特征结合形成聚集特征。得到所有的顶点特征后进行特征交互融合,得到每个顶点的结构特征。在分类部分使用DenseNet分别对定点对进行是否同行、同列、同单元格的结构关系分类。

图9

4.2 GFTE: Graph-based Financial Table Extraction[16]

本文[16]主要是为了主要解决金融类的、中文表格识别问题,提出一种新的基于图卷积神经网络的模型GFTE作为表格识别数据集(本文自己发布的中文数据集FinTab)中的基线方法。GFTE通过融合图像特征、位置特征和文本特征,能够对表格进行精确的边缘检测,取得较好的效果。

GFTE可以分为以下几个步骤:
1、构建基本事实,包括表格区域的图像、文本内容、文本位置和结构标签
2、基于单元格构造一个无向图G=<V, R, C>
3、使用GFTE来预测相邻关系,包括垂直和水平关系。

作者将表格中的每个单元格视作节点,每个节点包含三种类型的信息:文本内容、绝对位置和图像,节点与它的邻域理解成边,那么表格结构完全可以用一个图来表示,图结构如图10所示。

图11是GFTE的基本结构,首先将绝对位置转换成相对位置,然后使用这些位置生成图。然后将纯文本嵌入预先定义好的特征空间,使用LSTM获取语义特征。接着将位置特征与文本特征进行拼接,将他们传入两层的图卷积网络(GCN)。最后,利用节点的相对位置,计算网格,利用网格中国输入的像素位置计算输出,得到某一节点在某一点的图像特征。获得这三种不同的特征后,将生成图的一条边上的两个节点配对,并与三种不同的特征集合在一起,使用MLP预测这两个节点处于同行还是同列。

图神经网络通过合理建模,将表格中的不同特征进行融合,并取得了不错的效果,这种表格结构识别方法,为未来的工作提供了一些可能的发展道路。

图10

图11

————

结语:

目前,表格识别任务逐渐受到工业界和学术界的关注。表格识别对象从PDF文档,到文档图片,再到自然场景图片,复杂场景表格识越来越被重视。随着人工智能应用的成熟,表格识别方法已从传统图像算法转向到深度学习算法。目标检测、语义分割、序列预测、图神经网络等是目前表格识别的主要技术方向,它们各有优劣。作为文档理解的重要一环,一套在复杂场景中鲁棒性很强的表格识别方案仍有待探索。

对于表格识别及相关内容提炼,合合信息认为传统的表格识别方法设计起来较为复杂,对于版面布局分析和表格结构的提取,图像处理的方法依赖各种阈值和参数的选择,难以满足现实生活中复杂多样的表格场景,鲁棒性也相对较差。目前业界常用的表格识别方法多为深度学习算法,比较依赖算法工程师对于神经网络的精心设计,可以不用依赖阈值和参数,鲁棒性较强。

从公司角度来讲,算法落地的关键因素大致可分为计算力、数据、算法模型、业务场景等。计算力是人工智能技术的生产力,数据的价值在于给予解决某一场景问题足够的特征,算法模型是项目落地的承载体,业务场景是落地的关键。在技术落地中,应当一切以业务场景为核心,以最少的数据,最简单的模型,最少的计算力解决最实际的问题,达到最好的效果。对于表格识别这一技术来讲,一款推理速度快且在少样本情况下表现优异的表格识别算法会备受青睐。


参考文献:

[1] OpenCV-检测并提取表格:https://blog.csdn.net/yomo127/article/details/52045146

[2] pdfplumber解析表格:pdfplumber是怎么做表格抽取的(三) – 知乎

[3] camelot表格抽取:camelot是怎么做表格抽取的(三)—— 非线框类表格抽取 – 知乎

[4] T-recs:T. Kieninger and A. Dengel, “The T-Recs table recognition and analysis system,” in Document Analysis Systems: Theory and Practice, pp. 255–270, 1999.

[5] Siddiqui S A , Khan P I , Dengel A , et al. Rethinking Semantic Segmentation for Table Structure Recognition in Documents[C]// 2019 International Conference on Document Analysis and Recognition (ICDAR). 2019.

[6] 走进AI时代的文档识别技术之表格图像识别 – 云+社区 – 腾讯云 (tencent.com)

[7] LGPMA: Complicated Table Structure Recognition with Local and Global Pyramid Mask Alignment (arxiv.org)

[8] Sun N , Zhu Y , Hu X . Faster R-CNN Based Table Detection Combining Corner Locating[C]// 2019 International Conference on Document Analysis and Recognition (ICDAR). 2019.

[9] Long R , Wang W , Xue N , et al. Parsing Table Structures in the Wild. 2021.

[10] Zhong X , Shafieibavani E , Yepes A J . Image-based table recognition: data, model, and evaluation[J]. 2019.

[11] Li M , Cui L , Huang S , et al. TableBank: Table Benchmark for Image-based Table Detection and Recognition[J]. 2019.

[12] Ye J , Qi X , He Y , et al. PingAn-VCGroup’s Solution for ICDAR 2021 Competition on Scientific Literature Parsing Task B: Table Recognition to HTML[J]. 2021.

[13] Lu N , Yu W , Qi X , et al. MASTER: Multi-Aspect Non-local Network for Scene Text Recognition[J]. 2019.

[14]Li X , Wang W , Hou W , et al. Shape Robust Text Detection with Progressive Scale Expansion Network[J]. 2018.

[15] Qasim S R , Mahmood H , F Shafait. Rethinking Table Recognition using Graph Neural Networks. 2019.

[16] Li Y , Huang Z , Yan J , et al. GFTE: Graph-based Financial Table Extraction[J]. 2020.
————————————————