オヒサシブリデース。
ついにはてなブログに移行しました。インポート楽ちんでよいですね。
ここのところあんまり新しいことしてなかったので書くことなかったんですが、
久々に触ったことなかったライブラリに触ったので覚書までに。
HTMLをパースする案件がありまして、以下のパーサを触ったんですが、
割と精度も使い勝手もパフォーマンスもよかったjsoupを紹介してみます。
- jsoup
- jericho
- HtmlCleaner
- Validator.nu
- HTMLEditorKit
- TagSoup
- HTML Parser
- NekoHtml
- JTidy
実装してみる
まずはHTMLをパースして、タグ名、属性名、属性値、テキストを表示するものを実装してみます。
public class JsoupTest {
private static final String PREFIX = "<html><head></head><body>";
private static final String SUFFIX = "</body></html>";
private static final String BACKQUOTE = "`";
private static final String DOUBLEQUOTE = "\"";
public static void main(String[] args) {
String target = "<a href=\"#\">hoge</a>";
String replacedText = StringUtils.replace(target, BACKQUOTE, DOUBLEQUOTE);
replacedText = PREFIX + replacedText + SUFFIX;
Document doc = Jsoup.parse(replacedText);
Elements elements = doc.getAllElements();
for(Element element : elements) {
System.out.println("tagname:" + element.tagName() + ", text:" + element.ownText());
for (Attribute attr : element.attributes()) {
System.out.println("key:" + attr.getKey() + ", value:" + attr.getValue() + "\n-");
}
System.out.println("-----");
}
}
}
実行結果は以下の通り。
tagname:#root, text:
-----
tagname:html, text:
-----
tagname:head, text:
-----
tagname:body, text:
-----
tagname:a, text:hoge
key:href, value:#
-
-----
らくちんですね!
ちなみにhtmlタグとかheadタグとかをつけてるのは、
仮にこのあたりのタグがなかったとき、jsoupが勝手にそのへんのタグを補完してくれるからです。
基本的にはこの保管に任せておいて問題ないのですが、
たとえば以下のような文字列を渡した場合。
String target = "<a href=\"#\"aaa";
以下のような結果になってしまします。
<html>
<head></head>
<body></body>
</html>
_人人 人人_
> 突然の死 <
 ̄Y^Y^Y^Y ̄
というわけで自前でつけてます。
htmlタグとかheadタグとかが入力内容に入ってくるのであれば別の方法を考えないといけないかと思いますが、今回は入ってこない想定です。
まあこれはこれで以下みたいな変なHTMLが生成されたりはするんですが。
<html>
<head></head>
<body>
<a href="#" aaa<="" ody=""></a>
</body>
</html>
なくなるよりはいいかなという感じで今後の修正に期待します。
特定のタグを抽出する
jsoupには特定の条件に合致するタグを取得するメソッドが用意されています。
コードを以下のように書き換えてみます。
public class JsoupTest {
private static final String PREFIX = "<html><head></head><body>";
private static final String SUFFIX = "</body></html>";
private static final String BACKQUOTE = "`";
private static final String DOUBLEQUOTE = "\"";
public static void main(String[] args) {
String target = "<a href=\"#\" onmouseover=\"alert(0)\">aa</a><img onerror=\"alert(1)\">";
String replacedText = StringUtils.replace(target, BACKQUOTE, DOUBLEQUOTE);
replacedText = PREFIX + replacedText + SUFFIX;
Document doc = Jsoup.parse(replacedText);
System.out.println("a:" + doc.getElementsByTag("a").size());
System.out.println("href:" + doc.getElementsByAttribute("href").size());
System.out.println("on:" + doc.getElementsByAttributeStarting("on").size());
}
}
それぞれのメソッドが何をしてくれるかはコメントの通りです。
この実行結果は以下のようになります。
a:1
href:1
on:2
べんり!
特定のタグ抽出メソッドを実装する
上で便利な抽出メソッドを紹介しましたが、もともと用意されていない抽出方法を実装したいこともあるかと思います。
たとえば、属性名と属性値を指定して、指定した属性名の値に指定した値が含まれているものを抽出するメソッドははありますが、属性名を限定せずに指定した文字列が属性値に含まれるものを抽出するメソッドはありません。
というわけで実装してみます。
public class CustomEvaluator {
指定した文字列が属性値に含まれているものを抽出
public static final class ValueContainsEvalutor extends Evaluator {
private String value;
public ValueContainsEvalutor (String value) {
Validate.notEmpty(value);
this.value = value.trim().toLowerCase();
}
@Override
public boolean matches(Element root, Element element) {
for (org.jsoup.nodes.Attribute attribute : element.attributes().asList()) {
if (attribute.getValue().toLowerCase().contains(value))
return true;
}
return false;
}
}
}
次に、こいつを呼び出してみます。
System.out.println("alert:"
+ Collector.collect(new CustomEvaluator.ValueContainsEvalutor("alert"), doc).size());
実行結果は以下のようになります。
alert:2
できた!
おわり
以上、jsoupのご紹介でした。
お手軽でよいですね!
一番ブラウザに近いパースをしてくれたのはValidator.nuだったんですが、
パフォーマンスがjsoupの方が優れてる感じだったので見送りました。
こちらの紹介は気が向いたら。向かなさそうですが。