Skip to content

Latest commit

 

History

History
225 lines (166 loc) · 6.4 KB

File metadata and controls

225 lines (166 loc) · 6.4 KB

html2md アーキテクチャ解説

URLからマークダウンファイルを生成する仕組みを解説する。

処理フロー

URL
 ↓ fetch API または curl
HTML文字列
 ↓ jsdom
DOMツリー (window.document)
 ↓ @mozilla/readability
本文HTML (ナビゲーション等を除去)
 ↓ turndown
Markdown文字列
 ↓ fs.writeFileSync
ファイル出力

使用ライブラリ

1. jsdom

役割: HTML文字列からブラウザと同等のDOMツリーを構築する

なぜ必要か: @mozilla/readabilityはブラウザのdocumentオブジェクトを前提としている。Node.jsにはDOMがないため、jsdomで仮想的なDOM環境を作る。

使用箇所 (src/pipeline.mts):

import { JSDOM } from 'jsdom';

const dom = new JSDOM(input.html, { url: input.url });
// dom.window.document でブラウザと同じdocumentオブジェクトが得られる

JSDOMコンストラクタのオプション:

  • 第1引数: HTML文字列
  • url: ベースURLを設定。相対リンクの解決に使用される

内部の仕組み:

  • HTML5パーサー(parse5)でHTMLをパース
  • whatwg-urlでURL処理
  • cssstyleでCSSOM実装
  • DOMのほぼ全API(querySelector, getElementByIdなど)を実装

2. @mozilla/readability

役割: Webページから本文コンテンツを抽出する(Firefoxのリーダービュー機能と同じアルゴリズム)

なぜ必要か: Webページにはナビゲーション、サイドバー、広告、フッターなど本文以外の要素が多い。これらを除去して記事本文だけを取り出す。

使用箇所 (src/pipeline.mts):

import { Readability } from '@mozilla/readability';

const reader = new Readability(dom.window.document);
const article = reader.parse();

parse()の戻り値:

{
  title: string;        // 記事タイトル
  content: string;      // 処理済みHTML(本文のみ)
  textContent: string;  // プレーンテキスト版
  excerpt: string;      // 記事の抜粋
  byline: string;       // 著者情報
  siteName: string;     // サイト名
  publishedTime: string; // 公開日時
}

抽出アルゴリズムの概要:

  1. 候補要素のスコアリング

    • <p>, <div>, <article>などの要素に初期スコアを付与
    • クラス名・ID名でスコア調整(article, content → 加点、sidebar, nav → 減点)
  2. テキスト密度の計算

    • 各要素内のテキスト量、リンク密度、句読点の数を分析
    • リンクだらけの要素(ナビゲーション)は低スコア
  3. 最高スコア要素の選択

    • スコアが最も高い要素を本文コンテナとして採用
  4. クリーンアップ

    • 不要な要素(<script>, <style>, <nav>など)を除去
    • 空の要素を削除
    • 相対URLを絶対URLに変換

3. turndown

役割: HTMLをMarkdown形式に変換する

使用箇所 (src/pipeline.mts):

import TurndownService from 'turndown';

const turndown = new TurndownService({
  headingStyle: 'atx',       // # 形式の見出し
  codeBlockStyle: 'fenced',  // ``` 形式のコードブロック
  bulletListMarker: '-',     // リストのマーカー
});

const markdown = turndown.turndown(input.article.content);

変換ルール(デフォルト):

HTML Markdown
<h1> #
<h2> ##
<strong>, <b> **text**
<em>, <i> *text*
<a href="url"> [text](url)
<img src="url"> ![alt](url)
<code> `code`
<pre><code> ```code```
<ul><li> - item
<ol><li> 1. item
<blockquote> > quote

内部の仕組み:

  1. HTMLをDOMとしてパース(内部でdomino使用)
  2. DOMツリーを深さ優先で走査
  3. 各ノードに対応するルールを適用
  4. テキストノードはエスケープ処理(*, _, [ など)
  5. 結果を連結してMarkdown文字列を生成

オプション詳細:

  • headingStyle: 'atx'(#形式)or 'setext'(下線形式)
  • codeBlockStyle: 'fenced'(```)or 'indented'(4スペース)
  • bulletListMarker: '-', '+', '*' のいずれか

Node.js標準API

fetch API

役割: HTTPリクエストを送信してHTMLを取得

const response = await fetch(url);
const html = await response.text();

Node.js 18以降でグローバルに利用可能。内部実装はundici。

curl(代替モード)

役割: curlコマンドでHTMLを取得(--curlオプション使用時)

import { exec } from 'node:child_process';
import { promisify } from 'node:util';

const execAsync = promisify(exec);
const { stdout } = await execAsync(`curl -sL "${url}"`);

なぜ必要か: fetch APIがブロックされる環境(プロキシ、ファイアウォール等)でも、curlコマンドが使える場合がある。curlはシステムのプロキシ設定を自動で参照する。

オプション:

  • -s: サイレントモード(プログレス非表示)
  • -L: リダイレクトを追跡

fs (File System)

役割: ファイルの読み書き

import { writeFileSync, mkdirSync, existsSync } from 'node:fs';

writeFileSync(path, content, 'utf-8');

path

役割: ファイルパスの操作

import { join } from 'node:path';

const outputDir = join(import.meta.dirname, '..', 'output');

import.meta.dirnameはNode.js 20.11以降で利用可能。ESモジュールにおける__dirname相当。

データフローの詳細

1. CLI引数からURL取得
   process.argv[2] → "https://example.com/article"

2. HTTPリクエスト
   fetch(url) → Response { status: 200, ... }
   response.text() → "<html><head>..."

3. DOM構築
   new JSDOM(html, { url })
   → {
       window: {
         document: Document { ... }
       }
     }

4. 本文抽出
   new Readability(document).parse()
   → {
       title: "記事タイトル",
       content: "<div id=\"readability-page-1\">...</div>",
       ...
     }

5. Markdown変換
   turndownService.turndown(content)
   → "# 見出し\n\n本文テキスト..."

6. フロントマター付与
   "---\ntitle: ...\n---\n" + markdown

7. ファイル出力
   writeFileSync("output/20260123-123456_記事タイトル.md", fullMarkdown)