不燃ごみ捨てろよー
-
ワードクラウドの定期更新 →
1つできるとN個には簡単にスケールできる件。二番煎じ感あるけど再び。
備忘:メモ書き___φ(. . )
Janome
https://github.com/mocobeta/janomeword_cloud
https://github.com/amueller/word_cloudJanome
Janomeの形態素解析ではMeCabの辞書を使用していてパッケージのインストール時に辞書をビルドしている(setup.py、ipadic/build.sh、ipadic/build.py)。辞書に載っているか総当たりでチェックしている以下の処理がメイン処理かと思われる。
Tokenizerクラス
def __tokenize_partial(self, text, wakati, baseform_unk, dotfile): if self.wakati and not wakati: raise WakatiModeOnlyException chunk_size = min(len(text), Tokenizer.MAX_CHUNK_SIZE) lattice = Lattice(chunk_size, self.sys_dic) pos = 0 while not self.__should_split(text, pos): encoded_partial_text = text[pos:pos + min(50, chunk_size - pos)].encode('utf-8') # user dictionary if self.user_dic: entries = self.user_dic.lookup(encoded_partial_text) for e in entries: lattice.add(SurfaceNode(e, NodeType.USER_DICT)) matched = len(entries) > 0 # system dictionary entries = self.sys_dic.lookup(encoded_partial_text, self.matcher) for e in entries: lattice.add(SurfaceNode(e, NodeType.SYS_DICT)) matched = len(entries) > 0 # unknown cates = self.sys_dic.get_char_categories(text[pos]) if cates: for cate in cates: if matched and not self.sys_dic.unknown_invoked_always(cate): continue # unknown word length length = self.sys_dic.unknown_length(cate) \ if not self.sys_dic.unknown_grouping(cate) else self.max_unknown_length assert length >= 0 # buffer for unknown word buf = text[pos] for p in range(pos + 1, min(chunk_size, pos + length + 1)): _cates = self.sys_dic.get_char_categories(text[p]) if cate in _cates or any(cate in _compat_cates for _compat_cates in _cates.values()): buf += text[p] else: break unknown_entries = self.sys_dic.unknowns.get(cate) assert unknown_entries for entry in unknown_entries: left_id, right_id, cost, part_of_speech = entry base_form = buf if baseform_unk else '*' dummy_dict_entry = (buf, left_id, right_id, cost, part_of_speech, '*', '*', base_form, '*', '*') lattice.add(Node(dummy_dict_entry, NodeType.UNKNOWN)) pos += lattice.forward() lattice.end() min_cost_path = lattice.backward() assert isinstance(min_cost_path[0], BOS) assert isinstance(min_cost_path[-1], EOS) if wakati: tokens = [node.surface for node in min_cost_path[1:-1]] else: tokens = [] for node in min_cost_path[1:-1]: if type(node) == SurfaceNode and node.node_type == NodeType.SYS_DICT: tokens.append(Token(node, self.sys_dic.lookup_extra(node.num))) elif type(node) == SurfaceNode and node.node_type == NodeType.USER_DICT: tokens.append(Token(node, self.user_dic.lookup_extra(node.num))) else: tokens.append(Token(node)) if dotfile: lattice.generate_dotfile(filename=dotfile) return (tokens, pos)
ユーザー辞書にありますか?システム辞書にありますか?unknownですか?
形態素解析ってこういうことですね。
word_cloud
ワードクラウドは、パラメタお化けだけど柔軟なメソッドでコマンドのラッパーも用意されている。
classwordcloud.WordCloud(font_path=None, width=400, height=200, margin=2, ranks_only=None, prefer_horizontal=0.9, mask=None, scale=1, color_func=None, max_words=200, min_font_size=4, stopwords=None, random_state=None, background_color='black', max_font_size=None, font_step=1, mode='RGB', relative_scaling='auto', regexp=None, collocations=True, colormap=None, normalize_plurals=True, contour_width=0, contour_color='black', repeat=False, include_numbers=False, min_word_length=0, collocation_threshold=30)
中身は画像処理で、ワードクラウドの文字列をメモリ上に描画して、矩形の領域を取得して、ベースとなる画像に上書きしている。上書きするときに他の配置済みの文字列と重ならない場所を全画素、総当たりで判定して配置位置を決定している。
generate_from_frequencies関数
# start drawing grey image for word, freq in frequencies: if freq == 0: continue # select the font size rs = self.relative_scaling if rs != 0: font_size = int(round((rs * (freq / float(last_freq)) + (1 - rs)) * font_size)) if random_state.random() < self.prefer_horizontal: orientation = None else: orientation = Image.ROTATE_90 tried_other_orientation = False while True: # try to find a position font = ImageFont.truetype(self.font_path, font_size) # transpose font optionally transposed_font = ImageFont.TransposedFont( font, orientation=orientation) # get size of resulting text box_size = draw.textbbox((0, 0), word, font=transposed_font, anchor="lt") # find possible places using integral image: result = occupancy.sample_position(box_size[3] + self.margin, box_size[2] + self.margin, random_state) ・・・省略・・・
occupancy.sample_position()から呼び出されるquery_integral_image()が、「query_integral_image.pyx」に書かれていて「.pyx」の拡張子となっている。調べてみるとCythonのコードでCのソースを生成しているとのこと。
query_integral_image.pyx:35行
query_integral_image.c:21,510行巨大な.cのコードは、#ifdefだらけで人間が読むコードではなくなっている。
「query_integral_image.c」は、setup.pyに拡張モジュールとして書かれていてパッケージのインストール時にコンパイルされてlib配下にsoファイルが生成されていた。(query_integral_image.cpython-311-darwin.so)
ext_modules=[Extension("wordcloud.query_integral_image", ["wordcloud/query_integral_image.c"])],
以下が、「query_integral_image.pyx:35行 」のソース
# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False import array import numpy as np def query_integral_image(unsigned int[:,:] integral_image, int size_x, int size_y, random_state): cdef int x = integral_image.shape[0] cdef int y = integral_image.shape[1] cdef int area, i, j cdef int hits = 0 # count how many possible locations for i in xrange(x - size_x): for j in xrange(y - size_y): area = integral_image[i, j] + integral_image[i + size_x, j + size_y] area -= integral_image[i + size_x, j] + integral_image[i, j + size_y] if not area: hits += 1 if not hits: # no room left return None # pick a location at random cdef int goal = random_state.randint(0, hits) hits = 0 for i in xrange(x - size_x): for j in xrange(y - size_y): area = integral_image[i, j] + integral_image[i + size_x, j + size_y] area -= integral_image[i + size_x, j] + integral_image[i, j + size_y] if not area: hits += 1 if hits == goal: return i, j
画像全体をピクセル単位で走査して、そこに文字列を置けるか判定している。
area = integral_image[i, j] + integral_image[i + size_x, j + size_y] area -= integral_image[i + size_x, j] + integral_image[i, j + size_y]
ただ、性能的な理由でsoファイルに切り出していると思われるけれど、なぜか全体を2回走査している。何かそれ相応の理由があるのか?
ライブラリの中身がざっくり分かったところでAPI Referenceを眺めていたら以下のメソッドを発見。
to_svg([embed_font, optimize_embedded_font, …])
to_svg()のソースはテキストの書き出し処理でダラダラ長いだけの面白くないソースなので割愛。
PNGファイルをSVGファイルに差し替えて一旦終わり。SVGファイル公開。
フォント周り
ワードクラウドのフォントを変更した場合、PNGファイルには反映されるけれどSVGファイルには反映されない現象が発生した。
SVGファイルのフォントはローカル環境にインストールされているフォントが使われて指定したフォントがないと代替のフォントで表示されるとのこと。
すべての環境で同じフォントを使用させたい場合、Webフォントを使用する必要があるが、それだとネットに繋がっていない場合や、フォントサーバが落ちている場合に現象が再現すると思われる。
さらに調べるとWebフォントをbase64にエンコードしてSVGファイルに添付している人がいた。フォントファイルを丸ごとエンコードするとファイルサイズが巨大になるので使用している文字だけ切り出してサブセットのフォントを生成する必要があるとのこと。
そしてサブセットのフォントを生成してSVGファイルに添付する場合、フォントの改変や再頒布が可能なライセンスが必要でありフォントのライセンスを調査した。
- SIL Open Font License
- Apache License 2.0
- M+ FONT LICENSE
ライセンスや使用条件を確認しながらフリーフォントをダウンロードした。フリーフォントを公開している人に感謝です。ありがとうございます。
あとは修正作業。SVGファイルで使用している文字をpyftsubsetコマンドでフォントから切り出してきて、base64コマンドでエンコードして、SVGファイルを修正して、raspiにデプロイして、macOSとLinuxでbase64コマンドのオプションが異なるみたいなことにハマりつつ、SVGファイルにフリーフォントを反映できるようになった。最後にフォントをランダムに切り替える処理を追加して、バッチとして動くことを確認して、ミッションコンプリート!
1つできるまでが長い。0から1が長い。
正常ルートだけのやりたいことしか書いてないダメダメソースだけど40行から120行に膨らんでいる。
# word_cloudパッケージを6/1にインストールしていたので10日間ぐらいの作業メモ。読み返すと修正したくなる。書き足したくなる。推敲したくなる。けど、書き殴りのメモ書きが正解やんなあ?
-
眠る前の「私」と、目覚めた後の「私」は同じなのか?
安心してください、同じですよー
-
-
ル・マン ~レースに懸ける男たち~
ル・マンには魔物が棲んでいる!?
ル・マン24時間レース
https://ja.wikipedia.org/wiki/ル・マン24時間レース
公式ウェブサイト
https://www.24h-lemans.com/🎊100周年。
6/10 (土) 23:00(JST)スタート!
トヨタがんばれー
-
時の記念日
時の記念日は、日本の記念日の1つ。毎年6月10日。日本で初めて時計(「漏刻」と呼ばれる水時計)による時の知らせが行われたことを記念して制定された。記念日ではあるが、国民の祝日に関する法律に規定された国民の祝日ではない。日本では6月に国民の祝日がないため、時の記念日を6月の国民の祝日にすべきとの意見も多いが、実現には至っていない。
🕰 祝日で -
ポップ