それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

ブロッカー独自CSSルール文字列マッチCSSで、特定文字列を持つ要素を消す

CSSには「文字列」にマッチさせる方法がない。

CSS では、textContent にいい感じにマッチさせる方法がない。

xpath ならできる

xpath ならできるんですよね。「おすすめ」というリンク文字列を持つdiv を選ぶ

//div[class='xxx'][ .//a[ contains(. , 'おすすめ') ] ]

広告ブロッカーの独自CSSなら書ける。

CSSではかけなくても、広告ブロッカーが独自拡張しているCSSなら記述できる。

div.xxx:has( a:contains("おすすめ") )

たとえば、以下のように、Adguard では記述できる。 https://github.com/AdguardTeam/ExtendedCss#extended-css-contains

この他にも各種広告ブロッカが切磋琢磨しているので、x:contains文法は、広告ブロッカでは記述できることが多い。ublock 系でも同様の記述ができる。

文字列でマッチさせられて、指定テキストを子要素に持つ( :has )が使えるので、広告ブロッカに自分でルールを考えるのがとても楽になっている。

なんて便利なんだ知らなかったので、いつもXpathで工夫してたよ・・・・

追記

拡張CSSは、動作が不安定(もしかしてページロード時だけ?)なので、CSSで書けるならCSSで頑張ったほうが良い気がする。

Twitterのおすすめユーザー(Who to Follow)が代わり映えしないので、消したい。ずっと同じ人がレコメンドされてる。

ツイッターのおすすめユーザがうざい。

もうね、3年近く同じユーザがレコメンドされてるのよ。リアルの職場の知り合いだから絶対フォローを押したくないの。ずっと出てくるの。

ツイッター「フォロー押すまで、帰らない」って居座り続けるんですよ。ええ私は、永遠にフォローを押すことは無いんですけど。むしろレコメンドされるユーザーは、ブロックしてもいいと思ってたりする。

「おすすめユーザ」を消す

そこで、CSSセレクタのマッチングを工夫して「非表示」にすることを試みた。ツイッターdiv[data-testid="cellInnerDiv"] に全部のコンテンツが入ってるし、ReactなのでCSSクラス名も不安定。

そこでCSSのクラス名以外で該当ボックスにマッチさせる必要があります。これがずいぶんと長い間出来なかったんです。Xpathを使ってました。

最近になって:has() セレクタがまともに使えるようになりました。

これで「〇〇を含む親ボックス」が取得可能になり、CSSセレクタによるコンテンツブロックがはかどるようになりました。

おすすめユーザを非表示にする例

! 2023-01-08 https://twitter.com
twitter.com##div[aria-label="Timeline"]>div[data-testid="cellInnerDiv"]:has(div[aria-label^="Follow"][data-testid$="follow"])
twitter.com##div[aria-label="Timeline"]>div[data-testid="cellInnerDiv"]:has(a[href^="/i/connect_people?user_id="])

従来は、次のようなXPATHを書く必要がありました。

twitter.com##:xpath(//div[@data-testid="cellInnerDiv"][ .//span[contains(., "Who to follow")]])
twitter.com##:xpath(//div[@data-testid="cellInnerDiv"][ .//span[contains(., "おすすめユーザ")]])
twitter.com##:xpath(//div[@data-testid="cellInnerDiv"][.//span[contains(., "Who to follow")]]/following-sibling::div[position()<4])

div[following-sibling::div[position()<4] で隣接ノードを絞り込むほうが確実なんですよね。

ちなみに、メッセージを消す例

メッセージ(DM)のボックス消したいと思う人は多いのではないでしょうか。

twitter.com##div[data-testid="DMDrawer"]
twitter.com##div[data-testid="DMDrawerHeader"]

uBlockのフィルタで文字列(キーワード)を条件にブロックする

uBlock のブロックを要素のテキストでブロックする

これは、XPATHを使うとできます。

www.amazon.co.jp##:xpath(//div[@data-cel-widget][contains(.//span ,"再び購入")])

CSSブロックの問題点。

通常のublock は ドメイン、URLとCSSのクラス名で指定しますが、CSSのクラス名には限界がある。

また昨今のウェブサイトランダムはCSSクラス名が使われる。管理上cssnpm build されている。、CSSのクラス名がランダムでユニークなIDになっており、クラス名で指定すると、広告ブロックがあっという間に陳腐化(意味をなさなくなる)してしまう。

また、CSSブロックだと「どこをブロックした」かがわかりにくく、ブロックを間違えてたり、ブロックしすぎたときに除去するなどのメンテナンスが不便である。

キーワードで指定したい。

そこで、要素に含まれる文字列で指定したい。

たぶんこれが簡潔でメンテが用意だと思う。

しかし、CSSでテキスト文字列を指定するのは難しい。

かわりにXpathにある'contains()' 関数を使えるといいなと思ってたらuBlock origin はXpathをサポートしているとわかったので歓喜した。

xpath を記入する

f:id:takuya_1st:20211226162046p:plain

xpathブロックの記入例

ブロックのルールにxpathを書くときは、次のように書く。

:xpath(//element[contains(.//span ,"再び購入")])

ブロックに、xpathを書くときは、クォートとマーキングをちゃんとやる

##:xpath("//div") # ← クォートたらブロックルールの記述エラー
###:xpath ← `#` が多すぎ

正しい記述

##:xpath(//body//div)

xpath contains について

containsCSSではなくXpathで明示的に要素をセレクトするセレクターで使う関数である。

contains は慣れてないと使いにくいが次のようになっている。

contains( 対象ノード選択, 文字列 )

なので次のようになっている

contains( . , "オススメ商品" ) # カレントノードから下のすべてのテキスト・ノードを探す。
contains( ./span , "オススメ商品" ) # カレントノード直下の ./span に'文字列'が含まれているとマッチ
contains( .//span , "オススメ商品" ) # カレントノード子孫ノードに .//span に'文字列'が含まれているとマッチ

xpath と contains の関係

Xpathでは、条件にマッチしたノードを取り出す。そして選択時は選択条件を追記していく

//div # すべてのdiv

## この辺はCSSでもできそう
//div[@data-cwd]  ## <div  data-ads > とdivかつdata-ads属性があるもの すべて
//div[@data-cwd="youname"]  ## <div  data-cwd > とdivかつdata-cwd=younameであるもの すべて
//div[contains(@data-ads, "name" )]  ## div で 属性値チェック [data-cwd*="youname"]
//div[contains( ./text(), "name" )]  ## div のテキストノードで'name' があるか
## この辺はCSSでは無理そう
//div[contains( ./, "block" )][contains( ./, "detected" )]  ## div の文字列に 'block detected' の3つが含まれるもの
//div[contains( ./, "アド" )][contains( ./, "検出" )][contains( ./, "無効" )]  ## div の文字列に 'アド 検出 無効' の3つが含まれるもの

たとえば、Amazonのページ

「検索内容に関係したブランド」という迷惑なレコメンドがある場合。

##:xpath(//div[@data-cel-widget][contains(.//span ,"検索内容に関連したブランド")])

これでフィルタにマッチングさせて、わかりやすく文字列でフィルタを設定することができる。

このように、「表示されている文字」や「タイトルの文字」「文章」を元にブロックガッツリかけて便利です。 f:id:takuya_1st:20211226162103p:plain

xpathについて

XPATHについては、過去記事を参照してください。

過去記事

メールアドレスの入力が、@マークで分割された場合の簡単入力

誰が考えたか知らないが、謎文化のメアドを区切る文化

f:id:takuya_1st:20190706160501p:plain

入力がかったるくて仕方ない。思考するより速く入力したいスピード狂としてはストレスを感じる

ストレスを感じるより、プログラミングして解決したほうが気持ちがいい。

対策した

そこで、XpathBookmarklet を組み合わせて対処することにした。

(function(){
  maddr = []
  email = window.prompt("メールアドレス?", "");
  maddr = email.split(/@/);
  
  mail_inputs = [];
  ret = document.evaluate("//*[ count(./input[ contains( translate(@name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'),'email') ]) = 2 ]//input", document );
  while ((input = ret.iterateNext())) {
      mail_inputs.push(input)
  }
  mail_inputs.forEach( function(e,i){
    e.value = maddr[i%2]
  } )
})()

実行結果

メアドを貼り付ければ、簡単入力ができる。はー楽。

f:id:takuya_1st:20190706161757p:plain

f:id:takuya_1st:20190706161823p:plain

自動入力という概念がないSI'er世界。

これは愚痴なのですが、日本語の企業サイトには、「自動入力は悪」という概念があるようで。手入力より自動入力のほうが間違いが少ないっていう記述を見かける。 本当に何を考えているのかわからないし。フォームを分割したところで、ヒューリスティックな対処法でしかなく、経験則によりサポートコストが減るというのなら、リーンスタートアップ的にいえばA/Bテストで比較するべきなのである。フォーム項目を増やしても工数が増えるだけだし、RichUIとかUXとか言ってたりフォームをデザインするデザイナは、もっとA/Bテストをプッシュするべきだと思います。

関連記事

謎フォームの入力に苦労したので解決した話など

Xpath の contains で大文字小文字を区別しない(no case / insensitive match)

世の中には、@アットマークで、メアドを区切るという謎文化があります。

「メールアドレスをご入力ください」ただし、アットマークの前後を分けて。フォームの入力がすごくめんどくさいものがあります。

もっと手軽に入力したくてBookmarkletで対処するんだけど。大文字小文字を区別するのがめんどくさい

f:id:takuya_1st:20190706160501p:plain

*1

大文字小文字を区別しない、Xpathの例

$x("//*[ count(./input[ contains( translate(@name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'),'mail') ]) ]//input")

ポイント

ここで、先に大文字小文字を全部小文字にして丸める。

translate(@name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')

その後に、マッチさせる。

 contains( translate(@name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'),'mail')

これで、大文字小文字を考えないマッチングができる。

ほんと、Xpathって便利ですね。世間は、もうCSS セレクタばっかり使っってるだろうけど、私は、未だにXpath手放せないや

参考資料

https://stackoverflow.com/questions/8474031/case-insensitive-xpath-contains-possible

*1:こんなことをしてるの、日本語の大手企業のサイトだけで。これやるとIT能力低いことの暴露だと思うんですけど

Xpathで隣り合う前後要素を取得/ 兄弟・姉妹要素を取る

xpath で隣り合う要素を取得するには

妹(弟)ノードを取りに行く。

id("gaika_k")/following-sibling::div

あるノードの弟ノードをとるには、あるノードを指定してから following-sibling::タグ名とすれば取ることができる。

姉(兄)ノード

姉を取りに行くなら

id("gaika_k")/preceding-sibling::div

precedingを代わりに使えば同じように、上のノードが取れる。

xpath は成れてくるとCSSより細かい指定や適当な指定ができて、xpath書きやすい。

XPathで目的の要素を持つ親ノードから辿ることをする。

Xpath が便利なのがこれ

HTMLやテーブルで <a> を狙って取り出したいときに、複数取り出しちゃう。nth-child を使えば良いんだろうけど、Xpathの方がもうすこし柔軟に書けるので便利なんですよね。

<ul>
     <li><a></a>
     <li><a></a>
     <li><span><a></a></span>
     <li>
<ul>

これだと、2個めの a を狙うのは //li[2]/a になるんだけど。この書き方ではHTMLの構造でゴミが混じって意図したとおりにノードが取れないことが有る。

とくにいまどきテーブルレイアウトを使っていたり、意識他界系のマークアップで li と div が大量にあったりするともう大変。

○○を子ノードに持つXpathを条件する

子ノードを条件に持つXpath を使いつつ、インデックス指定をするときれいに取れてくる。

//li[ ./a ][2]/a  #=> a 

もし、うっかり インデックス指定を無精すると、配列になる。

//li[ ./a ]/a  #=> [a ,a]

もし、うっかり条件を甘くすると、もっと取れてしまって困る.

//li//a  #=> [a ,a, a]

Xpathは便利だけど奥が深いね。 まぁ contains を使うには最強ツールだと思うよほんと。

XSLTについてのまとめ

ちまちまxpathrubyで変換していくのが面倒になってきたので、XMLを変換するならXpath+nokogiriよりもしかしたら便利かもしれないXSLT使ってみた。

オワコン臭漂うXML界隈で、さらに忘れられた存在のXSLTです。。。

XSLT

XSLT ( Extensible Stylesheet Language Transformation ) です。XML Style Languaget and Tramsform 的に覚えていけばイイかと思います。

有り体に言えば、XSLTXMLを変換していくためのものです。大雑把なたとえではXMLにとってUnixパイプみたいなもん(?)

XML --> XSLT --> XML 

スタイルというわりにはCSSみたいなフォント指定だのボックスモデルなどはないわけで、XML->XHTML に変換するためのテンプレート。

XSLTXMLで記述されます。

XSLT で出来ること

XMLからXML(XHTML)を作る。HTML5は絶対に作れません。

巨大なXMLから必要データを取り出したXMLを作ります。

XSLT を使える場所

XML文書中に埋め込んで使う。

XML文書にXSLTの場所をURLで指定する

XSLT の変換エンジン(変換プロセッサ

事実上libxml/libxslt関連しかなく、XSLT3.0などは実装してる変換プロセッサはJavaのSAXONライブラリがあるくらい。

ある意味「枯れた」実装。枯れたというより死んでるけど、ブラウザ中で生きてる

今回はxsltproc を使うことにした。

xslt を使う

xsltproc でXMLXSLT処理するには、次のようにコマンドを指定する。

xsltproc  my.xslt input.xml > out.xml

xslt ファイルとターゲットのXMLを指定して実行すると、テンプレート適用されたXMLが得られる。

XSLTの基本構文

XSLTXMLなのでXMLとして記述する。XMLなので閉じタグ忘れや、要素を超えた閉じタグは許されない。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
 <!-- ここにかく -->
  </xsl:template>
</xsl:stylesheet>

XSLTでHTMLを出力するには

XMLを変換してHTML(XHTML)へ変換することができる。多分一番使うし一番便利。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" encoding="utf-8"  />
  <xsl:template match="/">
    <html>
      <body>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

output で method="html"で指定する。

これで、XMLではなく、HTMLで出力される。

単に<?xmlが出力されないだけなんだけどね・・・

ブラウザに変換させる

ブラウザでXMLを開けば、XSLTが実行されてHTML(XML)に変換される。

XMLの先頭にXSLTの場所を記述してやる

 <?xml version='1.0' encoding='UTF-8'?>
   <?xml-stylesheet type="text/xsl" href="./my-transform.xslt" ?>

ブラウザで処理してると変換結果のチェックが大変だったのでxmlproc のほうが楽だった。

XSLT の変換スタート

<xsl:template match="/">を書いたところから処理が始まる。

match="{xpath}" を書くことで、条件にマッチしたXMLノードで処理ができる。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/"><!-- ここから処理開始 -->
(中略
  </xsl:template>
</xsl:stylesheet>

さいしょは、<xsl:template match="/"> から </xsl:template>の間に処理を書くと憶える。

template は相互に呼び出しできる

apply-templateを使えば、テンプレの再利用や部品化ができる・・・けど

<xsl:template name="sidebar">
  <!-- サイドバーなんちゃら -->
</xsl:template>
<xsl:template match="/menu">
  <xsl:apply-templates name="sidebar"/>
</xsl:template>

一見すると便利そうだけど、ややこしいXSLT内部を飛び回るGOTOと同じ、GOTO地獄になったのでおすすめしない。

ループ

ループは for-each を使う。

<xsl:for-each select="//data"><!-- for-each .year/ -->
  <div class="year_list" id="year_{@year}"><!--- ここはカレントノードが data -->
  </div>
</xsl:for-each><!-- for-each ./year -->

for-each内部は、カレントノードを "."(ドット) 参照できるし省略可能。

上記の例だと //dataにループを回して、 //data/@year を出力している。divが //dataの個数だけ出来る。

テキストノード作成

XSLT でテキストノードを作る

出力でテキストノードをつくるにはvalue-of を使う。select にはカレントノードからみたXpathを使う。

<xsl:value-of   select="@attr_name" />
<xsl:value-of   select="@path/to/node" />
<xsl:value-of   select="@substring(path/to/node,10)" />

スペースを入れたい

スペース(空白)いれるのがめんどくさいので最初に覚えておく。

 <xsl:text>&nbsp;</xsl:text>

XMLなのでスペースは読み飛ばされる。なので、あえてスペースを記述する必要がある。しかもXMLノードとして。(めんどくさい・・・

他にも

<xsl:text>&#xa;</xsl:text><!-- 改行 -->
<xsl:text>&#x9;</xsl:text><!-- タブ-->
<xsl:text>&#x20;</xsl:text><!-- スペース-->

なども必要になることが多い。

属性値 を出力

出力XMLのノード属性値を出力するには、 変数に一旦格納する。

変数に格納したら {$image-title} のように

ただし、ダブルクオーテーション中なら、"{@width}" な "{@name}"のように埋込できる。

<xsl:variable name="image-dir">/images</xsl:variable>

<xsl:template match="photograph">
<img src="{$image-dir}/{href}" width="{size/@width}"/>
</xsl:template>

これで

<a href="リンク" >なまえ</a>

のようなXHTMLを作成できる

文字列の 置換

translate が一番カンタン

<a href="{translate(@filepath, '\', '/')}"><xsl:value-of select="@title" /></a>

XSLTでif を使う

条件分岐もできる

Xpathでするには、if すらOpen/Closeをいしきする

for-each 中で奇数偶数で一旦div を閉じるとか、そういうことは出来ない。

        <xsl:for-each select="//book">
            <xsl:if test="position() mod 3 = 0 ">
              <div>
            </xsl:if>
            <img src="{@coverImage}" alt="{@month}" width="200px" />
            <xsl:if test="position() mod 3 = 2 ">
              </div>
            </xsl:if>
        </xsl:for-each>

div と if が XMLとしてネストしちゃってるので、アウト

カンタンにはifできない。XSLTXMLとして定義されているので、XMLとしてただしくないので出来ない。めんどくさい for-each毎に、Xpathで同時に3個取り出せばできるらしい。

解決策はいくつかある。 disable-output-escaping="yes"を使う。CDATAを使う。クエリを工夫して3つ毎のノードでループ3つ同時に取り出す。ApplyTemplateを使うなど

if に関する解決策 ↓

並び替え

Xpathの結果を並び替えてループできる

<xsl:for-each select="//data"><!-- for-each .year/ -->
  <xsl:sort select="position()" data-type="number" order="descending"/>
</xsl:for-each><!-- for-each ./year -->

この場合、//dataを降順に並び替えている。なぜか for-eachの開始直後に書くことで並べられる。

ひどい仕様だ。。。

Xpathの比較演算子はどうするのか

最後の1件だけを表示したくない時。

        <xsl:for-each select="(//book)[position() &lt; count(//book) ]">
        </xsl:for-each>

&でエスケープして入れます。

・・・ひどい仕様だ

XSLTの変換速度

数GB級の巨大なXMLを扱うと遅い

takuya@:~/Desktop$ ls -hl main.xml
-rw-r--r-- 1 takuya staff 165M  9  2 11:16 main.xml

takuya@:~/Desktop$ time xmllint --noout  main.xml

real    0m3.573s
user    0m3.112s
sys    0m0.444s

takuya@:~/Desktop$ time xsltproc  test.xslt main.xml  > /dev/null

real    0m6.865s
user    0m6.204s
sys    0m0.586s

予想通り、メモリに乗るならそこそこ我慢できる速度になる。 メモリ量の30%くらいじゃない?限界は。 巨大なXMLを処理するのは不可能

メモリ8GBのMBPで、5GBで数分掛かってギブアップする

やっぱり、巨大データ処理を手元でやるならインデックスつけたRDB(SQL)が最強

match="/" から始める。必ず。

match="/" を書かなかった場合

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/rss/item"><!-- match="/" がない場合-->
(中略
  </xsl:template>
</xsl:stylesheet>

目的のノード以外に余計な情報がおおいと、ついつい目的ノードXpath書いちゃうけどそれはダメ。

XML 文書中の /* のノードのうち、match="/rss/item"のみ変換され、それ以外はテキストとして処理される(xmlprocの場合)

参考になりそうなのもの

Xpathまとめコマンドとその使用例・チートシートでカンタンに実験する

はじめに。

xpath 便利です。

Xpathの書式を学ぶにはトライ・アンド・エラーが1番です。Xpathをもっと使うのためには、手軽なツールが必要です。

libxml のxpathコマンド

手軽なツールはlibxmlについてくるxmlintです。xmllint についてはココに書きました→xmlを扱う xmllint コマンドで xpath - それマグで!

しかし、xmlint では日本語の取扱が残念なので、xpathコマンドをnokogiriで再発明しました。

足りないので、自分で作った。

Xpathの便利さをもっと気づいて欲しい。なので手軽にXpathを試して練習したい。手軽に試したら、Xpathできたら楽しいよね。

学習用に、Xpathコマンドを作りました。

で、お手軽にXpathを作れるような方法を考えた結果、Nokogiriを利用したコマンドを作ることにした。

xpath · GitHub

ダウンロード
git clone git@gist.github.com:894c5aeabc620344bcea.git
cd 894c5aeabc620344bcea
cp xpath /usr/local/bin/
chmod +x /usr/local/bin/xpath

起動には、ruby のnokogiriが必要です。

sudo gem install nokogiri
OSX 標準のxpath/xlimt 関数は日本語の扱いが残念

xpathOS Xには標準添付です。しかし、日本語が残念です。

わざわざ作ったのは、OSXxpathは日本語の扱いが残念だったのでした。/usr/bin/xpathPerlで書かれているので、Perlを編集したらいいんだろうけど、まぁOSX標準添付を編集するのも依存増えるからいや。なので、じぶんでつくることにした。

ruby + libxml なら他への移植も簡単だし。

Xpath のサンプル

実験環境が整ったら、Xpathについて見ていきたいと思います。

Xpath は次の書き方を覚えておけば戦える。

//*         All nodes.
(//*)[1]    First of all node.
(//a[1])    A Node list of "First child" in a parent node.
//a//span   A span node ancestor of a.
//a/@href   An  attribute  named href in all a node.
//a[@href="/index.html"] A node with href  attribute is  "index.html".
//a[contains(@href,"index.html")] A node contains  "index.html" in its href.
//title | //meta  Node list of <title> or <meta> nodes.(Join result)
//img[ contains(@src,'jpg') or contains(@src,'png')]  Nodes with attributes has some keyword.
//form[ //input[name="username"] ]    A node which has a node of pointed by xpath.
//div[@id=main]//form   A node ancester of node.
//div/*   A node list of children of div.
//div//*  A node list of ancesstor of div.
//table//td[2] A td node second child of parent which in a table node.

xpath チートシート

xpath 内容
//* 全てのノード
//a 全ての<a> ノード
(//a)[1] 全ての<a> ノードを取得して、最初の1個
(//a)[2] 全ての<a> ノードを取得して、2番め(配列アクセス)
(//a[1]) 親ノード中の最初の1個の<a>をすべて
//a/span span ノードで、親が<a>のものをすべて
//a/@href aノードのすべてのhref属性
//a/text() aノードのすべてのtext()表現
//a[@href="/index.html"] aノードのうち href属性が"/index.html"と合致するモノすべて
//a[contains(@href,"index.html")] aノードのうち href属性に index.html を含むものすべて
//title | //meta title と meta タグを両方
//img/@alt | //img/@alt img@alt と img@src を複数同時に
//img[ contains(@src,'jpg') or contains(@src,'png')] img ノードで src に jpg/pngを含むものすべて
//div[ contains(@class,'link') and contains(@class,'book')] div.link.bookに相当するもの
//form[ ./input[name="username"] ] 子ノードに //input[name="username"]を持つform ノード
//div[@id=main]//form form ノードで、親が div[@id=main] の物をすべて
//div/* div の子要素すべて
//div//* div の子孫要素をすべて
//table//td[2] table タグで2番めのtdのもの(2列目をすべて)
//*[@id] id属性があるものをすべて
id("tid_123") id属性がtid_123のもの(id="tid_123")
id("tid123")/following-sibling::div #tid123に隣接する div
//tbody//tr[ position() > 2 ] tbody中のtrで3番目以降もの(Range:範囲指定)
//li[.="次へ"] textContent が"次へ" に完全一致(equal) するもの
//li[not(@data-aria)] not で否定

これだけあれば、8割の場面で戦えると思う。

xpath のコマンド

たとえば、このブログにどれくらいA要素が含まれるか

xpath http://takuya-1st.hatenablog.jp/  "count((//a))"
137.0
--------------------
 xpath: count((//a))
result: Float

たとえば、このブログのmeta要素を取り出すと・・・

xpath http://takuya-1st.hatenablog.jp/  "/html/head/meta"
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=7; IE=9; IE=10; IE=11">
<meta itemprop="name" content="それマグで!">
(中略)
<meta name="google-site-verification" content="h5FlKu0qSTuoG6mttmTa0x1d0Nq9-Z2XmBMnkYI8hsU">
--------------------
 xpath: /html/head/meta
result: 19 found

たとえば、curl の結果などと組合せるには

curl -s http://www.drk7.jp/weather/ | ./xpath "//a[text()='大阪府']"
<a href="/weather/xml/27.xml">大阪府</a>
<a href="/weather/json/27.js">大阪府</a>
<a href="/weather/xml/pollen/27.xml">大阪府</a>
<a href="/weather/json/pollen/27.js">大阪府</a>
--------------------
 xpath: //a[text()='大阪府']
result: 4 found

この実験したソースはコレ。

gist.github.com

では残りの二割は?

  • 子孫ノード、祖先ノード
  • 兄弟ノードの選択
  • 文字列関数
  • ノード関数

などの各種セレクタと関数とを組み合わせると、更に戦える。

祖先ノード

ノードをツリーのルート方向へ上位へ辿って探していく

たとえばfooterを含むセクションを探すには

//footer/ancestor::section
兄ノードを辿る

たとえば、アクティブなli#active のひとつ前のli

<li> ←ここ
<li id="active">
<li>

これを選ぶには

//li[@id='active']/preceding::li

便利ね.ただし liタグが複数アレば全部取れちゃうので絞ること

弟ノードを取り出す。

選択しているノードの1つ後ろを取り出すには

<li>
<li>
<li>
<li id="active">
<li>←ここを取り出す。

これを選ぶには

//li[@id='active']/following::li

便利ね.ただし後ろに liタグが複数アレば全部取れちゃうので絞ること

自分上位の「祖先」を辿る

//input[name='user']//ancestor::form

兄弟ノードで後続3つを選ぶ

条件に div[position()<4] マッチしたもの。を取り出す。「範囲指定」で配列から取り出す。

div[@class]/following-sibling::div[position()<4]

テーブルの指定カラムを取り出す。

cat out | xpath '//tbody/tr/td[1]/text() | //tbody/tr/td[2]/text() |  //tbody/tr/td[3]/text() '

このようにすれば、並び順を維持したまま、テーブルの中身を取り出せる。

Xpathだってデータベース

XPathはとても便利で、HTML・XMLを扱うのには必須のツール。

  • テキストファイルを検索するのにgrep
  • テーブルを検索するのにSQL
  • JSONを検索するのに jq
  • XMLを検索するのには Xpath

Xpathはクエリ、XMLはテーブルといえる。XMLだって立派なデータベースになると思います。

xpath 扱えるとyamlよりずっと便利ですよ。yaml ならJSONのほうが楽だし。

関連資料

XPATHについて。XMLのXpathで条件にマッチしたノードを取り出す。 - それマグで!

document.evaluate でXPATH する具体的サンプル - それマグで!

Xpathで「条件◯◯」を子孫ノードに持つ要素を選択する - それマグで!

文字を含むタグを取り出すxpath - それマグで!

2015-08-03 重ねがけ出来るようにした

ページ階層をたどってURLを抽出するときに不便だから、重ねがけ出来るようにした。

xpath http://www.yahoo.co.jp "//a/@href" | xpath "//a/@href"|xpath "//a/@href"

ただし、絶対URLしか対応できない。。。。

2015-09-02 追記

xpath 表現でid() を知ったのでサンプルに追記

2015-12-02 追記

Google Sites: Sign-inのサイトに再見できたので、追記。

2018-02-17 追記

サンプルを拡充

2022-05-10

textContent へのequal(= 完全一致)のサンプル '//li[.="次へ"]/span' を追加

2023-05-23

記述修正。

xmlを扱う xmllint コマンドで xpath

xpath 大好きっ娘 な takuya です。こんばんは。

xpath をもっと手軽に扱いたい

xpath を扱うには、libxm2やlibxsltをrubypython から使ってたんですが。phpxpath は微妙だし。

コマンドラインから使おうと思ってコマンド書き始めてたら、すでにコマンドが用意されてるんですね。

xmllint コマンド

xmllint コマンドというものがあります。引数に --xpath をつけるとxpathを使えます。

takuya@rena:~/Desktop$ echo "<a><b><c>cccc</c></b></a>" | xmllint  --xpath "//c/text()"  - 
cccc

linuxOSX は標準装備

osx の場合は、最初から入ってる

takuya@osx:~/Desktop$ which xmllint
/usr/bin/xmllint

debianなどLinuxでも最初から入ってる。

takuya@debian:~$ which xmllint
/usr/bin/xmllint

どの環境でも使えるならとっても便利ですね!。

HTML を使う

html を扱うときは --html をつける

xmllint  --html  --xpath "//input"  -

HTMLは厳格な仕様で書かれていることが少ないので、Warningが大量に出る。これはSTDERRに出るので無視できる。

xmllint  --html  --xpath "//input[@name='post_id']/@value" - 2>/dev/null

2014-09-05 修正

標準入力 STDIN から受けるときは、 - を末尾に置かないと動かなかったの

2023-05-23 修正

html を扱うときにフラグが必要になっていた。

みんな大好き!man ページ

xmllint はその名の通りlint なので、奥が深いプログラムですね。

Xpathで「条件◯◯」を子孫ノードに持つ要素を選択する

Xpathは慣れてくるとパズル・ゲームみたいで私は大好きです。

Xpathで条件としてある子孫ノードをもつ要素を探す。

古臭いテーブル・レイアウトのHTMLをスクレーパーするときに大活躍します。

Xpathの条件としては書き方もいろいろあげられますが、今回は、絞り込みを [ ] で行う例です。

form 要素を持つ Tableを探す。

子ノードにformを持つTableを探す

//table[.//form]"

子ノードにformがaction=post.php を持つTableを探す。

//table[.//form[@action='post.php']]"

子孫ノードに form と button を持つ tableを探す。

//table[.//form[@action='post.php'] and .//button ]"

条件の書き方を覚えるとXpathが楽になる。

/element[ 条件 and 条件 ]

これが基本的な書式だと思います。

あとは、この条件を併せて書きます。

/element[ 条件 and 条件 ]/element[ 条件 and 条件 ]

まずは、この基本形を覚えたらXpathで戦えると思います。

参考資料

document.evaluate でXPATH する具体的サンプル - それマグで!

XPathで何番目を示すための方法(注意点

Xpathで何番目の要素を示す

一般的には次のように紹介されてることが多い。

/node/element[1]
/node/element[2]
/node/element[3]
/node/element[4]

これは、「兄弟ノード」siblingから選んでるところに注意が必要

兄弟として要素が並んでいるところから取り出す。

<root>
  <anchor> <em></em> </anchor>
  <anchor> <em></em> </anchor>
  <anchor> <em></em> </anchor>
  <anchor> <em></em> </anchor>
</root>      

XML中で要素が並んでいる時に使える。

また、複数の要素をタグ名関係なしに選びたいときは

/element/*[1] #=> 1つ
  • を使えば、並んでるところを同じ兄弟とみなすことが出来る。

siblingsとして並んでない場合は注意

強欲に "//*" を使っている場合は、n番目の指定は注意が必要。

//a
//a[1]

上のように、子孫ノード全部から選んだ場合は、子孫ノードで 並んでいるところの先頭を全部取り出す。

<root>
  <a></a><!-- ここ は1番目 -->
  <a></a>
  <a></a>
  <list>
    <a></a><!-- ここ は1番目 -->
    <a></a>
    <a></a>
  </list>
  <list>
    <a></a> <!-- ここ は1番目 -->
      <list>
        <a></a><!-- ここ は1番目 -->
        <a></a>
        <a></a>
      </list>
    <a></a>
    <a></a>
  </list>
</root>

なので4つノードが取得される。

//a[1] #=> 4つ

xpath の【a[1]】 は実行結果に対して、1番目のノードを選んでいるのではなく、検索条件であることをもう一度意識したら間違わないと思います。

参考資料

http://blog.bangboo.com/sub/xpath.html