matarilloの雑記

GitHubに公開したソフトウェアなどについて書きます。

HTML Parser "dcsoup" のバージョンを 0.2.0 に更新しました。

GitHubにもリリースタグを付けました。

NuGet Galleryはこちら。

ライブラリに含まれるクラスのプロパティが、Javaっぽい部分と、jQueryっぽい部分と、.NETっぽい部分がぐちゃぐちゃになってます。いずれきちんとした指針を立てた上で整理したいのですが。

と書いてたあたりをやっつけました。

具体的には

// getter
public string Text()
{
  return text;
}

// fluent setter
public Element Text(string text)
{
  this.text = text;
  return this;
}

みたいになってたところを

public string Text { get; set; }

みたいにプロパティにしてしまった上で、jQueryライクなメソッドチェーンのためには

public static class FluentUtility
{
  public static Element Text(this Element self, string text)
  {
    self.Text = text;
    return self;
  }
}

と、拡張メソッドで対処しました。

これが本当にいいのかというと、ちょっとわからないのですが。

Sharpenの最新版

「dcsoupがテストに通るようになりました」に書いたSharpenですが、MonoプロジェクトのGitHubに置いてあるやつを使ってみました。

  • Eclipseプラグインとして使うのは想定してないっぽい。EclipseJavaパーザーとかそういうのも含めて全部jarにまとめてしまって、コマンドラインから叩けってことみたい。
  • ただコマンドライン引数の解析のやり方がやっつけっぽい。java -jar sharpenの全部入りjar SOURCEPATH でいいとか言ってるんだけど、ソースパスにスラッシュ(/)が入ってることを前提としてたりして、コード読まないとわからん。
  • コマンドライン引数がちゃんとしてればちゃんと動くっぽい。
  • ジャグ配列の初期化 int[][] array = new int[3][4]; で変換がこけた。たぶん古いバージョンでもこける。
  • エンクロージング型の親クラスメソッドへの呼び出し、つまり エンクロージング型名.super.メソッド(); も変換がこけた。たぶん古いバージョンでもこける。
  • タイプセーフenumがどうなったかは確認してない。 以下のような感じになりました。

Javaコード

public enum Strategy {
    Foo {
        String execute() {
            return "this is Foo";
        }
    },
    Bar {
        String execute() {
            return "this is Bar";
        }
    };
    abstract String execute();
}

C#コード

using Sharpen;

[System.Serializable]
public sealed class Strategy
{
    public static readonly Strategy Foo = new Strategy();

    public static readonly Strategy Bar = new Strategy();

    internal abstract string execute();
}

変換ではエラーにならなかったけど、execute() の実装がぜんぶ抜け落ちてるじゃん…… Strategyクラスはabstractメンバー持ってるよね、コンパイル通らないよ……

dcsoupがテストに通るようになりました

というわけで本体コードもテストコードもJavaからのコンバートが片付いたので、知見をメモしておきたいと思います。

Sharpen

JavaのコードをC#に変換するのには Sharpen を使いました。 とはいえ私が実際に使ったのは(ngitのリポジトリにある)古いバージョンのやつでした。なーんだ、こっちはちゃんとメンテされてるじゃん……

というわけで以下に書くのは古いSharpenに基づくものなので、最新版では事情が違うかもしれません。違うといいな。あとで試そう。

Eclipse

Eclipse 3.7 IndigoじゃないとSharpenプラグインがうまく動きませんでした。最新のSharpenなら違うのかな……

Eclipse 3.7 の \plugins フォルダに sharpen.core_1.0.0.jar を放り込んでEclipseを起動したら認識しているはず。

eclipse

設定ファイル

プロジェクト直下に設定ファイルが必要です。run-sharpen.xml (Antタスク定義)、sharpen.properties (パスの設定)、sharpen-all-options (マッピング定義)、header.txt (C#の共通ヘッダテンプレート、Antタスクで参照されてる)に関しては、 この記事その翻訳からリンクされている、Paulさんの設定ファイルテンプレートを元にするといいかも。

ちなみに、sharpen.properties にSharpenの作業ディレクトリが書いてあるけど、そのディレクトリが見つからないとエラーになるから注意。

実行前

最初は極小のサンプルコードみたいなやつを変換して、動くか確認するといいと思います。

ant task

run-sharpen.xml にAntのアイコンが出てると思うので、右クリックして "Run as"を選べば動きます。

思ったより普通に変換してくれましたが、注意点はあります。それは、一部のタイプセーフenumが変換エラーになったことです。 いわく、「enumにはフィールドとかメソッドとか定義できないよ!」とのこと。いや、C#ではできないのはわかるけど、そこはよしなにやってほしかった。

仕方ないので、コンバート前のJavaコードを人力で書き換え。メソッドなどを持つenumをまるっと普通の抽象クラスにしました。

人力プリコンパイル前:

enum Strategy {
    Foo {
        void execute() { ... }
    },
    Bar {
        void execute() { ... }
    };
    abstract void execute();
}

人力プリコンパイル後:

abstract class Strategy {
    public static final Strategy Foo = new Strategy("Foo") {
        void execute() { ... }
    };
    public static final Strategy Bar = new Strategy("Bar") {
        void execute() { ... }
    };
    abstract void execute();

    final String name;
    protected Strategy(String name) { this.name = name; } 
    public String name() { return name; }
    public static Strategy valueOf(String name) { ... }
}

なにこれ面倒くさい。nameの処理とかすげえ面倒くさい。 こんなんだったらSharpenを使わずに手でC#にしようか……とも思ったのだけどそっちのほうがミスが多くなりそうだったから諦めて上みたいに書き換えました。

実行後

さて、変換エラーを手で回避する作業が終われば、無事C#に変換されたコードが手に入ります。 とはいえ、すぐコンパイルが通るかというとそうでもなかったり。

まず気づくのが、using Sharpen; とか Sharpen.Runtime.Substring(...) とか IList<string> list = new AList<string>(); みたいなコード。 結局ね、Javaの標準クラスライブラリを全部.NETの標準クラスライブラリに機械的に置き換えることには無理があるのですよ。なので、拡張メソッドを生やしたり、JavaのクラスをシミュレートするC# クラスを作ったりしているのです。 そのあたりのクラスのソースコード(ただし古い)はこちら。最新のものは最初に書いたリポジトリにあるんじゃないかな。

次に注意するのは命名規則とかそういうやつのあれ。Java

class Foo {
    public static class Bar {}
    public Bar bar() { return new Bar(); }
}

みたいな感じで、メンバーに Barbar() がいたとき、Sharpenにお任せすると

class Foo
{
    public class Bar {}
    public Bar Bar() { return new Bar(); }
}

と、メソッド bar()Bar() になって、そのせいでコンパイルに通らなかったりしました。 そこはレアケースだから許容するんだけど。

3番目のの注意点はジェネリクスワイルドカード

いや、変換はできるんですが

public boolean addAll(Collection<? extends Foo> col) {
    ...
}
public boolean removeAll(Collection<?> col) {
    ...
}

public virtual bool AddAll<_T0>(ICollection<_T0> col)
    where _T0:Foo
{
    ...
}
public virtual bool RemoveAll<_T0>(ICollection<_T0> col)
{
    ...
}

になって嬉しいかは自分で考えなければならないでしょう。 つまり、「その型 _T0 はほんとに必要なのか?」ってことです。

そして最後の注意点。変換後のC# コードには、おそらく virtual とか protected とか、そういう修飾子が山ほど入ってきます。これも、「その修飾子はほんとに必要なのか?」と考えたほうがいいでしょう。とはいえ、要らないからといって削除するのも面倒だったりしますが……

~~~~~~~~

と、長くなったのでこのあたりで。続く!(かも)

HTML Parser "dcsoup" を公開しました。

Javaで書かれたHTML Parser、“jsoup” .NET (C#) に移植中です。なんとなく動く感じなのでとりあえず公開しました。

https://github.com/matarillo/dcsoup

これは何?

HTMLパーサーです。こんな感じで使います。

using System;
using System.Globalization;
using System.Net.Http;
using Supremes;

class Program
{
    public static void Main(string[] args)
    {
        // 株価を取得したいサイトのURL
        var code = "7984.T";
        var urlstring = string.Format("http://stocks.finance.yahoo.co.jp/stocks/detail/?code={0}", code);

        using (var client = new HttpClient())
        {
            // 指定したサイトのHTMLを取得する
            var message = client.GetAsync(urlstring).Result;
            // dcsoupでパースする
            var document = message.Parse();
            // jQueryライクなセレクタで株価部分を取得する
            var priceNode = document.Select("td[class=stoksPrice]")[0];
            // 取得した株価がstring型なのでint型にパースする
            var price = int.Parse(priceNode.Text, NumberStyles.AllowThousands);
            Console.WriteLine("コクヨ(7984.T)の株価: {0}円", price);
        }
    }
}

(酢酸さんインスパイア)

あ、何やってるかが分かりにくいですね。using Supremes;System.Net.Http.HttpResponseMessageクラスに対してParse()拡張メソッドが有効になってます。

文字列、ファイル、ストリームなどからパースするときは Supremes.Dcsoup 静的クラスを使うとよいです。

HTML Agility Packと何が違うの?

jQueryライクなセレクタ」のとこが、XPathよりもっとjQueryライクです。

using System;
using System.Net.Http;
using Supremes;

class Program
{
    public static void Main(string[] args)
    {
        using (var client = new HttpClient())
        {
            var doc = client.GetAsync("http://ja.wikipedia.org/").Result.Parse();
            var featuredArticles = doc.Select("#mf-tfa b a");
            Console.WriteLine(featuredArticles.Text);
        }
    }
}

パーサーはHTMLの構造を知っています。たとえば次のようなHTMLをパースすると……

using System;
using Supremes;

class Program
{
    public static void Main(string[] args)
    {
        var doc = Dcsoup.Parse("<table> <tr> <td>ABC <td>DEF <td>GHI </tr></table>");
        Console.WriteLine(doc);
    }
}

出力はこうなります。

<html>
 <head></head>
 <body>
  <table>

   <tbody>
    <tr>

     <td>ABC </td>
     <td>DEF </td>
     <td>GHI </td>
    </tr>
   </tbody>
  </table>
 </body>
</html>

さらに、jQueryライクにパース結果のDOMを変更できます。上のコードに1行追加してみましょう。

using System;
using Supremes;

class Program
{
    public static void Main(string[] args)
    {
        var doc = Dcsoup.Parse("<table> <tr> <td>ABC <td>DEF <td>GHI </tr></table>");
        doc.Select("td").AddClass("foo bar"); // クラスを追加
        Console.WriteLine(doc);
    }
}

その結果、出力はこうなりました。

<html>
 <head></head>
 <body>
  <table>

   <tbody>
    <tr>

     <td class=" foo bar">ABC </td>
     <td class=" foo bar">DEF </td>
     <td class=" foo bar">GHI </td>
    </tr>
   </tbody>
  </table>
 </body>
</html>

既知の問題

  • テストコードがまだコンバートされていません。 テストコードをコンバート中です。 テストのコンバートが終わってバグ修正もしました。テストに通ります。
  • 元のjsoupにはないバグが多く含まれています。すぐ例外を吐いて死にます。 目立つバグは潰したので、わりとちゃんと動くと思います。ただし、元のjsoupとはAPIレベルでの差異や、細かい挙動の違いはあります。いずれGitHubにまとめます。
  • ライブラリに含まれるクラスのプロパティが、Javaっぽい部分と、jQueryっぽい部分と、.NETっぽい部分がぐちゃぐちゃになってます。いずれきちんとした指針を立てた上で整理したいのですが。 .NETのプロパティに基本的に寄せました。あとで記事を書きます。
  • まだNuGetで配布してません。 テストがまともに通るようになったら考えます。 テストに通るようになったので着手します。 できました。