大于100万条数据时,默认不启用数字型主键降序
智能大石头 authored at 2025-07-14 17:09:34
10.97 KiB
NewLife.Cube
using System.Text.RegularExpressions;

namespace NewLife.Cube.Web;

/// <summary>
/// 浏览器特性分析器
/// </summary>
public class UserAgentParser
{
    #region 属性
    /// <summary>原始字符串</summary>
    public String UserAgent { get; set; }

    /// <summary>
    /// 兼容性,一般是 Mozilla/5.0
    /// </summary>
    public String Compatible { get; set; }

    /// <summary>平台</summary>
    public String Platform { get; set; }

    /// <summary>加密特性</summary>
    public String Encryption { get; set; }

    /// <summary>系统或处理器</summary>
    public String OSorCPU { get; set; }

    /// <summary>设备</summary>
    public String Device { get; set; }

    /// <summary>设备编译版本</summary>
    public String DeviceBuild { get; set; }

    /// <summary>发行版本</summary>
    public String Version { get; set; }

    /// <summary>用户浏览器</summary>
    public String Brower { get; set; }

    /// <summary>移动版本</summary>
    public String Mobile { get; set; }

    /// <summary>网络类型</summary>
    public String NetType { get; set; }
    #endregion

    #region 方法
    private static readonly Regex _regex = new(@"([^\s\(\)]+)\s*(\([^\(\)]+\))?");
    /// <summary>
    /// 分析浏览器UserAgent字符串
    /// </summary>
    /// <param name="userAgent"></param>
    /// <returns></returns>
    public Boolean Parse(String userAgent)
    {
        if (userAgent.IsNullOrEmpty()) return false;

        UserAgent = userAgent;

        var ms = _regex.Matches(userAgent);

        var count = ms.Count;
        if (count == 0) return false;

        var infos = ms.Select(e => e.Value?.Trim()).ToArray();
        var exts = ms[0].Groups[2].Value?.Trim('(', ')').Split(';');

        // 首先识别主流浏览器,不同浏览器格式不同
        ParseFirefox(infos, exts);
        ParseOpera(infos, exts);
        ParseHuawei(infos, exts);
        ParseTencent(infos, exts);
        ParseAliApp(infos, exts);

        // 其它浏览器
        if (Brower.IsNullOrEmpty()) ParseOtherBrowser(infos);
        if (Brower.IsNullOrEmpty()) ParseChrome(infos);
        if (Brower.IsNullOrEmpty()) ParseSafari(infos, exts);

        // 移动
        var inf = infos.FirstOrDefault(e => e.StartsWithIgnoreCase("Mobile/") || e.EqualIgnoreCase("Mobile"));
        if (inf != null) Mobile = inf.Trim();

        // 网络类型
        inf = infos.FirstOrDefault(e => e.StartsWithIgnoreCase("NetType/"));
        if (inf != null) NetType = inf.Trim()["NetType/".Length..];

        {
            // 识别操作系统平台
            Compatible = ms[0].Groups[1].Value;

            // Mozilla/MozillaVersion (Platform; Encryption; OS-or-CPU; Language; PrereleaseVersi
            var ss = exts;
            if (ss != null && ss.Length > 0 && Platform.IsNullOrEmpty())
            {
                if (ss.Length >= 5)
                {
                    Platform = ss[0]?.Trim();
                    Encryption = ss[1]?.Trim();
                    OSorCPU = ss[2]?.Trim().TrimStart("CPU ");
                    //Device = ss[3]?.Trim();

                    // 有可能是设备和版本
                    var str = ss[4]?.Trim();
                    if (!str.IsNullOrEmpty() && str.Contains("Build/"))
                        Device = str;
                    else
                        Version = str;
                }
                else if (ss.Length >= 4)
                {
                    Platform = ss[0]?.Trim();
                    if (Platform.EqualIgnoreCase("compatible"))
                    {
                        Brower = ss[1]?.Trim();
                        OSorCPU = ss[2]?.Trim();
                    }
                    else
                    {
                        Encryption = ss[1]?.Trim();
                        OSorCPU = ss[2]?.Trim().TrimStart("CPU ");

                        //// WebKit 特殊
                        //if (!infos.Any(e => e.Contains("WebKit"))) Device = ss[3]?.Trim();
                        Device = ss[3]?.Trim();
                        if (Device == "en" || Device.StartsWithIgnoreCase("en")) Device = null;
                    }
                }
                else if (ss.Length >= 3 && ss[0].EqualIgnoreCase("compatible"))
                {
                    Platform = ss[0]?.Trim();
                    Brower = ss[1]?.Trim();
                    OSorCPU = ss[2]?.Trim();
                }
                else if (ss.Length >= 2)
                {
                    Platform = ss[0]?.Trim();
                    OSorCPU = ss[1]?.Trim().TrimStart("CPU ");
                }
                else if (ss.Length >= 1)
                {
                    Platform = ss[0]?.Trim();
                }
            }

            // 处理操作系统与平台
            if (Platform.StartsWithIgnoreCase("Windows "))
            {
                OSorCPU = Platform;
                Platform = "Windows";
            }
            else if (Platform.EqualIgnoreCase("Linux") && OSorCPU.StartsWithIgnoreCase("Android ", "HarmonyOS"))
            {
                Platform = "Android";
            }
            else if (Platform.EqualIgnoreCase("X11") && Encryption.StartsWithIgnoreCase("Ubuntu"))
            {
                Platform = Encryption;
                Encryption = null;
            }

            // 处理系统和处理器
            if (!OSorCPU.IsNullOrEmpty())
            {
                var p = OSorCPU.IndexOf("like");
                if (p >= 0) OSorCPU = OSorCPU[..p].Trim();
            }

            // 处理设备
            if (!Device.IsNullOrEmpty())
            {
                var p = Device.IndexOf("Build/");
                if (p >= 0)
                {
                    DeviceBuild = Device[(p + "Build/".Length)..].Trim();
                    Device = Device[..p].Trim();
                }
            }
        }

        // 浏览器兜底
        if (Brower.IsNullOrEmpty()) Brower = Compatible;

        return true;
    }

    private void ParseChrome(String[] infos)
    {
        var inf = infos.FirstOrDefault(e => e.StartsWith("Chrome/"));
        if (inf == null) return;

        Brower = inf;
    }

    private void ParseFirefox(String[] infos, String[] exts)
    {
        var inf = infos.FirstOrDefault(e => e.StartsWith("Firefox/"));
        if (inf == null) return;

        Brower = inf;

        if (exts.Length >= 5)
        {
            Platform = exts[0]?.Trim();
            Encryption = exts[1]?.Trim();
            OSorCPU = exts[2]?.Trim();
            Version = exts[4]?.Trim();
        }
        else if (exts.Length >= 4)
        {
            Platform = exts[0]?.Trim();
            Encryption = exts[1]?.Trim();
            OSorCPU = exts[2]?.Trim();
            Version = exts[3]?.Trim();
        }
        else if (exts.Length >= 3)
        {
            Platform = exts[0]?.Trim();
            //Encryption = exts[1]?.Trim();
            Version = exts[2]?.Trim();
        }
    }

    private void ParseOpera(String[] infos, String[] exts)
    {
        var inf = infos.FirstOrDefault(e => e.StartsWith("Opera/"));
        if (inf == null) return;

        Brower = inf;

        var p = inf.IndexOf(' ');
        if (p > 0)
        {
            Brower = inf[..p];

            if (exts.Length >= 3)
            {
                Platform = exts[0]?.Trim();
                Encryption = exts[1]?.Trim();
                OSorCPU = exts[2]?.Trim();
            }
        }
    }

    private void ParseHuawei(String[] infos, String[] exts)
    {
        var inf = infos.FirstOrDefault(e => e.StartsWith("HuaweiBrowser/"));
        if (inf == null) return;

        Brower = inf;

        if (exts.Length >= 5)
        {
            Platform = exts[0]?.Trim();
            //Encryption = exts[1]?.Trim();
            OSorCPU = exts[2]?.Trim();
            Device = exts[3]?.Trim();
            Version = exts[4]?.Trim();
        }
    }

    private void ParseTencent(String[] infos, String[] exts)
    {
        var inf = infos.FirstOrDefault(e => e.StartsWith("wxwork/"));
        inf ??= infos.FirstOrDefault(e => e.StartsWith("QQ/"));
        inf ??= infos.FirstOrDefault(e => e.StartsWith("MicroMessenger/"));
        if (inf == null) return;

        var p1 = inf.IndexOf('(');
        if (p1 > 0) inf = inf[..p1];

        Brower = inf;

        if (exts.Length >= 4)
        {
            Platform = exts[0]?.Trim();
            OSorCPU = exts[1]?.Trim();
            Device = exts[2]?.Trim();
            Version = exts[3]?.Trim();
        }
    }

    private void ParseAliApp(String[] infos, String[] exts)
    {
        var inf = infos.FirstOrDefault(e => e.StartsWith("AliApp("));
        if (inf == null) return;

        var p1 = inf.IndexOf('(');
        if (p1 < 0) return;

        var p2 = inf.IndexOf(')', p1 + 1);
        if (p2 < 0) return;

        Brower = inf.Substring(p1 + 1, p2 - p1 - 1);
    }

    private void ParseSafari(String[] infos, String[] exts)
    {
        var inf = infos.FirstOrDefault(e => e.StartsWith("Safari/"));
        if (inf == null) return;

        var p1 = inf.IndexOf('(');
        if (p1 > 0)
        {
            var str = inf[p1..];
            inf = inf[..p1];

            // 识别扩展
            var ss = str.Trim('(', ')').Split(';');
            if (ss.Length >= 2) Device = ss[1].Trim();
        }
        Brower = inf.Trim();

        // 合并版本
        inf = infos.FirstOrDefault(e => e.StartsWithIgnoreCase("Version/"));
        if (inf != null)
            Brower = Brower.Split('/')[0] + "/" + inf.Split('/')[^1];
    }

    private void ParseOtherBrowser(String[] infos)
    {
        var list = infos.Where(e => !e.Contains("(") && !e.StartsWithIgnoreCase("Mozilla/", "AppleWebKit/", "Chrome/", "Safari/", "Gecko/", "Mobile/", "Version/") && !e.EqualIgnoreCase("Mobile")).ToList();
        if (list.Count == 0) return;

        Brower = list[0];
    }
    #endregion

    #region 扩展判断
    private static String[] _robots = new[] { "bot", "crawler", "spider", "okhttp", "python" };
    /// <summary>是否蜘蛛机器人</summary>
    public Boolean IsRobot
    {
        get
        {
            //if (UserAgent.StartsWithIgnoreCase("okhttp")) return true;

            var str = Brower;
            if (!str.IsNullOrEmpty())
            {
                var p = str.IndexOf('/');
                if (p > 0) str = str[..p];

                if (str.EndsWithIgnoreCase(_robots)) return true;
            }

            str = OSorCPU;
            if (!str.IsNullOrEmpty())
            {
                var p = str.LastIndexOf('/');
                if (p > 0) str = str[(p + 1)..];

                if (str.EndsWithIgnoreCase(_robots)) return true;
            }

            str = Device;
            if (!str.IsNullOrEmpty())
            {
                var p = str.LastIndexOf('/');
                if (p > 0) str = str[(p + 1)..];

                if (str.EndsWithIgnoreCase(_robots)) return true;
            }

            return false;
        }
    }

    /// <summary>是否移动端</summary>
    public Boolean IsMobile => !Mobile.IsNullOrEmpty();
    #endregion
}