Skip to content

Instantly share code, notes, and snippets.

@sheepla
Last active January 21, 2024 08:06
Show Gist options
  • Save sheepla/c89e0eed1fc00e95a7f74bb0982ad6a1 to your computer and use it in GitHub Desktop.
Save sheepla/c89e0eed1fc00e95a7f74bb0982ad6a1 to your computer and use it in GitHub Desktop.
Search duckduckgo with Deno/TypeScript

DenoでHTMLドキュメントをパースする: 雑な解説

DOMの扱い方はよく忘れそうになるので、備忘録がてら書いてみる。MDNから引用するようにしているが、もしかしたら理解が間違っているかもしれない。 DenoでHTMLドキュメントをパースするには、deno-dom-wasmを使う。

deno-dom-wasmはDenoのためのDOMおよびHTML parserの実装。Rust (WASMコンパイル)とTypeScriptで書かれている。

parseFromStringメソッドを使ってHTMLドキュメントをパースする。 第1引数はパースしたいHTML文字列。第2引数はMIMEタイプとして text/html を指定する。 戻り値の型は null または NodeList となっている。

parseFromString(source:string, mimeType:DOMParserMimeType) : NodeList | null

NodeListは、Nodeの集合体を表す。NodeはHTMLドキュメントのそれぞれの節(ノード)を表す抽象クラスで、Document, Element, DocumentFragmentなどのサブクラスを持つ。 つまり、ElementはNodeのふるまいを持っていると言える。

EventTarget ◁- Node ◁- Element

NodeListは、そのままではNodeの配列として扱うことができないため関数型プログラミングで多用される map()filter() のような便利なメソッドを直接呼び出すことができない。MDNにはこう書かれている。

メモ: NodeList は Array とは異なりますが、forEach() メソッドで処理を反復適用することは可能です。Array.from() を使うことで Array に変換することができます。

Array.from() を使うとNodeListをNodeの配列として扱うことができる。 配列のプロトタイプ関数 map() を使ってそれぞれの要素(Node)に関数を適用し、新しく作る配列にマップする。

NodeからquerySelector()メソッドを呼び出したいが、Nodeはそのメソッドを持っていないため一旦Elementに変換する。 あまりしっくりこない方法だと感じるのでもっと良い方法があれば知りたい

Array.from(nodeList).map((node: Node) => {
    const el = node as Element;

    // TODO: Do something with `el`
    return {
      // ...
    }
});

Elementはセレクタから子ノードを取得する querySelector()querySelectorAll()、属性を取得する getAttribute()、中身のテキストを取得するtextContentそれぞれのメンバーを持っている。

  • Element.querySelector(selectors:string) : Element | null
  • Element.querySelectorAll(selectors:string) : NodeList
  • Element.getAttribute(name:string) : string | null
  • Node.textContent : string

map() 関数内でElementから取得した属性やテキストをオブジェクトに詰め込み、パース結果の入ったオブジェクトの配列を作ることができる。

return {
  title: el.querySelector(".result__title")?.textContent.trim(),
  link: extractSearchParam(
    `https:${el.querySelector("a.result__url")?.getAttribute("href")!}`,
    "uddg",
  ),
  snippet: el.querySelector(".result__snippet")?.textContent.trim(),
};

オブジェクトに詰め込んでしまえばあとは.filter()でフィルタリングしたりJSON.stringify()でJSONに変換したり好きなように取り回せる。

解説おわり。

リファレンス

{
"title": "DOM APIs - DOMParser | Deno Doc - Identity Digital",
"link": "https://doc.deno.land/deno/dom/~/DOMParser",
"snippet": "Provides the ability to parse XML or HTML source code from a string into a DOM Document. interface DOMParser { parseFromString ( string: string, type: DOMParserSupportedType): Document; } var DOMParser: { prototype: DOMParser; new (): DOMParser; }; Methods parseFromString ( string: string, type: DOMParserSupportedType): Document"
}
{
"title": "Using deno-dom with Deno | Deno Docs",
"link": "https://docs.deno.com/runtime/manual/advanced/jsx_dom/deno_dom",
"snippet": "deno-dom is an implementation of DOM and HTML parser in Deno. It is implemented in Rust (via Wasm) and TypeScript. There is also a \"native\" implementation, leveraging the FFI interface. deno-dom aims for specification compliance, like jsdom and unlike LinkeDOM."
}
{
"title": "docs.deno.com",
"link": "https://docs.deno.com/manual/jsx_dom/deno_dom",
"snippet": "deno-dom is a library that allows you to use DOM and HTML features in Deno, a secure and modern JavaScript runtime. Learn how to install, import, and use deno-dom in your Deno projects with this official manual."
}
{
"title": "GitHub - b-fuze/deno-dom: Browser DOM & HTML parser in Deno",
"link": "https://github.com/b-fuze/deno-dom",
"snippet": "html-parser src test wpt @ e41b015 .gitignore .gitmodules Cargo.toml LICENSE README.md deno-dom-native.ts deno-dom-wasm-noinit.ts deno-dom-wasm.ts deno.jsonc deno.lock design.md"
}
{
"title": "deno_dom@v0.1.43 | Deno",
"link": "https://deno.land/x/deno_dom",
"snippet": "Deno DOM has two backends, WASM and native using Deno native plugins. Both APIs are identical, the difference being only in performance.The WASM backend works with all Deno restrictions, but the native backend requires the --unstable --allow-ffi flags. You can switch between them by importing either deno-dom-wasm.ts or deno-dom-native.ts. Deno DOM is still under development, but is fairly ..."
}
import {
DOMParser,
Element,
HTMLDocument,
Node,
NodeList,
} from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";
const baseURL = "https://html.duckduckgo.com/html";
function buildURL(
baseURL: string,
searchParams: Iterable<string[]> | Record<string, string>,
): URL {
const url = new URL(baseURL);
url.search = new URLSearchParams(searchParams).toString();
return url;
}
function buildSearchRequest(query: string): Request {
const url = buildURL(baseURL, [
["q", query],
["v", "1"],
["o", "json"],
["api", "/d.js"],
]);
return new Request(url);
}
function extractSearchParam(url: string, keyName: string): string {
const u = new URL(url);
return u.searchParams.get(keyName)!;
}
function parseDocument(doc: HTMLDocument) {
const nodeList: NodeList | null = doc?.querySelectorAll(".result")!;
return Array.from(nodeList!).map((node: Node) => {
const el = node as Element;
return {
title: el.querySelector(".result__title")?.textContent.trim(),
link: extractSearchParam(
`https:${el.querySelector("a.result__url")?.getAttribute("href")!}`,
"uddg",
),
snippet: el.querySelector(".result__snippet")?.textContent.trim(),
};
});
}
const query = Deno.args.join(" ");
const html = await fetch(buildSearchRequest(query)).then((resp) => {
if (!resp.ok) {
throw new Error(`HTTP Status Error: ${resp.status} ${resp.statusText}`);
}
return resp.text();
});
const parser = new DOMParser();
const doc: HTMLDocument | null = parser.parseFromString(html, "text/html");
const result = parseDocument(doc!)
console.log(JSON.stringify(result));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment