cheerioを使ってhtmlのimgタグを相対パスに変換して出力した話

htmlソースを読み込み、ソース中のimgのパスを相対パスに変換したい。

ブラウザ上であれば、jQueryあたりでサクッとできるのだけど、これをサーバーサイド(node.js)で行いたい。

jQueryはdom環境でないと動かないので、node上でdomを扱えるライブラリと組み合わせる手もあるけど、今回はcheerioというnode上でhtmlをjQueryチックに扱えるパッケージを使ってみた。

インストールはnpmで。

npm install cheerio

さて、cheerioのドキュメントを読むと、今回の目的には「.replaceWith( content )」を使うと行けそうな感じ。contentに、Domデータ(cheerioオブジェクトと呼べばいいのかな?)かテキストデータを渡せば置換されるらしい。

cheerioは、jQueryチックなセレクタが使えるので、img要素は以下のようにすれば取得できる。

const cheerio = require('cheerio')
const $ = cheerio.load('<ul id="fruits">...</ul>') // htmlを読み込ませる
const imgs = $('img')

cheerio、なかなかやりおる。

公式のドキュメントには記載がないけど、replaceWith に関数を渡すこともできるらしい。(ググって見つけた)

$('img').replaceWith((i) => {
    return xxxxxxx
}

期待通り、img要素の数だけコールバックが呼ばれる。そうそう、これを探してた。
eachやsomeみたいで、分かりやすくて良いね。

ただし、関数に渡される引数が、配列のインデックスなのが何とも惜しい。せっかくならDom要素を返してくれたら良いのに。

そう思う人は他にもいるだろう、というかそういう要望が上がってすでに実装されているんじゃないかと思い、ソースを追ってみると…

ビンゴでした!第2引数として、Dom要素が渡されるようになってました。

$('img').replaceWith((i, elm) => {
    return xxxxxxx
}

つまり、elmのsrcを書き換えてreturnしてあげればOK。
コードがすっきりして嬉しい。

srcを相対パスに変換する際に、ドメインやパスが一定の条件にマッチした場合のみ変換してやる必要があるので、例えば「https://cashe.hogehoge.com/」から始まるURLのみを相対パスにする場合は、以下のようにすればよい。

const cheerio = require('cheerio')

// htmlを読み込む
const $ = cheerio.load('<ul id="fruits">...</ul>')

// 置換用の正規表現オブジェクトを用意する
const reg = new RegExp('^https://cashe.hogehoge.com/([0-9a-zA-Z!$()/._-]+)$')

// srcを相対URLに変換する
$('img').replaceWith((_i, elm) => {
    const src = $(elm).attr('src')
    const [_dummy, path] = src.match(reg)
    if (path) {
        return $(elm).attr('src', path)
    }
    return $(elm)
}

// htmlソースを出力する
const html = cheerio.html($('body > *'))

簡単に相対URLに変換できた!

htmlソースを出力する際に注意があって、$.html() で出力すると、htmlタグやbodyタグが付いたソースが出力されるので、これらが不要な場合はもうひと工夫が必要。今回はbodyより下が必要だったので、cheerio.html($(‘body > *’)) というように、 cheerioのセレクタを利用して簡単に解決できた。

よし!これで完成か、と思いきや、もうひとつ厄介な問題がある。

cheerio.html($(‘body > *’)) で出力すると、日本語が数値文字参照で出力されてしまう。

↓こんなやつ

これは間違いという訳ではなく、このままブラウザに渡せばきちんと表示はされる…けれど、ソースを見てチェックする際には辛すぎる。UTF-8で出力して欲しい。

これについては、ドキュメントに何も記載がなく、ググっても全く情報が見つからなかった。自前でデコードするしかないのかと諦めかけたところで、一応念のためにcheerioのソースを追ってみることに。

すると…

むむむ…このオプションは臭い…、ぷんぷん匂う…。

cheerio.html($('body > *'), { decodeEntities: false })

第2引数に、{ decodeEntities: false }) を渡してやれば、 数値文字参照での出力が回避できました。

↓完成 \(^o^)/

const cheerio = require('cheerio')

// htmlを読み込む
const $ = cheerio.load('<ul id="fruits">...</ul>')

// 置換用の正規表現オブジェクトを用意する
const reg = new RegExp('^https://cashe.hogehoge.com/([0-9a-zA-Z!$()/._-]+)$')

// srcを相対URLに変換する
$('img').replaceWith((_i, elm) => {
    const src = $(elm).attr('src')
    const [_dummy, path] = src.match(reg)
    if (path) {
        return $(elm).attr('src', path)
    }
    return $(elm)
}

// htmlソースを出力する
const html = cheerio.html($('body > *'), { decodeEntities: false })

シェアする

  • このエントリーをはてなブックマークに追加

フォローする