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

15

我正在尝试解析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文档阅读器...如果这是可能的话。

parse_document的文档说,如果输入是字节(它就是),则使用from_utf8from_bytes

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

我看了一下html5ever的这个示例,它似乎做了我想要的并遍历了DOM,但我无法运行这个示例本身--要么它已经过时了,要么tendril/html5ever太新了。这似乎也将整个HTML解析为一个流,而不是作为一个流,但我不确定。

“在当前这些库的实现下,我想做的事情是否可能实现?”
3个回答

8

很抱歉html5ever和tendril缺乏类似教程的文档...

除非您百分之百确定您的内容是UTF-8,否则请使用from_bytes而不是from_utf8。它们返回一个实现了TendrilSink的东西,允许您逐步提供输入(或不提供)。

std::io::Read::read_to_end方法采用&mut Vec<u8>,因此无法与TendrilSink一起使用。

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

为了避免手动执行该操作,还有TendrilSink::read_from方法,它需要&mut R where R: std::io::Read。由于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的信息,比如如何实现树遍历,特别是如何使用“打开标签”事件来检查标签/文本内容?这正是我需要做的。 - Explosion Pills
看起来文档有问题,还有一些没有显示的方法。例如,节点有.descendants().inclusive_descendants()等方法,返回节点的迭代器。我不确定你所说的“开放标签”是什么意思。 Kuchiki不是基于事件的,一旦解析完成,就会得到树形数据结构。 - Simon Sapin
谢谢。如果必须一次解析整个文档,那就太糟糕了。我想要像 htmlparser2 for node 这样的东西,可以将 HTML 流传输到解析器并响应 onstarttag 等事件。 - Explosion Pills
1
为了与传统的Web内容兼容,符合规范的HTML解析器需要执行各种复杂的树操作,例如"收养机构算法"。唯一能够实现这一点并具有基于事件的API的方法是缓冲整个文档,但这样做就失去了意义。我想你可以牺牲标准的合规性,但你会冒着与其他解析器(如Web浏览器中的解析器)不兼容(解析页面不同)的风险。 - Simon Sapin
您可以在 Github 上搜索 extern crate kuchiki 获取示例代码。 - user1115652

1

除非我理解有误,否则处理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,其中包含“回调函数”,并允许您保持所需的任何状态。对于此示例,我将检测 <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());

-3

尝试添加这个:

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

根据创建文档,返回翻译后的文本:

参数按照创建文档...


你实际上在解析器中哪里使用了 result - Explosion Pills
我猜在你提供的文档中描述的参数中,类似于.read_from(&mut result.lock())这样的写法吧。 - Ivan Temchenko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接