すべてのページにブラウザーが必要なわけではありません。必要なデータがサーバーレンダリングされたHTMLの中にあるなら、http.get で取得してCSS selectorで分解できます。Chromiumも、モデルも、記録されたブラウザーセッションも不要です。std/html には、そのための3つのpure Toolが含まれています。
Tool使う場面
html.extractstring[]見出し、価格、hrefなど、繰り返し現れる1種類の値が欲しい場合。
html.tablestring[][]セルの行を取得し、下流で列をindex指定して選ぶ場合。
html.recordsobject[]列ヘッダーをキーにした行が欲しく、名前でfilterしたい場合。
3つとも capabilityなしkind: "transform" として宣言されています。HTML文字列を受け取り、データを返すだけで、ネットワーク、ファイルシステム、時計には一切触れません。pureなので、エンジンはこれらをJournalに記録しません。同じ htmlselect が与えられれば毎回同じ結果を生成するため、リプレイでは記録済みの値を再生するのではなく、parseを再実行します(ToolとPackを参照)。記録されるのは、http.get 経由で取得したページそのものであり、そのparse結果ではありません。
SDKは、この3つのTool専用のbuilderをまだexportしていません(@browserflow/sdkhttp / browser / transform helperがある http.get / http.postbrowser.tooltransform とは異なります)。下に示すように、YAMLから use: html.extract / html.table / html.records で参照してください。fluent SDK builderでFlowを作成している場合は、生の { use: "html.extract", withRaw: {...} } as StepDef というescape hatchがサポートされていると考える前に、SDK ownerに確認してください。これはまだdocumented APIではありません。

アンカーとなるrecipe: recipe substitutions

examples/01-recipe-substitutions は、このrepo内で最小の、完全なブラウザーなし・AIなしのFlowです。HTTPでrecipe pageを取得し、1つのCSS selectorから本文を取り出し、実際にsubstitutionの名前を挙げている文だけを残します。
flow.yaml
flow: recipe-substitutions
steps:
  - id: page
    use: http.get
    with:
      url: https://www.bbcgoodfood.com/recipes/keto-pancakes
  - id: notes
    use: html.extract
    with:
      html: ${{ steps.page.output.body }}
      select: .editor-content
  # このrecipeは置き換えを本文中に書いているため、substitutionを名指しする
  # 文だけを残し、出力を絞り込まれた決定論的なリストにする。
  - id: subs
    use: transform
    with:
      expr: 'filter(input.notes, contains(item, "instead"))'
      input: "${{ { notes: steps.notes.output.values } }}"
output: ${{ steps.subs.output.value }}
3手順で、ブラウザーもモデルも使いません。http.get がページ本文を一度Journalに記録し、html.extract が各実行(またはリプレイ)でそこから本文を決定論的に再導出し、transform がexpression languageでfilterします。同じ入力なら、バイト単位で同じ答えになります。これがruntimeの要点です(実行を参照)。

html.extract — 選択して投影する

html.extract はHTML文字列に対してCSS selectorを実行し、matchした各要素を文字列へ投影します。対象はそのtext、href、または任意の属性です。
- id: notes
  use: html.extract
  with:
    html: ${{ steps.page.output.body }}
    select: .editor-content
    # as: "text" がdefault。href/attrが必要な場合以外は省略する。
- id: headlines
  use: html.extract
  with:
    html: ${{ steps.page.output.body }}
    select: "article h2 a"
    limit: 10

- id: links
  use: html.extract
  with:
    html: ${{ steps.page.output.body }}
    select: "article h2 a"
    as: href
    limit: 10
入力備考
htmlstringparseするマークアップ。通常は ${{ steps.<fetch>.output.body }} です。
selectstringCSS selector。tag、class、id、attribute、またはdescendant combinatorを指定できます。
as"text" | "href" | "attr"任意。defaultは "text" です。"href"href 属性を読みます(存在しない場合は空文字列)。"attr"attr で指定した任意の属性を読みます。
attrstringas: "attr" の場合は必須です。指定しないと、このToolは as:"attr" requires a non-empty attr をthrowします。
limitnumberdocument orderで、最初のN件のmatchだけを残します。
出力: { values: string[] }。matchした要素ごとに1つの文字列を、document orderで返します。
html.extracttrim済み のtextを返しますが、html.table / html.records のheader keyのように内部のwhitespaceをcollapseしません。selectorが複数行の本文にmatchする場合、values には埋め込まれた改行が含まれると想定してください。1行のtextが必要な場合は、下流の transform でfilterするか replace してください。

html.table — セルの行

html.tablehtml.extract の2次元版にあたるToolです。行を選択し、次に各行内のセルを選択して、セル配列のリストを返します。列は下流でindex指定して選びます。
YAML
- id: table
  use: html.table
  with:
    html: ${{ steps.page.output.body }}
    select: "table.scores tbody tr"   # row selector、defaultは"tr"
    cell: "td"                        # cell selector、defaultは"td, th"

- id: names
  use: transform
  with:
    expr: 'map(input.rows, nth(item, 0))'
    input: "${{ { rows: steps.table.output.rows } }}"
入力備考
htmlstringparseするマークアップ。
selectstringrow selector。defaultは "tr" です。
cellstring行内のcell selector。defaultは "td, th" です。
as / attrhtml.extract と同じ各行ではなく、各 cell を投影します。
limitnumber最初のN行だけを残します。
出力: { rows: string[][] }
select にmatchしても、そのセルが cell にmatchしない行は 丸ごとdrop されます。rows 内に空配列として現れることはありません。<th> のheader rowは、cellth にmatchする場合にだけ保持されます(defaultの "td, th" はmatchしますが、より狭い cell: "td" では静かにdropされます)。そのため、明示的な header selectorを渡さない場合、html.records は「セルを持つ最初の行」をheaderとして扱います。詳しくは下を参照してください。

html.records — headerをキーにした行

html.recordshtml.table のキー付き形式です。各行のセルを列ヘッダーと対応付け、objectの配列を返します。そのため、数値indexではなく、名前付きの列でfilterできます。
YAML (explicit header selector)
- id: games
  use: html.records
  with:
    html: ${{ steps.page.output.body }}
    select: "table.games tbody tr"
    header: "table.games thead th"

- id: cheap
  use: transform
  with:
    expr: 'filter(input.games, to_number(item.metascore) < 60)'
    input: "${{ { games: steps.games.output.records } }}"
YAML (header inferred from the first row)
# `header:` なし。最初にmatchした行がkeyを提供し、
# dataとしてemitされるのではなくheaderとして消費される。
- id: games
  use: html.records
  with:
    html: ${{ steps.page.output.body }}
    select: "table.games tr"
入力備考
htmlstringparseするマークアップ。
selectstringrow selector。defaultは "tr" です。
cellstring行内のcell selector。defaultは "td, th" です。
headerstringheader cell用の任意selector。指定した場合、matchした要素の text がcolumn keyになり、select にmatchしたすべての行がdataとして扱われます。省略した場合、少なくとも1つのセルを持つ、matchした 最初の 行がkeyを提供し、dataとしてはskipされます。
as / attrhtml.extract と同じ各cellの value を投影します(header textではありません。header textは常にplain textです)。
limitnumber最初のN data rowだけを残します。
出力: { records: Array<Record<string, string>> }
Header textはkeyになる前に replace(/\s+/g, " ").trim() でnormalizeされます。" Meta Score \n""Meta Score" になります。2つのheader cellが同じkeyにcollapseされる場合、後続のものにはdocument orderで数値suffixが付きます。colcol_2col_3、… のようになります。そのため、空の <th> が2つあるtableでは """_2" というkeyが生成されます。重要なunlabeled columnがあるマークアップでは、上流でrenameする価値があります。
ある行が select にmatchしても、その子要素が1つも cell にmatchしない場合、その行は何も生成しません。空のrecordすら生成しません。
header keyより多くのcellを持つ行では、余分なcellは静かにdropされます(cells.length > keys.length)。keyより少ないcellしかない行では、不足したkeyを "" で埋めるのではなく、そのrecordから単に省略します。したがって、すべてのrecordがすべてのkeyを持つとは想定しないでください。列が疎になる可能性がある場合は、下流で input.foo ?? "default" のようなexpressionを使ってguardしてください。

下流でrecordをfilterする

html.records は各行に列名でkeyを付けるため、自然な次の手順は expression language を使った transform filterです。
YAML
- id: cheap_games
  use: transform
  with:
    expr: 'filter(input.games, to_number(item.metascore) < 60)'
    input: "${{ { games: steps.games.output.records } }}"
html.tablerows は名前を持たないため、同等の処理では item.<key> ではなく nth(item, <index>) を使います。tableに実際のheader rowがある場合は、できるだけ html.records を使ってください。

関連

標準Pack

std/html が他のfirst-party Packの中でどこに位置するか、そしてなぜpure ToolはJournalに記録されないのか。

カスタムTool

独自の defineTool を作成します。これは html.extract が実装しているものと同じpure-tool contractです。

式言語

filtermapnthto_number。抽出したlistを答えに変換する関数です。

実行

pure Toolがreplayされるのではなく再実行される理由と、それによってFlowの再現性が保たれる仕組み。