Jupyter NotebookでエクスポートしたHTMLのファイルサイズを削減する
まとめ
Jupyter NotebookでエクスポートしたHTMLファイルに対し、埋め込まれた画像ファイルの形式を変換することで、ファイルサイズを1/10程度に削減できます。画像ファイルはBase64エンコードされたPNG形式で、BeautifulSoupでパース・置換、OpenCVでエンコード(JPEG形式へ変換)できます。
はじめに
画像認識アルゴリズムの実験結果を共有する際に、Jupyter NotebookのHTMLエクスポート機能が重宝しています。Jupyter Notebookを使うことでテキストデータや画像データの読み込み・整形・出力はもちろん、Confusion matrixやPR curveなどの性能評価指標のプロットも手軽にできます。加えて、エクスポートの形式をHTMLにすることで、ブラウザさえあれば誰でも閲覧できるようになり、幅広いステークホルダーと情報共有ができます。
しかしながら、ノートブックに含まれる画像の大きさ・数によっては、エクスポートしたHTMLファイルの容量が大きくなってしまいます。私の場合は、画像認識アルゴリズムで不正解だった画像データを並べて表示した結果、HTMLファイルの容量が数十MBになってしまうことが多いです。そのため、メッセンジャーやメールなどのコミュニケーションツールで送信するには憚られたり、添付容量制限に引っかかったりしてしまいます。
同様の問題で困っている人や解法を探してみましたが見つからなかったので、手間のかからない範囲で解決することにしました。
HTML内の画像を圧縮する
Jupyter NotebookでエクスポートしたHTMLでは、Base64エンコード済みのPNG画像がHTMLファイル内に埋め込まれています。matplotlibで出力するグラフは、同じ画素値が連続する領域が広いため、PNG形式でもデータ容量が十分に圧縮できます。一方で、写真などの自然画像については、PNG形式では同様の圧縮効果は期待できません。これらの画像をJPEG形式にすることで、HTML全体の容量を削減することを考えます。
ソースコード
HTMLファイルを文字列として受け取り、ファイル内のBase64エンコードされた画像をJPEG圧縮するクラスです。
import base64
import re
import cv2.cv2 as cv2
import numpy as np
from bs4 import BeautifulSoup
class EmbedImageCompressor(object):
_pattern = 'data:image/\w+;base64,(.+)'
_pattern_compiled = re.compile(_pattern, re.S)
_src_prefix = 'data:image/jpeg;base64,'
def __init__(self, file_path):
with open(file_path, 'r') as f:
self.content = f.read()
self.soup = BeautifulSoup(self.content, 'html.parser')
def process(self):
for tag in self.soup.findAll('img'):
src_str = tag.attrs.get('src')
result = self._pattern_compiled.match(src_str)
if result is None:
continue
img_b64 = result.group(1)
img_cv2 = decode(img_b64)
img_b64_new = compress_and_encode(img_cv2)
tag_new = self.soup.new_tag('img')
tag_new.attrs['src'] = f'{self._src_prefix}{img_b64_new}'
tag.replace_with(tag_new)
def save(self, path: str):
with open(path, 'w') as f:
f.write(self.soup.prettify())
def compress_and_encode(img: np.ndarray, quality=95) -> str:
encode_param = [cv2.IMWRITE_JPEG_QUALITY, quality]
success, img_compressed = cv2.imencode('.jpg', img, encode_param)
# TODO: handle fail of encoding appropriately
if not success:
raise ValueError
img_b64 = base64.b64encode(img_compressed)
return img_b64.decode('utf-8')
def decode(img_b64: str) -> np.ndarray:
img_data = base64.b64decode(img_b64)
img_np = np.frombuffer(img_data, np.uint8)
img = cv2.imdecode(img_np, cv2.IMREAD_ANYCOLOR)
return img
こんな感じで呼びます。
from embed_image_compressor import EmbedImageCompressor
input_path = 'resources/cifar10_tutorial.html'
out_path = 'resources/out.html'
def main():
compressor = EmbedImageCompressor(input_path)
compressor.process()
compressor.save(out_path)
if __name__ == '__main__':
main()
HTMLのパース
エクスポートしたHTMLの中身を見てみると、画像は次のように埋め込まれていることが確認できました。
<img src="data:image/png;base64,{画像をbase64エンコードした文字列}">
したがって、正規表現などのパターンで src
の部分を引っ掛けて置換すればよいことが分かります。HTMLのパースおよび正規表現のマッチングには標準モジュールのHTMLParserを使えば十分ですが、置換後のHTMLを出力する必要があり、この置換がより手軽にできそうなBeautifulSoupを採用しました。
for tag in self.soup.findAll('img'): # HTML内のimgタグに対してイテレート
src_str = tag.attrs.get('src') # imgタグ内のsrcプロパティを取得
result = self._pattern_compiled.match(src_str) # 正規表現マッチング
# 中略。ここでsrcプロパティをデコード→変換→再エンコードする
# 置換も楽チン
tag_new = self.soup.new_tag('img')
tag_new.attrs['src'] = f'{self._src_prefix}{img_b64_new}'
tag.replace_with(tag_new)
Base64エンコードされた画像のデコード・圧縮・再エンコード
画像のJPEG圧縮にはOpenCVを利用しました。理由は、単に使い慣れているためです。JPEG圧縮というタスクに対してはオーバースペックなライブラリですが、学習コストがかからないことを重視しました。
Base64のデコード
この記事を参考にしました:[Python3] 画像をBase64にエンコード、Base64をNumPy配列へ読み込みOpenCVで処理、NumPy配列をBase64に変換 - Qiita
def decode(img_b64: str) -> np.ndarray:
img_data = base64.b64decode(img_b64)
img_np = np.frombuffer(img_data, np.uint8)
img = cv2.imdecode(img_np, cv2.IMREAD_ANYCOLOR)
return img
JPEG圧縮
この記事を参考にしました:OpenCVを利用して画像を圧縮(encode→decode)する - Qiita
cv2.imencode
はエンコードの成否を返しますが、エンコードが失敗するケースが不明のためハンドリングが適当です。
def compress_and_encode(img: np.ndarray, quality=95) -> str:
encode_param = [cv2.IMWRITE_JPEG_QUALITY, quality]
success, img_compressed = cv2.imencode('.jpg', img, encode_param)
# TODO: handle fail of encoding appropriately
if not success:
raise ValueError
img_b64 = base64.b64encode(img_compressed)
return img_b64.base64_to_ndarray('utf-8')
留意点
- 変換前後で、タグのインデントなど、HTMLのフォーマットが変わることがあります。BeautifulSoupの使い方次第で解決できそうですが、HTMLの見た目には変化ないので手付かずになっています。
-
など一部エンティティうまく扱えず、文字化けすることがあるようです。これも大きな問題になっていないので手付かずです。
処理結果
など一部エンティティうまく扱えず、文字化けすることがあるようです。これも大きな問題になっていないので手付かずです。HTMLファイル内の画像枚数によりますが、私の場合は1/10程度の削減(数十MB→数MB)ができました。
Author And Source
この問題について(Jupyter NotebookでエクスポートしたHTMLのファイルサイズを削減する), 我々は、より多くの情報をここで見つけました https://qiita.com/mamo3gr/items/c2a4c94673f106dfea63著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .