Rawler Framework C#用、webスクレイピングとテキスト処理のためのフレームワークを作ったよ。その2

その1からの続き。

PreTreeを使っての前処理の記述とツリーの折りたたみ

このフレームワークでいろいろ作っていくうちに気付いたのだけど、タグで囲むのはわかりやすいのだけど、その階層が深くなると、XAMLの見通しがすごく悪くなる。なるべくなら、概念のレベルで扱いたい。例えば、処理の前のテキストの加工(置換、trim、タグの除去など)は本質的な処理とはいえない、これらは必ず必要であるが、階層を深くし見通しを悪くさせてまで必要なものでもない。そのため、<クラス名.PreTree>で囲まれたところにそれらの処理を入れることによって、そのオブジェクトが実行する前の処理をすることができる。XAMLエディタの補助として、PreTreeの中身を折りたためるので見通しはよくなる。

例。

    <TagClear>
        <Trim>
            <DataWrite Attribute="date"></DataWrite>
        </Trim>
    </TagClear>

    <DataWrite Attribute="date">
        <DataWrite.PreTree>
            <TagClear>
                <Trim></Trim>
            </TagClear>
        </DataWrite.PreTree>
    </DataWrite>

は、同じ意味。DataWriteがツリーの上位に来たため、データを書き込むという意味が明確化される。行数的には後者は2行増えるが、PreTreを折りたためるので、3行となり2行分節約される。当然、前処理の数が増えれば恩恵は大きくなる。PreTreeでの注意点は、PreTreeでは複数を扱えない。エラーは出すことはないが、予期しないテキストが出てしまうことになるだろう。複数を扱うときは、今まで通りのやり方でやらないといけない。

ちなみに、このPreTreeはすべてのクラスにあるが、所々のクラスには***Treeというのがある。これは同様にTreeを格納することができるため、一つのオブジェクトで親からのテキストの変数だけではなく、複数の変数を扱うための仕組みである。Tree構造故に、親は一つしか持てず、本来は一つの変数しか扱えなかったことの拡張である。それぞれ、ツリーの一番最後のオブジェクトのテキストが採用される。

ファイル入出力機能

初めは、このツール、結果をメモリーに貯めて、最後の書きだすのを基本として、コードビハインドでの処理を追加によって、逐次的にDBなどに書き込めるような仕様にしていた。プログラマのためのフレームワークということであったが、XAMLのテキストファイルから、起動できるようにしたことから、それだけで完結できるような仕組みにした方がいいと思うようになった。
そこで追加したのがファイル入出力機能だ。クラス名的には、FileSave,FileReadLines,GetCurrentFileReadLine にあたる。
FileSaveはDataを継承しているので、Dataと同じようにDataWriteで内容を書き込み、NextDataRowで次のデータ行になる。FileSaveでは、NextDataRow発生時にファイルへの書き込みが発生する。そのため、途中で仮にソフトが落ちたとしても、そのデータは保存されている。FileSaveは書き込みモードとして、新規書き込みと追加書き込みの二つのモードをもっている。状況に応じて使い分けてくれ。FileNameに値を入れるとそれがファイル名になる。ツールを起動したところにそのファイルが生成されるはずだ。FileNameの値がない時、ダイアログが立ち上がり、そこで選択できるようになっている。ちなみに書き込むファイルのエンコードはUTF8である。
一方、FileReadLinesでは指定したファイルを一行分づつ読むことができる。FileSaveと同様にFileNameでの指定がない時、ダイアログがひらいて指定できる。テキストでの一般的なデータ形式である、タブ区切り、カンマ区切りを扱うためのSplitというクラスを用意した。指定したものでの区切りでテキストをリストにする。Num(0番から始まる)を指定することで、何番目の要素を採用するかを指定できる。これによりテキスト一行分から複数のデータを取得できる。このフレームワークの使用上、階層を進んでいく中でテキストはどんどん変わってしまうため、もともとのテキストが跡形もなくなってしまう。それを回避するために、GetCurrentFileReadLineは上流にあるFileReadLinesのテキストを取得できるクラスだ。Splitと組み合わせることで、DataWriteで読み込んだテキストの内容を書き込むことができる。

例 読み込むファイルの形式は、タブ区切りで初めの列にはURLが入っている。起動すると保存ファイル名と読み込むファイル名の指定をしないといけない。ExtendFilterでtsvと指定しているため、ダイアログではフィルタされた状態になる。

    <FileSave FileSaveMode="Create"  ExtendFilter="tsv"
      xmlns="clr-namespace:Rawler.Tool;assembly=Rawler"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <FileReadLines ExtendFilter="tsv">
            <Split Num="0" SeparatorType="Tab">
                <Page>
                    <Tags Tag="title">
                        <DataWrite Attribute="title"></DataWrite>
                    </Tags>
                    <GetCurrentFileReadLine>
                        <Split Num="1" SeparatorType="Tab">
                            <DataWrite Attribute="key"></DataWrite>
                        </Split>
                    </GetCurrentFileReadLine>
                </Page>
                <NextDataRow></NextDataRow>
            </Split>
        </FileReadLines>
    </FileSave>

このフレームワークの出力として、タブとカンマの混合での出力がなされる。一つの属性に複数の要素が入るためだ。これは他のツールでは扱いにくい。そのための変換も作ることができる。

タブ区切りのデータで、初めの列がKeyとなるもの 次の列がカンマ区切りのデータを想定している。

    <FileSave FileSaveMode="Create"
      xmlns="clr-namespace:Rawler.Tool;assembly=Rawler"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <FileReadLines ExtendFilter="tsv">
            <Split Num="1" SeparatorType="Tab">
                <Split SeparatorType="Comma">
                    <DataWrite Attribute="tag"></DataWrite>
                    <GetCurrentFileReadLine>
                        <Split SeparatorType="Tab" Num="0">
                            <DataWrite Attribute="key"></DataWrite>
                        </Split>
                    </GetCurrentFileReadLine>
                    <NextDataRow></NextDataRow>
                </Split>
            </Split>
        </FileReadLines>
    </FileSave>

出力はKeyとtagのペアが続くものとなる。

条件分岐

構造化プログラミングに置いて、抽象化すると、順次、反復、分岐の三つの要素があるという。このフレームワークにおいて、順次は親子関係と子の並びで達成されており、反復は、RawlerMultiBaseを継承した複数のテキストをもつクラスとその子要素で達成されている。あとは、分岐を組み込めばその3要素をすべてを達成したことになる。
単純な分岐としてEqualとContainsがある。プログラミング言語でいう、IF文に相当する。
Equalは親テキストとの完全一致、Containsは部分一致したとき、処理する内容を子に記述する。ResultをFalseとした時、一致しなかったときとなる。

    <Contains ContainsText="ビジー状態です">
   親テキストが「ビジー状態です」という文字を含んでいる時の処理
    </Contains>
    <Equal EqualCSV="た,です,し,な,だ,ん,で,い,う,さ,れ,て,せ,ます" Result="False">
     親テキストがEqualCSVに含まなかった文字列だった時の処理・・・
    </Equal>

しかし、これでは、if{}else{}は表現できても(ResultをTrue,False両方を用意すればいい)、iF{}else if{}else{} といった条件を指定することができない(if{}else{if{}else{}}とう感じに階層化すれば可能ではあるが、可読性は落ちる)。
そのための、複雑な分岐としてSwitchがある。これはそのあとに、必ず、CaseTextなどのCaseで始まるクラスがそのあとに来ないといけない。指定条件に当てはまるものだけが実行される。また、Switch.SwitchValueTreeを指定することで、Caseで一致の対象となる文字列の取得条件を指定できる。

例、テーブルタグ中で、一行文ずつ読んでいき、その指定されたところに入っている文字列で分岐を行う。

    <TagExtraction Tag="table"  IsSingle="True">
        <TagExtraction Tag="tr" >
            <Switch>
                <Switch.SwitchValueTree>
                    <TagExtraction Tag="td" ParameterFilter="nowrap">
                    </TagExtraction>
                </Switch.SwitchValueTree>
                <CaseText CheckText="レベル">
                  「レベル」だった時の処理
                </CaseText>
                <CaseText CheckText="参加メンバー">
                  「参加メンバー」だった時の処理
                </CaseText>
                <CaseText CheckText="登録タグ" >
                  「登録タグ」だった時の処理
                </CaseText>
                <Switch.OutsideTree>
                    いずれにも当てはまらなかった時の処理。
                </Switch.OutsideTree>
            </Switch>
        </TagExtraction>
    </TagExtraction>

CaseIntでは、数値を扱え、数値の範囲を扱える。テキストでは苦手な連続値を指定の文字列に変更することができる。
例は、タブ区切りのデータのある列を読み、その数値によってテキストを替える例。

     <Split Num="1" SeparatorType="Tab">
        <Switch>
            <CaseInt Start="0" End="9">
                <DataWrite Attribute="Age" Value="10才未満"></DataWrite>
            </CaseInt>
            <CaseInt Start="10" End="19">
                <DataWrite Attribute="Age" Value="10代"></DataWrite>
            </CaseInt>
            <CaseInt Start="20" End="29">
                <DataWrite Attribute="Age" Value="20代"></DataWrite>
            </CaseInt>
        </Switch>
    </Split>

CaseDateを使えば、日付データに対して同様のことができる。

クエリ機能

重複削除機能を実装しようと思ったが、子にその機能を持つクラスを追加する形ではどうにもうまくいかない。そこで、複数のテキストを出すクラスに対して簡単なクエリを行えるようにした。クエリといっても実装はLINQである。まだLINQにあるすべての機能は実装していない。

ページの中のリンクを取得し、重複を削除したものを表示させる。

    <Page>
        <Links>
            <Links.Query>
                <QueryDistinct></QueryDistinct>
            </Links.Query>
            <Report></Report>
        </Links>
    </Page>

また、クエリはQueryで始まるQueryBaseを継承したものなら、いくらでもつなげることができる。ただし複数の子はもつことができない。(意味ないので)

例:1から100までのリストをシャッフルして、上から10個取ってきて、それをソートしたものを出力する。(テキストとして扱っているため綺麗なソートはできない。)

    <Range Start="1" End="100">
        <Range.Query>
            <QueryShuffle>
                <QueryTake Count="10">
                    <QueryOrderBy></QueryOrderBy>
                </QueryTake>
            </QueryShuffle>
        </Range.Query>
        <Report></Report>
    </Range>

他にも、QueryFirst、QueryLast、QueryElementsAt はそれぞれリストの初めのもの、最後のもの、指定した番号のものを返します。これは対象の構造がわかっていれば便利に扱えます。
まぁ、クエリに関しては未実装クエリは多いです。必ず必須なはずのWhereもないですしwww先の条件分岐の命令を使えばできるからこそですけどね。


これで仕様の大半の説明は終わりました。僕的には生産性の向上が高いのですが、難しすぎなんでしょうかねぇ・・・。もっとも作りやすいエディタがVS.NETであるという点が難点といったら、難点ですが…。