構文エラーを起こすHTMLをjsoupで綺麗にする

はい。
半年以上ぶりです。

信頼できないユーザーの入力タグをどうすれば楽にきれいに出来るか考えてみたりしてました。
ある程度まともな動きをしてくれそうなものがあったのでメモを兼ねて。

以前にも書いたjsoupでやってみます。

jsoupでHTMLをパースする - するめとめがね

コードは以下のような感じ。

String str = "<div><a href=\"#1\">aaa<a href=\"#2\">bbb</div></div></div>";
Document doc = Jsoup.parseBodyFragment(str);
doc.outputSettings().prettyPrint(false);
System.out.println(doc.body().html());

prettyPrintってやつはデフォルトtrueなんですが、これがtrueだとhtmlメソッドを使ってStringにしたとき、インデントやら改行やら素敵な感じでやってくれちゃうんですが、今回全く求めてないので問答無用でfalseにします。

結果

<div>
 <a href="#1">aaa</a>
 <a href="#2">bbb</a>
</div>

※prettyPrintがfalseになってるので実際は整形されないんですが、わかりづらいのでtrueの状態を書いてます。

うむ。
割といい感じにきれいになったんじゃないでしょうか。

次に以下で試してみます。

<div><a href="#1">aaa<div><a href="#2">bbb</div>

結果

<div>
 <a href="#1">aaa</a>
 <div>
  <a href="#1"></a>
  <a href="#2">bbb</a>
 </div>
</div>

お、おう。
ちょっと、ちょっと変だな。

そんなかんじで意地悪しすぎるとちょっと想定外の挙動をしますが、ある程度の良識あるちょっと間違っちゃったてへぺろ程度のコードであれば割とよろしくやってくれそうです。べんりですね。

MavenでSpringを含む依存jarごと一つのjarにするときハマった件

タイトルが長い。

さらに詳しく言うなら、
MavenでSpringを含む依存jarごと一つのjarにしてインターネットに出られないサーバでそのjarを実行したときハマった件
です。

まず前提として、Springさんはクラスパス内にapplicationContext.xmlとかで定義したxsdファイルが見つからない場合、そのURLを見に行ってパースするようです。
このxsdファイルは、例えば「http://www.springframework.org/schema/beans/spring-beans-4.0.xsd」で言うと、spring-beans-xxx.jarの中のMETA-INF/spring.schemasの中で

http\://www.springframework.org/schema/beans/spring-beans-2.0.xsd=org/springframework/beans/factory/xml/spring-beans-2.0.xsd
http\://www.springframework.org/schema/beans/spring-beans-2.5.xsd=org/springframework/beans/factory/xml/spring-beans-2.5.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.0.xsd=org/springframework/beans/factory/xml/spring-beans-3.0.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.1.xsd=org/springframework/beans/factory/xml/spring-beans-3.1.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.2.xsd=org/springframework/beans/factory/xml/spring-beans-3.2.xsd
http\://www.springframework.org/schema/beans/spring-beans-4.0.xsd=org/springframework/beans/factory/xml/spring-beans-4.0.xsd
http\://www.springframework.org/schema/beans/spring-beans.xsd=org/springframework/beans/factory/xml/spring-beans-4.0.xsd

のように記載されており、ここにある通り、その実体は同じくspring-beans-xxx.jarの中のorg.springframework.beans.factory.xmlパッケージの中に存在しています。

次に、依存jarごと一つのjarにまとめる場合、よく使うのはmaven-assembly-pluginです。
ということで実際にmaven-assembly-pluginでjarを作って、インターネットに出られないサーバで実行してみます。

WARN  org.springframework.util.xml.SimpleSaxErrorHandler:48 - Ignored XML validation warning
org.xml.sax.SAXParseException: schema_reference.4: 1)ドキュメントが見つからなかった、2)ドキュメントを読み取れなかった、3)ドキュメントのルート要素が<xsd:schema>ではなかったため、スキーマ・ドキュメント'http://www.springframework.org/schema/beans/spring-beans-4.0.xsd'の読取りに失敗しました。

_人人 人人_
> 突然の死 <
 ̄Y^Y^Y^Y ̄

なんぞこれーということでちょっとjarを展開してみます。
META-INFの下にはspring.schemasがありましたが、中身を見てみるとspring-beansの記述が無く、
spring-contextなどの記述がある状態。
いくつかのSpringのjarに依存していたので、どうやらそれぞれのspring.schemasが上書きに次ぐ上書きを繰り返し、最後に上書きされたやつが成果物に含まれている模様。
というわけで色々調べていると、「maven-shade-plugin」というプラグインならうまいこと出来るみたいなので、maven-assembly-pluginの部分と置き換えてみます。

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-shade-plugin</artifactId>
	<version>2.2</version>
	<configuration>
		<finalName>shade-sample</finalName>
		<transformers>
			<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
				<mainClass>com.tm8r.ShadeTest</mainClass>
			</transformer>
			<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
				<resource>META-INF/spring.handlers</resource>
			</transformer>
			<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
				<resource>META-INF/spring.schemas</resource>
			</transformer>
		</transformers>
	</configuration>
	<executions>
		<execution>
  			<phase>package</phase>
			<goals>
			  <goal>shade</goal>
			</goals>
		</execution>
	</executions>
</plugin>

こんな感じ。
「org.apache.maven.plugins.shade.resource.AppendingTransformer」を指定したtransformerを定義してやると、resourceに指定したファイルが複数あったとき、そのファイルに追記をしていくような挙動をしてくれる模様。

というわけでこれでpackageを実行してやると、spring.schemasとspring.handlersに対して、依存jarに含まれる同名ファイルの内容が全て記述されたものが成果物に含まれる形になりました。
めでたしめでたし。

GuavaのSetsでSetの差分をとったりする

えー今更Guava-?キモーイ!Guavaの記事が許されるのは2012年までだよねー!キャハハハ!
みたいな反応が無いかgkbrしながらも、ちょっといじる機会があったので。
いやGuava全然まだ開発されてるんですけども。

AとBのSetがあってAにあってBにないもの、BにあってAにないものを抽出したい、そんなとき使えるのがSetsのdifferenceメソッドさんです。

Set<String> testSet1 = new HashSet<>();
testSet1.add("a");
testSet1.add("b");
testSet1.add("c");
Set<String> testSet2 = new HashSet<>();
testSet2.add("b");
testSet2.add("c");
testSet2.add("d");
testSet2.add("e");
Set<String> diff1 = Sets.difference(testSet1, testSet2);
Set<String> diff2 = Sets.difference(testSet2, testSet1);

このようにすると、以下のような結果が返ってきます。

diff1:[a]
diff2:[d, e]

べんり!

両方に含まれるものを抽出する場合はintersectionメソッドさんを使います。

Set<String> intersection = Sets.intersection(testSet1, testSet2);

結果は以下の通り。

intersection:[b, c]

捗る。やっぱりGuavaさんすてき。

jsoupでHTMLをパースする

オヒサシブリデース。
ついにはてなブログに移行しました。インポート楽ちんでよいですね。

ここのところあんまり新しいことしてなかったので書くことなかったんですが、
久々に触ったことなかったライブラリに触ったので覚書までに。

HTMLをパースする案件がありまして、以下のパーサを触ったんですが、
割と精度も使い勝手もパフォーマンスもよかったjsoupを紹介してみます。

  1. jsoup
  2. jericho
  3. HtmlCleaner
  4. Validator.nu
  5. HTMLEditorKit
  6. TagSoup
  7. HTML Parser
  8. NekoHtml
  9. JTidy

準備

mavenでビルドするのを想定して、まずはpom.xmlに以下を追記します。

<dependency>
	<groupId>org.jsoup</groupId>
	<artifactId>jsoup</artifactId>
	<version>1.7.3</version>
</dependency>

公式サイトによると1.7.3が最新みたいです。
jsoup Java HTML Parser, with best of DOM, CSS, and jquery

実装してみる

まずは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>";
    // パーサがバッククォートをタグ内のクォートとして認識しないので置換(IE対策)
    String replacedText = StringUtils.replace(target, BACKQUOTE, DOUBLEQUOTE);

    // 閉じられてないタグのみを渡した場合にパーサが勝手にHTMLとして整形し、渡したものが除去されるので自前で整形する
    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)\">";
    // パーサがバッククォートをタグ内のクォートとして認識しないので置換(IE対策)
    String replacedText = StringUtils.replace(target, BACKQUOTE, DOUBLEQUOTE);

    // 閉じられてないタグのみを渡した場合にパーサが勝手にHTMLとして整形し、渡したものが除去されるので自前で整形する
    replacedText = PREFIX + replacedText + SUFFIX;
    Document doc = Jsoup.parse(replacedText);
    // aタグを抽出
    System.out.println("a:" + doc.getElementsByTag("a").size());
    // 属性名がhrefのものを含むタグを抽出
    System.out.println("href:" + doc.getElementsByAttribute("href").size());
    // 属性名がonで始まるものを含むタグを抽出
    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の方が優れてる感じだったので見送りました。
こちらの紹介は気が向いたら。向かなさそうですが。

jacksonでデシリアライズする際に未知のプロパティを無視する

大したことじゃないんですけど毎回忘れて調べてる気がするので覚書。
jacksonであるjsonをデシリアライズしたいんだけど、そこに未知のプロパティがあったとき、UnrecognizedPropertyExceptionが発生しちゃうので、それを回避するやつです。


たとえば以下のようなクラスがあったとして。

public class TestJsonBean {
	private String str;
	public String getStr() {
		return str;
	}
	public void setStr(String str) {
		this.str = str;
	}
}


こんなコードでデシリアライズを実行してみます。

ObjectMapper om = new ObjectMapper();
String testStr = "{\"str\":\"hoge\",\"unknownstr\":\"fuga\"}";
TestJsonBean bean = om.readValue(testStr,TestJsonBean.class);


すると、案の定以下のようなExceptionが発生します。

Exception in thread "main" org.codehaus.jackson.map.exc.UnrecognizedPropertyException: Unrecognized field "str3" (Class jp.ameba.TestJsonBean), not marked as ignorable


今回は未知のプロパティを無視して、存在するプロパティだけをデシリアライズしたいので、クラスに以下のようなアノテーションを追記します。

@JsonIgnoreProperties(ignoreUnknown=true)
public class TestJsonBean {
(略)
}


この状態で先ほどと同じコードでデシリアライズを実行すると、無事存在する「str」だけがデシリアライズされる結果になります。
かんたん!

javaでパスワード保護されたExcelファイルを作る

javaでExcelファイルとか作るのテンション下がりますよね!こんにちは!


Excelファイル全体をパスワード保護して、パスワード入力しないと開けないようにしたいという案件がありまして。
それをjavaで実装する必要がありまして。まあ自分の案件じゃないんですけど。
質問されたのでちょっと調べてみました。


java - Create a password protected excel file using apache poi? - Stack Overflow
去年の1月の投稿ですが、「無料で暗号化できるライブラリなんて存在しないぜ!金払え金!それが嫌ならzip圧縮してそいつにパスワードかけな!」みたいなことが書いてある雰囲気です。
有償だとAspose.Cellとかが結構有名みたいですね。


でもやっぱりこんなことのために有償のもの使うの嫌なので必死に探して見つけました。
Edit/View Excel Spreadsheet with Jxcell in Java
このライブラリでパスワードがかけられそうです。


サンプルも結構充実してます。
Edit/View Excel Spreadsheet with Jxcell in Java
暗号化は「Open Encrypted Excel files and Save Encrypted Excel files」ってやつを使えばいけそうです。


サンプルのソースを一部引用します。

View m_view = new View();
// 指定したセルに値を書き込み
m_view.setTextAsValue(1,2,"Jan");
m_view.setTextAsValue(1,3,"Feb");
(略)
// encrypt.xlsという名前でhiというパスワードをかけて保存
m_view.write(".\\encrypt.xls", "hi");


かんたん…!
あんまり詳しく調べてませんが、関数の使用なども含め、簡単なExcelファイルの作成は出来そうです。
ちなみに日本語も使えました。


Mavenに上がってないのだけがちょっとアレ。
他にも同様の操作の出来る無償ライブラリはあるかもしれませんが、とりあえず要件は満たせたので今回はこんなところで。

JMeterでリクエストパラメータを使いまわす

こんにちはこんばんは!JMeterシナリオ、書いてますか!
僕は書いてません!


例のごとく質問されて調べたりしたので覚書です。


以前こんなのを書いたんですけど、
JMeterでCSVファイルのランダムな行を抽出する! - するめとめがね
「ランダムに取るのはこれで出来るようになったけど、取った値を連続で次のリクエストに使うにはどうすればいいんだよコラ、教えろよコラ、お前のブログ情報足りねーんだよクソが」みたいな質問を受けました。
具体的にはデータ登録のリクエストを投げたのち、その更新が果たして成功してるか確認するリクエストを同じパラメータを用いて行うみたいなシナリオです。


確かに普通にやるとサンプラーごとに取れる値が変わっちゃうので上手くいきません。
またBeanShellみたいなめんどくさいことをせずに直後のリクエストまで参照可能な変数作れないかなーとか考えたんですけど、どうにも思いつかないので正規表現を使います。


まず、CSVファイルからランダムで値を取得して変数にセットする処理は上に書いた記事と同じ形にします。
すると、userIdとentryIdの組み合わせをランダムで取得することができます。
次にまず登録のリクエストを投げるサンプラーを作ります。
普通にサンプラー>HTTPリクエストを選択し、パスを以下のような感じにします。
※URLとかパラメータの型とかはご自分の環境に置き換えてください。

add?user_id=${userId}&entry_id=${entryId}


次に生成したサンプラーを右クリック、追加>後処理>正規表現抽出を選択し、以下のように設定します。

Response Field to Check : URL
参照名 : req_param
正規表現 : .*(user_id=([a-z0-9-]+)&&entry_id=([0-9]+))
テンプレート : $1$
一致番号 : 0


Response Field to Check項目はデフォルトだとBodyになっていて、この場合レスポンスの中身を参照しますが、URLにするとリクエストのURLが対象になります。
なのでURLを指定し、その中から必要な部分だけ抽出してやればいいわけです。
今回はまとめて取りましたけど、パラメータの名前が変わったりする場合は必要な部分だけ抜き出せばよいです。


で、仕上げに先ほどのサンプラーの下にサンプラーを追加し、パスを以下のような感じにします。

get?${req_param}


かんたんですね。
リダイレクトとかがあるとうまくいかないかもしれないので、その場合は2つ目のサンプラーを実行する前にロジックコントローラを噛ませてreq_paramの存在確認とかをしてやればよいかなと。


なんかもっとスマートな方法があるといいんですが、思いつかず。
とりあえずやりたいことは実現できたのでよいかなという感じで。

スポンサーリンク