#user-agent #string #extractor #extract #regex #yaml #user-agent-parser

ua-parser

Rust实现的用户代理字符串解析项目

2个不稳定版本

0.2.0 2024年6月22日
0.1.0 2024年6月17日

#120 in HTTP客户端

Apache-2.0

1.5MB
2K SLoC

Rust 1.5K SLoC // 0.1% comments JavaScript 393 SLoC // 0.0% comments C++ 91 SLoC Perl 29 SLoC // 0.2% comments

用户代理解析器

此模块实现了browserscope / uap标准,允许从用户代理中提取各种元数据。

browserscope标准是面向数据的,其中regexes.yaml指定了从用户代理字符串中进行匹配和提取。此库实现了匹配协议并提供各种类型以简化数据集的加载,但它不提供数据本身,以避免对序列化库的依赖或加载限制。

数据集加载

该crate不提供任何预编译的数据文件或专用加载器,但是Regexes实现了serde::Deserialize并可以加载一个regexes.yaml文件或任何格式保留的转换(例如,如果应用程序已经依赖于其中之一,则可能更喜欢从json或cbor加载)

# let ua_str = "";
let f = std::fs::File::open("regexes.yaml")?;
let regexes: ua_parser::Regexes = serde_yaml::from_reader(f)?;
let extractor = ua_parser::Extractor::try_from(regexes)?;

# Ok::<(), Box<dyn std::error::Error>>(())

所有数据描述结构也都是纯旧数据,因此可以直接嵌入到应用程序中,例如通过构建脚本

let parsers = vec![
    ua_parser::user_agent::Parser {
        regex: "foo".into(),
        family_replacement: Some("bar".into()),
        ..Default::default()
    }
];

提取

该crate提供提取单个信息集(用户代理 — 浏览器、操作系统和设备)或在一次调用中提取所有三个的能力。

这三个信息集是独立且不重叠的,因此如果只需要其中一个,完整的提取可能是方便的,但也是不必要的开销,提取器本身也相对昂贵,并且占用内存。

完整提取器

完整的提取器简单地将Regexes结构体转换为。生成的Extractor将三个模块级别的提取器作为属性嵌入,并将Extractor::extract转换为一个包含三个ValueRef的元组。

单独的提取器

单独的提取器位于user_agentosdevice模块中,这三个模块遵循完全相同的模型

  • 一个Parser结构体,用于指定单个解析器配置,作为Builder的输入
  • 一个Builder,可以将相关解析器push到其中
  • Builder创建的Extractor,用户可以从其中extract一个ValueRef
  • 数据提取的结果ValueRef,可能从(因此与其相关)Parser替换数据以及从中提取的用户代理字符串借用
  • 为了方便,ValueRef的拥有者Value变体
use ua_parser::os::{Builder, Parser, ValueRef};

let e = Builder::new()
    .push(Parser {
        regex: r"(Android)[ \-/](\d+)(?:\.(\d+)|)(?:[.\-]([a-z0-9]+)|)".into(),
        ..Default::default()
    })?
    .push(Parser {
        regex: r"(Android) Donut".into(),
        os_v1_replacement: Some("1".into()),
        os_v2_replacement: Some("2".into()),
        ..Default::default()
    })?
    .push(Parser {
        regex: r"(Android) Eclair".into(),
        os_v1_replacement: Some("2".into()),
        os_v2_replacement: Some("1".into()),
        ..Default::default()
    })?
    .push(Parser {
        regex: r"(Android) Froyo".into(),
        os_v1_replacement: Some("2".into()),
        os_v2_replacement: Some("2".into()),
        ..Default::default()
    })?
    .push(Parser {
        regex: r"(Android) Gingerbread".into(),
        os_v1_replacement: Some("2".into()),
        os_v2_replacement: Some("3".into()),
        ..Default::default()
    })?
    .push(Parser {
        regex: r"(Android) Honeycomb".into(),
        os_v1_replacement: Some("3".into()),
       ..Default::default()
    })?
    .push(Parser {
        regex: r"(Android) (\d+);".into(),
        ..Default::default()
    })?
    .build()?;

assert_eq!(
    e.extract("Android Donut"),
    Some(ValueRef {
        os: "Android".into(),
        major: Some("1".into()),
        minor: Some("2".into()),
        ..Default::default()
    }),
);
assert_eq!(
    e.extract("Android 15"),
    Some(ValueRef { os: "Android".into(), major: Some("15".into()), ..Default::default()}),
);
assert_eq!(
    e.extract("ZuneWP7"),
    None,
);
# Ok::<(), Box<dyn std::error::Error>>(())

性能

该包尚未进行性能分析或优化,但与uap-cpp(在M1 Pro MBP上测试)相当有竞争力

> ./UaParserBench ../uap-core/regexes.yaml benchmarks/useragents.txt 10
   25.13s user 0.07s system 99% cpu 25.279 total
> ./UaParserBench ../uap-core/regexes.yaml ../uap-python/samples/useragents.txt 100
  246.49s user 0.47s system 99% cpu 4:07.55 total
> target/release/examples/bench -r 10 ../uap-core/regexes.yaml ../uap-cpp/benchmarks/useragents.txt
   10.10s user 0.04s system 99% cpu 10.169 total

> target/release/examples/bench -r 100 ../uap-core/regexes.yaml ../uap-python/samples/useragents.txt
   98.46s user 0.04s system 99% cpu 1:38.73 total

依赖项

约4–6MB
约107K SLoC