【问题标题】:Parsing HTML page content in a stream with hyper and html5ever使用 hyper 和 html5ever 在流中解析 HTML 页面内容
【发布时间】:2016-06-09 20:01:25
【问题描述】:

我正在尝试解析 HTTP 请求的 HTML 响应。我使用hyper 进行请求,使用html5ever 进行解析。 HTML 将非常大,我不需要完全解析它——我只需要从标签中识别一些数据,所以我更喜欢流式传输它。从概念上讲,我想做类似的事情:

# bash
curl url | read_dom

/* javascript */
http.get(url).pipe(parser);
parser.on("tag", /* check tag name, attributes, and act */)

到目前为止,我想出的是:

extern crate hyper;
extern crate html5ever;

use std::default::Default
use hyper::Client;
use html5ever::parse_document;
use html5ever::rcdom::{RcDom};

fn main() {
    let client = Client::new();

    let res = client.post(WEBPAGE)
        .header(ContentType::form_url_encoded())
        .body(BODY)
        .send()
        .unwrap();

    res.read_to_end(parse_document(RcDom::default(),
      Default::default().from_utf8().unwrap()));
}

似乎read_to_end 是我想在响应中调用以读取字节的方法,但我不清楚如何将其通过管道传输到 HTML 文档阅读器……如果这可能的话。

The documentation for parse_document 表示如果输入以字节为单位(确实如此),则使用 from_utf8from_bytes

似乎我需要从响应中创建一个接收器,但这就是我卡住的地方。我也不清楚如何创建事件来监听我感兴趣的标签开始。

我查看了this example of html5ever,它似乎在执行我想要的操作并遍历 DOM,但我无法让这个示例本身运行——要么它已经过时,要么卷须/html5ever 太新。这似乎也将 HTML 解析为一个整体而不是一个流,但我不确定。

是否可以对这些库的当前实现做我想做的事情?

【问题讨论】:

    标签: rust hyper html5ever


    【解决方案1】:

    很抱歉缺少 html5ever 和卷须的类似教程的文档……

    除非您 100% 确定您的内容是 UTF-8,否则请使用 from_bytes 而不是 from_utf8。它们返回实现 TendrilSink 的内容,允许您以增量方式(或不增量方式)提供输入。

    std::io::Read::read_to_end 方法采用&mut Vec<u8>,因此它不适用于TendrilSink

    在最低级别,您可以对每个&[u8] 块调用一次TendrilSink::process 方法,然后调用TendrilSink::finish

    为避免手动执行此操作,还有采用 &mut R where R: std::io::ReadTendrilSink::read_from 方法。由于hyper::client::Response 实现了Read,您可以使用:

    parse_document(RcDom::default(), Default::default()).from_bytes().read_from(&mut res)
    

    为了超越您的问题,RcDom 非常小,主要是为了测试 html5ever 而存在。我建议改用Kuchiki。它具有更多功能(用于树遍历、CSS 选择器匹配……),包括可选的 Hyper 支持。

    在你的Cargo.toml:

    [dependencies]
    kuchiki = {version = "0.3.1", features = ["hyper"]}
    

    在您的代码中:

    let document = kuchiki::parse_html().from_http(res).unwrap();
    

    【讨论】:

    • 你能否给我链接更多关于 Kuchiki 的信息,比如如何实现树遍历,特别是如何使用“打开标签”事件等检查标签/文本内容?这是我需要做的。
    • 看起来documentation 有问题,还有更多方法没有显示在那里。例如,节点有像 .descendants().inclusive_descendants() 这样返回节点迭代器的方法。我不确定您所说的“打开标签”是什么意思。 Kuchiki 不是基于事件的,一旦解析完成,你就会得到一个树形数据结构。
    • 谢谢。如果它必须一次解析整个文档,那就太糟糕了。我想要htmlparser2 for node 之类的东西,我可以通过管道将html 流传输到解析器并响应onstarttag 等。
    • 为了与旧版 Web 内容兼容,符合标准的 HTML 解析器需要进行各种复杂的树操作,例如 "adoption agency algorithm"。做到这一点并拥有基于事件的 API 的唯一方法是发送至 buffer the entire document,这与问题无关。我想您可以牺牲标准合规性,但您可能会与其他解析器(例如 Web 浏览器中的解析器)不兼容(以不同方式解析页面)。
    • 例如代码你可以在github上搜索extern crate kuchiki
    【解决方案2】:

    尝试添加这个:

    let mut result: Vec<u8> = Vec::new();
    
    res.read_to_end(&mut result);
    
    let parse_result = parse_document(RcDom::default(), Default::default())
        . //read parameters
        .unwrap();
    

    参数与 crate 文档一致...

    【讨论】:

    • 你在哪里实际使用 result 和解析器?
    • 我猜像 .read_from(&amp;mut result.lock()) 这样的参数,如您链接的文档中所述...
    【解决方案3】:

    除非我误解了某些东西,否则处理 HTML 标记是相当复杂的(不幸的是,原子常量的名称远非完美)。此代码演示了如何使用html5ever 版本0.25.1 来处理令牌。

    首先,我们需要一个带有 HTML 正文的 String

    let body = {
        let mut body = String::new();
        let client = Client::new();
    
        client.post(WEBPAGE)
            .header(ContentType::form_url_encoded())
            .body(BODY)
            .send()?
            .read_to_string(&mut body);
    
        body
    };
    

    其次,我们需要定义我们自己的Sink,其中包含“回调”并让您保持所需的任何状态。对于这个例子,我将检测&lt;a&gt;标签并将它们打印回HTML(这需要我们检测开始标签、结束标签、文本,并找到一个属性;希望是一个足够完整的例子):

    use html5ever::tendril::StrTendril;
    use html5ever::tokenizer::{
        BufferQueue, Tag, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer,
    };
    use html5ever::{ATOM_LOCALNAME__61 as TAG_A, ATOM_LOCALNAME__68_72_65_66 as ATTR_HREF};
    
    // Define your own `TokenSink`. This is how you keep state and your "callbacks" run.
    struct Sink {
        text: Option<String>,
    }
    
    impl TokenSink for Sink {
        type Handle = ();
    
        fn process_token(&mut self, token: Token, _line_number: u64) -> TokenSinkResult<()> {
            match token {
                Token::TagToken(Tag {
                    kind: TagKind::StartTag,
                    name,
                    self_closing: _,
                    attrs,
                }) => match name {
                    // Check tag name, attributes, and act.
                    TAG_A => {
                        let url = attrs
                            .into_iter()
                            .find(|a| a.name.local == ATTR_HREF)
                            .map(|a| a.value.to_string())
                            .unwrap_or_else(|| "".to_string());
    
                        print!("<a href=\"{}\">", url);
                        self.text = Some(String::new());
                    }
                    _ => {}
                },
                Token::TagToken(Tag {
                    kind: TagKind::EndTag,
                    name,
                    self_closing: _,
                    attrs: _,
                }) => match name {
                    TAG_A => {
                        println!(
                            "{}</a>",
                            self.text.take().unwrap()
                        );
                    }
                    _ => {}
                },
                Token::CharacterTokens(string) => {
                    if let Some(text) = self.text.as_mut() {
                        text.push_str(&string);
                    }
                }
                _ => {}
            }
            TokenSinkResult::Continue
        }
    }
    
    
    let sink = {
        let sink = Sink {
            text: None,
        };
    
        // Now, feed the HTML `body` string to the tokenizer.
        // This requires a bit of setup (buffer queue, tendrils, etc.).
        let mut input = BufferQueue::new();
        input.push_back(StrTendril::from_slice(&body).try_reinterpret().unwrap());
        let mut tok = Tokenizer::new(sink, Default::default());
        let _ = tok.feed(&mut input);
        tok.end();
        tok.sink
    };
    
    // `sink` is your `Sink` after all processing was done.
    assert!(sink.text.is_none());
    

    【讨论】:

      猜你喜欢
      • 2012-06-27
      • 1970-01-01
      • 2021-07-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多