⤴️

PowerShell向けの自作HTMLパースモジュールをアップデートした

に公開

はじめに

AngleParseは、HTMLをパースするためのPowerShellモジュールとして以前開発したものです。簡単なスクレイピングをPowerShellで実現したいという思いから、さらに自分好みのAPIを追求する形で開発を進めました。完成後、約5年間放置していましたが、先日大幅なアップデートを実施しました。そこでこの度、以前Qiitaに投稿した紹介記事の更新を兼ねて、AngleParseそのものと今回のアップデート内容を説明するために新しく記事を作成しました。

概要

AngleParse は、.NET向けの信頼性の高いHTMLパーサーライブラリ AngleSharp を土台に C# で実装されています。最大の特徴は、HTML のパースから処理までをパイプラインとして宣言的に定義できる点です。たとえば、はてなブックマークからエントリを抽出するコードは以下のようになります。手前味噌ながら、宣言的な記述でスクレイピングのワークフローをシンプルに表現できているかと思います。

Invoke-WebRequest 'https://b.hatena.ne.jp' |
Select-HtmlContent 'div.entrylist-contents' @{
  Bookmarked = 'span.entrylist-contents-users > a > span', { [int]$_ }
  Title = 'h3.entrylist-contents-title > a'
  Url = 'h3.entrylist-contents-title > a', ([AngleParse.Attr]'href')
  Time = 'li.entrylist-contents-date', ([regex]'\S+ (\d{2}:\d{2})'), { [TimeOnly]$_ }
  Tags = 'a[rel=tag]'
} {[pscustomobject] $_ } | select -first 2
Tags       : {人生, あとで読む, ドイツ, 食…}
Time       : 11:41
Bookmarked : 321
Url        : https://note.com/goodjoshi/n/n65ba3d0830a6
Title      : ドイツの肉屋で1年間働いてみた|ソーセージ姉さん

Tags       : {文章, あとで読む, 教育, 脳…}
Time       : 6:23
Bookmarked : 288
Url        : https://togetter.com/li/2547331
Title      : 「文字は読めるが文章が読めない」は「機能的非識字」といって、世界中で問題になっている。日本では...

公開後は長らく開発をしていませんでしたが、その間に内部で利用している AngleSharp が大幅にバージョンアップしてユーザーから依存関係の更新の要望をいただいたこと、さらに旧コードベースの品質が酷く、大規模なリファクタリングを自分自身でも行いたいと感じていたことから、この度、大幅なアップデートを実施しました。この記事ではまず、初めてAngleParseを使う方向けに導入から基本的な使い方を解説します。続いて、既存ユーザー向けに今回のアップデートポイントをまとめてご紹介します。

導入方法

対応バージョン
PowerShell 7.4 以降

PowerShell Gallery にモジュールを公開しているので、そちらからインストールしてください。
実行前にはモジュールがインポートされている必要があることにも留意してください。

# モジュールのインストール
Install-Module AngleParse

# モジュールのインポート
Import-Module AngleParse

使用方法

概念

AngleParseのパイプラインは、ステップごとに1つの入力を受け取り、複数の結果(0個や1個以上)を出力します。これらの出力単位を「セレクター」と呼び、各段階での出力はステップごとに平坦化され最終的に一次元の配列として出力されます。パイプラインの終端ではPowerShellのパイプの挙動をエミュレートし、出力が0個の場合は$nullを返します。また、1個だけの場合は配列ではなくその要素だけを返します。この仕組みにより、配列を意識せず情報を抽出可能です。

また、セレクターが受け取る入力を「リソース」と呼びます。リソースには、DOM要素のElement型、文字列のString型、オブジェクトのObject型があり、これらはObject <- String <- Elementの継承関係で扱われます。各セレクターには入力できる型と出力する型が定義されているため、適合しない型どうしをつないだパイプラインは構築時にエラーになります。

抽象的な説明だけではわかりにくいこともあるため、よりイメージしやすいように、共変と反変をサポートしつつ簡潔なScalaでの抽象コード例を以下に示します。

type Selector[-In <: Out, +Out <: ObjectResource] = In => Seq[Out]

sealed class ObjectResource(val obj: Any)
class StringResource(val str: String) extends ObjectResource(str)
final class ElementResource(element: IElement) extends StringResource(element.textContent)

object Selector:
  def connect[LIn <: LOut , LOut <: RIn, RIn <: ROut, ROut <:ObjectResource]
    (lhs: Selector[LIn, LOut], rhs: Selector[RIn, ROut]): Selector[LIn, ROut] =
      (x: LIn) => lhs(x).flatMap(rhs)

セレクターについて

AngleParseには6種類のセレクターがあり、それらを組み合わせることでシンプルに情報を抽出できます。もし定義済みのセレクターだけでは不足する場合、内部で使われているAngleSharpのIElementにも直接アクセスできる回避策が用意されています。ここでは、それぞれのセレクターとその特徴を順に解説します。

1. CSSセレクター

Element -> Element
CSSセレクターはDOM要素を受け取り、指定したCSSセレクター式に一致するDOM要素を出力します。単なる文字列として与えられたものは、すべてCSSセレクターとして解釈されます。

<div>
  <span class="foo">text content here</span>
</div>
Get-Content css_selector.html -raw |
  Select-HtmlContent "div > span.foo"
# Output: 'text content here'

2. 属性セレクター

Element -> String
属性セレクターはDOM要素を受け取り、一致した属性を文字列として出力します。
以下のように、あらかじめ定義された属性があります:

  • Href
  • Src
  • Title
  • Name

他の属性にアクセスする場合は、文字列を [AngleParse.Attr] クラスで変換することで独自の属性セレクターを定義できます。(例: ([AngleParse.Attr]'some-attribute'))値のない属性にアクセスすると空文字列が返され、存在しない属性を指定すると $null が返されます。

<a href="https://example.com" some-attribute="hey"><span>some link</span></a>
Get-Content attribute_selector.html -raw |
  Select-HtmlContent ([AngleParse.Attr]::Href)
# Output: https://example.com

Get-Content attribute_selector.html -raw |
  Select-HtmlContent ([AngleParse.Attr]'some-attribute')
# Output: hey

また、実際には属性ではないもののHTMLの処理に便利な特別な属性セレクターも含まれます。
以下がその一覧です:

  • InnerHtml - 要素のinner HTML
  • OuterHtml - 要素のouter HTML
  • TextContent - 要素のテキスト
  • Id - 要素のID
  • ClassName - 要素のクラス名
  • SplitClasses - スペース区切りで分割したクラス名の配列

3. プロパティセレクター

Element -> Object
プロパティセレクターはDOM要素を受け取り、内部に保持している AngleSharp.Dom.IElement のプロパティの値を動的に参照して出力します。これはDOM要素の IElement プロパティにアクセスしたい場合に便利です。文字列を [AngleParse.Prop] クラスで変換することでプロパティセレクターを作成できます。(例: ([AngleParse.Prop]'some-property')

<div><span class="foo">text content here</span></div>
# 本来、[AngleParse.Attr]::TextContent を使うべきです。
# これは単なる例です。
Get-Content property_selector.html -raw |
  Select-HtmlContent ([AngleParse.Prop]'TextContent')
# Output: text content here

属性セレクターと同様に、このカテゴリにも特別なプロパティがいくつかあります。
以下がその一覧です:

  • Element - DOM要素のAngleSharp.Dom.IElementそのものを返す
  • AttributesTable - DOM要素の属性一覧の辞書

4. 正規表現セレクター

String -> String
このセレクターは文字列を受け取り、キャプチャした文字列を出力します。DOM要素を渡した場合は、その要素のテキストに対してキャプチャを行います。Regex型の値は正規表現セレクターとして解釈されます。

<div><span>2020/07/22</span></div>
Get-Content regex_selector.html -raw |
  Select-HtmlContent ([regex]'(\d{4})/(\d{2})')
# Output: 2020, 07

5. スクリプトブロックセレクター

Object -> Object
このセレクターは任意のオブジェクトを受け取り、スクリプトブロックの結果を出力します。抽出したデータを処理したい場合に便利です。スクリプトブロック内ではPowerShellの慣習通り、$_で入力されたオブジェクトを参照できます。DOM要素をこのセレクターに渡した場合、その要素のテキストに対し処理を行います。

<span class="some-date">2025/05/04</span>
Get-Content scriptblock_selector.html -raw |
  Select-HtmlContent { [DateTime]$_ }
# Output: 2025/05/04 0:00:00

6. テーブルセレクター

T -> Object : Tは各枝で要求される入力の型の中で最も厳しい型
テーブルセレクターは、与えられたキーと値の組み合わせをハッシュテーブルとして出力します。各値は対応する枝で指定されたパイプラインによって処理された結果です。このセレクターに入力を与える際は、枝の中で最も厳しい入力型を想定する必要があります。AngleParseでは、ハッシュテーブルをテーブルセレクターとして解釈します。

<body>
  <div class="a">
    1a
  </div>
  <div class="b">
    2b
  </div>
</body>
Get-Content table_selector.html -raw |
  Select-HtmlContent @{
    ClassName = ([AngleParse.Attr]::ClassName);
    NumPlus1 = ([regex]'(\d)\w'), { [int]$_ + 1 }
  }
# Output:
# ClassName Number
# --------- ------
# a         2
# b         3

# これはエラーが発生します。
# 理由として、入力型が string である一方、
# 各枝で要求される最も厳しい入力の型は string のサブタイプである Element であり、
# 型制約に合わないためです。
Get-Content table_selector.html -raw |
  Select-HtmlContent ([regex]'.*') @{
    ClassName = ([AngleParse.Attr]::ClassName);
    NumPlus1 = ([regex]'(\d)\w'), { [int]$_ + 1 }
  }

今回のアップデートについて

今回のアップデートでも「パイプライン処理」という基本の部分は変わりませんが、型安全な処理や新しいセレクターの追加など、大幅な改善が施されています。改めて利用方法をご確認いただくのを推奨しますが、以下では、今回の主な変更点を簡潔にご紹介します。

内部リファクタリング:型安全なパイプライン

パイプラインの内部処理ロジックが再設計され、パイプライン構築時に型の整合性が保証されるようになりました。この改善により、パイプラインの組み立てに伴うエラーを実行時ではなくビルド時に発見できるようになりました。

APIの変更

a. -Selector パラメーターの指定の仕方の変更

-Selector パラメーターは ValueFromRemainingArguments 属性 をサポートするようになりました。この変更によって、セレクターの指定はカンマ区切りではなくスペース区切りになります。

# Previous syntax
'content' | Select-HtmlContent 'div', 'span'
# New syntax
'content' | Select-HtmlContent 'div' 'span'

b. [AngleParse.Attr]::Element の廃止

以前は object を返していた [AngleParse.Attr]::Element 属性は、今回の型安全なパイプラインの導入に伴い廃止されました。内部の IElement オブジェクトにアクセスする場合は、新しい [AngleParse.Prop]::Element プロパティを使用してください。

c. プロパティセレクター の追加

新たに プロパティセレクター が導入され、内部の IElement プロパティに、より柔軟かつ動的にアクセスできるようになりました。詳細については、該当項目を参照してください。

d. その他

  • [AngleParse.Attr]::Class[AngleParse.Attr]::ClassName に変更しました。
  • 全てのパラメーターはポジショナルパラメーターでなくなりました。

Discussion