使用 hyper 和 html5ever 解析流中的 HTML 页面内容

Parsing HTML page content in a stream with hyper and html5ever

我正在尝试解析 HTTP 请求的 HTML 响应。我正在使用 hyper for the requests and 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 文档 reader ...如果这甚至可能的话。

The documentation for parse_document 表示如果输入以字节为单位(它是)使用 from_utf8from_bytes

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

我查看了 this example of html5ever,它似乎在做我想做的事并走 DOM,但我无法将这个例子本身带到 运行——要么是已过时或 tendril/html5ever 太新。这似乎也将 HTML 作为一个整体而不是一个流来解析,但我不确定。

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

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

除非您 100% 确定您的内容是 UTF-8,否则请使用 from_bytes 而不是 from_utf8。他们 return 实现了 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();

尝试添加这个:

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();

根据板条箱文档的参数...

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

首先,我们想要一个 String 和 HTML 正文:

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,其中包含“回调”并允许您保持所需的任何状态。对于这个例子,我将检测 <a> 标签并将它们打印回 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());