XPath 基礎 (4)

XPath の基礎 (4) です。


XPath では、CSS ではできないような強力な選択を行うことができます。

これはテキストノードを使用できることがその主な要因であると感じます。

個人的によく使用するのが、「ある文字列が要素に含まれているか」という判断です。
例えば「"マツタケ"という言葉を含む div タグを探す」などです。これは CSS で行うことはできません。

このケースでは、以下のような XPath で選択することができます。

//div[contains(text(),'マツタケ')]


text() は、そのノードの子テキストノードという意味でした。ですから上記で、「すべての div タグのうち、子テキストノードに"マツタケ"を含むもの」を選択することになります。


ところで、上記の XPath では、以下のどちらの div も選択されるでしょうか?

<div>
マツタケの土瓶蒸し
</div>
<div>
  <span>マツタケ</span>の土瓶蒸し
</div>


実は上記の XPath では、2つ目の div は選択されません。text() はあくまで子テキストノードを指すからです。
2つ目の div の子テキストノードは空であり、条件に一致しません。


こういった場合はどうするかというと、以下のようにします。


//div[contains(.,'マツタケ')]


text() を . に変えています。. は、self::node() の省略形です。つまり上記でいえばそれぞれの div を指しています。

これでなぜ2つ目の div も選択されるかというと、

  • 関数呼び出しにおいては、引数が文字列型の場合は文字列値に変換化される
  • 要素の文字列値は、その子孫ノードのすべての子テキストノードを連結したものである

からです。

つまり、contains(.,'マツタケ') に渡される . は div 要素であり、contains の引数は文字列型なので引数は文字列化され、div 要素を文字列化したものはその div が囲っているすべてのテキストノードを連結したものであり、それはどちらの div も "マツタケの土瓶蒸し" なので、"マツタケ" を含むため条件に一致するということになります。

まとめると、タグのすぐ直下にある文字列だけを狙い撃ちしたいときは text() を使い、タグの下全部を対象にしたい場合は . を使うようにすればよいでしょう。



XPath 基礎 (3)

XPath の基礎 (3) です。

//div[@name='hoge']


上記のように書けば、name 属性が "hoge" であるすべての div タグを選択できます。

実は XPath には様々な省略記法があり、実は上記もその省略記法で書かれていて、省略しないで書くと以下のようになります。

/descendant-or-self::node()/child::div[attribute::name='hoge']


省略記法で書かれたものに比べると、省略記法を使用しないものは長ったらしくなっていますが、XPath の構成要素をちゃんと理解することは、XPath を書くときの一助になってくれると思います。

XPath の主要な構成要素には、ノードテスト述部 があります。

軸は、選択するもののベースを決めるもので、child、self、parent、attribute などがあります。

child であれば子ノード、self であれば自ノード、parent であれば親ノード、attribute であれば属性を意味します。

なお軸の後ろには :: をつける決まりです。

ノードテスト

ノードテストは、指定した軸の何を選択するかを決めるもので、要素名や属性名 や text()、 * などがあります。

例えば child::div であれば、子ノードのうちの div 要素を意味し、child::* であれば、子ノードのうちのすべての要素を意味します。

child::text() であれば、子テキストノードを意味します。CSS では意識することはありませんが、タグとタグの間にあるテキスト部分(<div>ここ</div>)がテキストノードです。

述部

述部は、軸とノードテストで指定したものをフィルタリングするものです。なくてもかまいません。

述部は [ と ] で表し、その中に を指定します。ここでは簡単に、真か偽かを判定する式だと考えましょう。

例えば attribute::name='hoge' なら「属性 name が "hoge" である」という条件でフィルタリングすることになります。



ここで最初に戻って、以下の XPath について考えてみます。

/descendant-or-self::node()/child::div[attribute::name='hoge']

まず、最初の / は「文書のルート」です。これはそのように決められています。

descendant-or-self が軸で、その意味は「自分およびその子孫」になります。

node() はノードテストで、すべての子ノードを意味します。

したがって /descendant-or-self::node() は、「全部のノード」を意味することになります。

次に child::div[attribute::name='hoge'] ですが、この child は軸で、/descendant-or-self::node() で選択されたそれぞれの子、ということになります。

child::div の div はノードテストで、要素 div をあらわしています。つまり、/descendant-or-self::node() で選択されたそれぞれの子の div 要素、平たく言えば 全部の div 要素 ということになります。

[attribute::name='hoge'] は述部で、上記にある通り「name 属性が "hoge" である」という条件になります。ですから、すべての div 要素のうち、name 属性が "hoge" であるもの を選択するということになります。




XPath 基礎 (2) 属性値での指定

HTMLタグの属性値の値によって、何らかの要素を指定したい場合があります。

<div class="foo bar">
</div>
<div id="hoge">
</div>
<div name="hoge">
</div>


CSS の場合、上記であれば、 以下のようなセレクタで指定できます。

.foo { ... }
#hoge { ... }
[name="hoge"] { ... }


XPath の場合は、以下のようになります。

//div[contains(@class, 'foo')]
//div[@id='hoge']
//div[@name='hoge']


上記を見てわかる通り、XPath では属性を @~ であらわします。@id なら id 属性ということになります。

CSS セレクタはあくまで HTML タグを選択するものですが、XPath では属性値を取得するために使うこともあります。そういった場合には、例えば //div/@id のようにして属性を指定することになります。

「id 属性が hoge であるもの」を指定するには、@id='hoge' とします。

name 属性でも同じわけですが、class 属性は contains(@class, 'foo') となっています。

これは、class 属性の値はあくまでも "foo bar" であって、"foo" ではないからです。CSS では .foo とすればそのクラス foo が指定されているものを選択できますが、XPath では属性毎に記法が分かれているわけではないため、「class 属性に foo が含まれているもの」という指定をしなければなりません。

それを指定するのが contains(@class, 'foo') というわけです。この contains は関数であり、他にも様々な関数が使用できます。

関数はネストさせることもできます。例えば「id 属性に hoge含まないもの」を選択するなら、not を使って以下のようにできます。

//div[not(contains(@id, 'hoge'))]


関数は他にも様々あり、それらを使用することで複雑な検索を行うことができます。


XPath 基礎 (1)

みなさんもご存知の通り、Web ページは HTML で書かれています。

ですので、スクレイピングしたりクライアントサイドで動的なページを作ったりするときには、自分が処理したいその部分を何らかの方法で指定する必要があります。

jQuery 使いの方に馴染みが深いのは CSS セレクタかと思います。

.block {
  width: 100px;
}

.block がセレクタですね。 「class 属性に block が指定されている要素」を指定するものです。

CSS セレクタでもかなり複雑な指定ができますが、XPath を使用するとさらに複雑な指定ができるようになります。

CSS セレクタでは、

  • タグ名で指定した要素 (div, p)
  • id 属性や class 属性に、指定した文字列が設定されている要素 (#hoge, .hoge)
  • 任意の属性の値が任意の文字列と一致する要素 (div[class="hoge"])
  • 指定した要素の下位、直下、兄弟などの要素 (div p a, div > p)

といった指定の仕方が基本となります。

XPath でも同じように、タグ名、構造、属性値などで要素を指定することができます。

XPath では、ロケーションパスという表現を使って要素を指定します。例えば、

//div

これは全部の div タグを指定することになります。

XPath では、ディレクトリ構造を指定するように、/ (スラッシュ) で要素を区切って指定します。

//div/p/a

上記であれば、「すべての div タグの直下にある p タグの直下にある a タグ」ということになります。

最初の / はルートノードを意味します。ルートノードとはなんぞやということですが、HTML 文書は入れ子の構造であり、つまりツリー構造になっています。
その一番上の頂点がルートノードです。ですので、

/div

としてしまうと、それは頂点の直下の div ということになってしまいますので、これに合致する要素はありません。

// は、「自身および子孫」を表します。なので、//div はすべての div タグを意味するというわけです。

//div//p

上記のようにすれば、「すべての div タグのそれぞれの子孫にある p タグ」ということになります。

<div id="d1">
  <p>...</p>
  <div id="d1-1">
    <p>...</p>
    <section>
      <p>...</p> <!-- この p は div/p ではマッチしない -->
    </section>
  </div>
</div>
</html>




Salesforce で You have uncommitted work pending エラー

Salesforce の Apex コードでは、外部のWebサービスを利用することができます。つまり HTTP リクエストを送信することができます。 これを Salesforce ではコールアウトと呼んでいます。


通常の Apex コードではコールアウトを行うことはできず、callout=true を付与した future メソッドなどの、個別のスレッドを使用する仕組みを用いなければコールアウトを行うことはできません。


future メソッドを実装すること自体は、単に @future アノテーションを使用すればよいだけなので、特段難しいことはありません。
ただし、以下のような点には注意が必要です。

  • future メソッドの引数にはプリミティブ型かプリミティブ型のコレクションしか指定できない (つまりオブジェクトをそのまま渡すことはできないし、独自のクラスのインスタンスを渡すこともできない)
  • future メソッドは別のトランザクションになる - static な変数を共有することはできない
  • future メソッドは非同期である - いつ実行されるのかは不定である (ただし単体テストでは Test.stopTest の後には完了している)
  • future メソッドから future メソッドを呼ぶことはできない
  • future メソッドは、ガバナ制限により1つのトランザクションでは50回までしかコールできない


上記ののような future メソッドに関する注意点には気をつけていたのですが、先日初めてのエラーに遭遇しました。
こんな処理がありました。

トリガ発動 → コールアウト実行 → 結果をDMLで書き込み


コールアウトを行う箇所は future メソッドになっていて、コールアウト実行後、その結果を UPDATE するというよくあるような処理です。

当初は問題なく動作していました。


その後、複数レコードに未対応であることが発覚し、以下のように修正しました。

トリガ発動 → (コールアウト実行 → 結果をDMLで書き込み)をレコード数分繰り返し


ところがここで、標題の「System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out」が発生してしまいました。


この原因はメッセージの通りで、未コミットのデータベース変更がある状態ではコールアウトは実行できないというものです。

コールアウト実行 → 結果をDMLで書き込み(ここまではOK) → コールアウト実行(ここでエラー) というわけです。


なんのための制限かわかりませんが、(コールアウト実行 → 結果をリストやマップなどに保持)を繰り返す → レコード更新 とする必要があります。



Salesforce はこの手の落とし穴のようなところが少なくありませんので、気をつけたいところです。


MySQL をコマンドラインで使う

自動化するならコマンドラインですね。ということで、最近使った MySQLコマンドラインを記録しておきます。

データベースを作成する

mysql -u<ユーザID> -p<パスワード> -e "CREATE DATABASE <データベース名> CHARACTER SET utf8mb4"

※ -u と -p の後ろは空けない

そのデータベースに全権を持つユーザを作成する

mysql -u<ユーザID> -p<パスワード> <データベース名> -e "GRANT ALL ON <データベース名>.* TO <新しいユーザ名>@localhost"


その作成したユーザにパスワードを設定する

mysql -u<ユーザID> -p<パスワード> <データベース名> -e "SET PASSWORD FOR <ユーザ名>@localhost=password('<セットするパスワード>')"


そのデータベースにデータをインポートする

zcat <gzip圧縮されたSQL文> | mysql -u<ユーザID> -p<パスワード> <データベース名>


UPDATE文を発行してインポートしたデータを修正する

mysql -u<ユーザID> -p<パスワード> <データベース名> -e "UPDATE wp_options SET option_value=<変更後の値> WHERE option_name='siteurl'"

※ここでは Wordpress の wp_options を直す例



上記はバッチ/スクリプトにできるので、例えば新しくデータベースを作成して、そのデータベースと同名のユーザにそのデータベースへの全権を付与して、何らかのデータをインポートする といったことであれば、以下のようにできます。

#!/bin/bash

mysql -u<ユーザID> -p<パスワード> -e "CREATE DATABASE $1 CHARACTER SET utf8mb4"
mysql -u<ユーザID> -p<パスワード> $1 -e "GRANT ALL ON $1.* TO $1@localhost"
mysql -u<ユーザID> -p<パスワード> $1 -e "SET PASSWORD FOR $1@localhost=password('$1')"
wget http://<どこかのサーバに置いてあるデータ>/data.sql.gz
zcat data.sql.gz | mysql -u<ユーザID> -p<パスワード> $1
rm -f data.sql.gz

この例では可変なのはデータベース名(ユーザ名)のみですが、可変にしたい箇所は引数として受け取るようにすればよいでしょう。

これを c.sh として保存したすると、使うときは、

sh c.sh <データベース名>

となります。


Tooling API を使ってログを設定する

Salesforce には Tooling API という非常に強力な API があります。
どのようなことができるかというと...

  • Apex クラスおよびトリガと Visualforce ページおよびコンポーネントの作業コピーを管理する。
  • 静的リソースファイルの作業コピーを管理する。
  • Apex クラスおよびトリガと Visualforce ページおよびコンポーネントの作業コピーに更新やエラーがないかチェックし、変更を組織にコミットする。
  • ヒープダンプマーカーを設定する。
  • Apex 実行時に Apex コードまたは SOQL ステートメントをフロート表示する。
  • Apex を匿名実行する。
  • チェックポイントを設定して自分または他のユーザ用にログファイルを生成する。
  • デバッグログおよびヒープダンプファイルにアクセスする。
  • カスタムオブジェクトのカスタム項目を管理する。
  • コードカバー率の結果にアクセスする。

https://developer.salesforce.com/docs/atlas.ja-jp.salesforce1api.meta/salesforce1api/apis_tooling_introducing.htm より

動的に生成されたApexクラスを追加するなど、そんなことが出来ていいのかというようなこともできてしまうようです。

この中で最近使用したのが、デバッグログへのアクセスです。
私の知る限りログ設定の有効期限は24時間が最大のため、24時間を超えてログを出力させたい場合は都度設定しなければなりません。
しかも、デバッグログはユーザごとに設定が必要なため、なにかの現象の再現待ちを行うなどで全ユーザにログを設定したいとなった場合には非常に手間がかかります。

そこで、以下のようなコードでデバッグログの設定を行いました。

Map<String, String> record = new Map<String, String>();
record.put('TracedEntityId', <ユーザのID>);
record.put('LogType', 'USER_DEBUG');
record.put('DebugLevelId', <デバッグレベルのID>);
record.put('ExpirationDate', DateTime.now().addDays(1).addHours(-9).format('yyyy-MM-dd\'T\'HH:mm:ss'));

Http h = new Http();
HttpRequest req = new HttpRequest();
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
req.setHeader('Content-Type', 'application/json');
req.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm() + '/services/data/v43.0/tooling/sobjects/TraceFlag/');
req.setMethod('POST');
req.setBody(JSON.serialize(record));
HttpResponse resp = h.send(req);


API を使用するときは認証をどうするかという問題がありますが、開発者コンソールの匿名ブロックで実行すれば現在のユーザのセッションIDが使用できます。
使用するオブジェクトは TraceLog です。デバッグログの設定はこのオブジェクトのレコードとして格納されています。

ExpirationDate で -9h しているのは、UTC で指定する必要があるためです。

なお実際のコードはもう少し複雑で、実際のコードでは、ユーザの一覧を取得してすでにそのユーザのデバッグログ設定があれば、ExpirationDate を更新するということも行っています。