tag:crieit.net,2005:https://crieit.net/tags/CSV/feed 「CSV」の記事 - Crieit Crieitでタグ「CSV」に投稿された最近の記事 2019-11-27T13:45:50+09:00 https://crieit.net/tags/CSV/feed tag:crieit.net,2005:PublicArticle/15563 2019-11-27T13:45:50+09:00 2019-11-27T13:45:50+09:00 https://crieit.net/posts/django-import-export-CSV django-import-exportで管理画面にCSVエクスポート機能を追加する <p>Djangoのadminサイト、調べてみるといろいろプラグインがあるらしい。</p> <p>管理画面のデータをCSVなどの形式でインポート/エクスポートしたくなったので、<br /> 調べてみたら<a target="_blank" rel="nofollow noopener" href="https://django-import-export.readthedocs.io/en/stable/index.html">django-import-export</a>で簡単にできた。その時の備忘録。</p> <h5 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h5> <p>まずはpipでインストール</p> <pre><code class="shell">$ pip install django-import-export </code></pre> <h5 id="設定"><a href="#%E8%A8%AD%E5%AE%9A">設定</a></h5> <p><code>import_export</code>をINSTALLED_APPSに追加</p> <pre><code class="python"># settings.py INSTALLED_APPS = ( ... 'import_export', ) </code></pre> <h5 id="admin.pyに追加"><a href="#admin.py%E3%81%AB%E8%BF%BD%E5%8A%A0">admin.pyに追加</a></h5> <p>対象のデータに対する設定を追加していく。</p> <p>サンプルのモデルはこんな感じ。</p> <pre><code class="python"># models.py class Book(models.Model): name = models.CharField('Book name', max_length=100) author = models.CharField('Book name', max_length=100) </code></pre> <p>django-import-exportの設定。</p> <p>対象とするモデルに対して<code>ModelResource</code>を継承したクラスを追加する。<br /> 設定関連はココに書いていくらしい。</p> <pre><code class="python"># admin.py from django.contrib import admin from import_export import resources from import_export.admin import ImportExportModelAdmin from .models import Book class BookResource(resources.ModelResource): # Modelに対するdjango-import-exportの設定 class Meta: model = Book @admin.register(Book) class BookAdmin(ImportExportModelAdmin): # ImportExportModelAdminを利用するようにする ordering = ['id'] list_display = ('id', 'title', 'author') # django-import-exportsの設定 resource_class = BookResource </code></pre> <p>最後に、ImportExportModelAdminを継承したAdminクラスを用意して、<br /> resource_classに<code>ModelResource</code>を継承したクラスを設定すればOK!</p> <p>すると、こんな感じにボタンが表示される。簡単(<em>´ω`</em>)</p> <p><img width="221" alt="スクリーンショット 2019-11-27 13.38.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/aaaac4b5-e0dd-97ff-6391-a57c9d27ad63.png"></p> <h4 id="小ネタ"><a href="#%E5%B0%8F%E3%83%8D%E3%82%BF">小ネタ</a></h4> <h5 id="Exportだけにする: Importを無効化"><a href="#Export%E3%81%A0%E3%81%91%E3%81%AB%E3%81%99%E3%82%8B%3A+Import%E3%82%92%E7%84%A1%E5%8A%B9%E5%8C%96">Exportだけにする: Importを無効化</a></h5> <p>インポートは別にいらないなと思ったので、無効化してみた。<br /> <code>ExportMixin</code>だけにするといいらしい。</p> <pre><code class="python"># ... 略 from import_export.admin import ExportMixin @admin.register(Book) class BookAdmin(ExportMixin, admin.ModelAdmin): # ExportMixinをadmin.ModelAdminに追加すればOK ordering = ['id'] list_display = ('id', 'title', 'author') # django-import-exportsの設定 resource_class = BookResource </code></pre> <h5 id="Exportできるフォーマットを指定する"><a href="#Export%E3%81%A7%E3%81%8D%E3%82%8B%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B">Exportできるフォーマットを指定する</a></h5> <p>デフォルトだとJSONとかYMLとかいろいろ選べるけどCSVだけでいいので、<br /> 選択できる部分を絞ってみた。formatsを指定すればOK</p> <pre><code class="python"># ... 略 from import_export.formats import base_formats @admin.register(Book) class BookAdmin(ImportExportModelAdmin): ordering = ['id'] list_display = ('id', 'title', 'author') # django-import-exportsの設定 resource_class = BookResource formats = [base_formats.CSV] # formatsで指定できる </code></pre> <p>以上!!</p> <h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんなのつくってます!!</a></h2> <p>積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!<br /> <a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、Nuxt.js+Firebaseで開発してます!</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/></p> <p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p> <p>要望・感想・アドバイスなどあれば、<br /> 公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで♪</p> <h1 id="参考にしたサイト様"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E6%A7%98">参考にしたサイト様</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://django-import-export.readthedocs.io/en/stable/index.html#">Django import / export — django-import-export 1.2.0 documentation</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/29252500/django-import-export-only-export">Django import-export: only export - Stack Overflow</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/45930421/how-to-have-only-csv-xls-xlsx-options-in-django-import-export">How to have only CSV, XLS, XLSX options in django-import-export? - Stack Overflow</a></li> </ul> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/15387 2019-09-10T20:49:01+09:00 2019-09-10T20:49:01+09:00 https://crieit.net/posts/TSV-Markdown TSVで定義した辞書をMarkdownで出力するツールを作った話 <h2 id="TL;DR"><a href="#TL%3BDR">TL;DR</a></h2> <p>電子辞書が欲しくなったので作ることにしました。今回の要件は、品詞別の索引と全単語の索引、先頭の文字ごとの詳細解説があることです。</p> <p>そこで辞書本体をmarkdownで書くことにしたのですが、ちまちま手で書くのは面倒くさい。なのでTSVを読み込んでmarkdownを吐くジェネレータを簡単に書いてみることにしました。</p> <h2 id="TSVに格納する辞書の形式を考えてみた"><a href="#TSV%E3%81%AB%E6%A0%BC%E7%B4%8D%E3%81%99%E3%82%8B%E8%BE%9E%E6%9B%B8%E3%81%AE%E5%BD%A2%E5%BC%8F%E3%82%92%E8%80%83%E3%81%88%E3%81%A6%E3%81%BF%E3%81%9F">TSVに格納する辞書の形式を考えてみた</a></h2> <p>TSVとは言いつつ、純粋なTSVは使っていません。まずは辞書のヘッダ部分です。</p> <pre><code>BEGIN_HEADER LANGUAGE_LONG Language Name LANGUAGE_CODE LC(注1) PHONETICAL_CHARS 頭文字になりうる文字の列挙(注2) END_HEADER </code></pre> <ul> <li>注1 これは2~3文字の言語コードです。<code>ja</code>, <code>en</code>など</li> <li>注2 スペース区切りで列挙します。<code>a b c d e f g h i j k l m n o p q r s t u v w x y z</code>のように</li> </ul> <p>続いて、辞書の本体を考えてみました。</p> <pre><code>BEGIN_DICTIONARY 単語 品詞ID(注1) 意味 関連語(注2) END_DICTIONARY </code></pre> <ul> <li>注1 品詞IDは任意の文字列です。</li> <li>注2 関連語はスペース区切りで列挙します。<code>study learn</code>のように</li> <li>単語は任意個この形式で列挙します。</li> </ul> <p>最後に、品詞の定義です。</p> <pre><code>BEGIN_DEFINITION 品詞ID 品詞の名称 END_DEFINITION </code></pre> <p>このフィールドでは、DICTIONARYフィールド内で使用した品詞IDとその名称の対応(<code>NOUN</code>と名詞のような)を定義します。</p> <h2 id="パーサーをざっくり書いてみる"><a href="#%E3%83%91%E3%83%BC%E3%82%B5%E3%83%BC%E3%82%92%E3%81%96%E3%81%A3%E3%81%8F%E3%82%8A%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">パーサーをざっくり書いてみる</a></h2> <p>さて、このパーサーをざっくり書いてみました。</p> <pre><code class="python">import csv class ParseError(SyntaxError): pass def open_dict(dic_path: str) -> list: with open(dic_path, encoding='utf-8') as f: reader = csv.reader(f, delimiter='\t') return list(reader) def parse_dict(dic: list) -> dict: ret = {} state = 'none' for i in dic: if i[0] == 'BEGIN_HEADER': if state != 'none': raise ParseError('Unexpected BEGIN_HEADER tag.') state = 'header' ret['header'] = {} continue if i[0] == 'END_HEADER': if state != 'header': raise ParseError('Unexpected END_HEADER tag.') state = 'none' continue if i[0] == 'BEGIN_DICTIONARY': if state != 'none': raise ParseError('Unexpected BEGIN_DICTIONARY tag.') state = 'dictionary' ret['dict'] = {} continue if i[0] == 'END_DICTIONARY': if state != 'dictionary': raise ParseError('Unexpected END_DICTIONARY tag.') state = 'none' continue if i[0] == 'BEGIN_DEFINITION': if state != 'none': raise ParseError('Unexpected BEGIN_DEFINITION tag.') state = 'definition' ret['defs'] = {} continue if i[0] == 'END_DEFINITION': if state != 'definition': raise ParseError('Unexpected END_DEFINITION tag.') state = 'none' continue if state == 'none': continue if state == 'header': ret['header'][i[0]] = i[1] continue if state == 'dictionary': if i[0] not in ret['dict']: ret['dict'][i[0]] = {} ret['dict'][i[0]][i[1]] = { 'meaning': i[2], 'reference': i[3].split(' ') } continue if state == 'definition': ret['defs'][i[0]] = i[1] if state != 'none': raise ParseError(f'A match pair tag of END_{state.upper()} not found.') return ret </code></pre> <p>まぁ、本当に簡単に書いているので、解説することもほとんどないんですけれど……。ざっくり説明すると、tsvを読み込んで2次元配列に格納し、それを先ほど定義したフォーマットに従って辞書に格納しなおしているだけです。</p> <p>次に、この生成した辞書から索引情報を抽出する関数を定義してみます。</p> <pre><code class="python">def get_comparator(_order): class _Comparator(str): def __gt__(self, other): order = list(_order) for s, o in zip(self, other): oi = order.index(o) si = order.index(s) if oi > si: return True if si > oi: return False return len(self) > len(other) def __lt__(self, other): order = list(_order) for s, o in zip(self, other): oi = order.index(o) si = order.index(s) if oi < si: return True if si < oi: return False return len(self) < len(other) return _Comparator def generate_indices(dic: dict): chars = dic['header'].get( 'PHONETICAL_CHARS', 'a b c d e f g h i j k l m n o p q r s t u v w x y z').split(' ') nodes = {i: {c: [] for c in chars} for i in dic['defs']} nodes['ALPHABETICAL'] = {c: [] for c in chars} comp = get_comparator(''.join(chars)) for word, data in dic['dict'].items(): nodes['ALPHABETICAL'][word[0]].append(word) for kind in data: nodes[kind][word[0]].append(word) for i in nodes.values(): for j in i.values(): j.sort(key=comp) return nodes </code></pre> <p><code>get_comparator</code>関数は、<code>PHONETICAL_CHARS</code>ヘッダフィールドで定義された辞書順に従って文字列を比較できるようにするラッパークラスを返す関数です。品詞ごとに単語の頭の文字の配列を作り、そこに単語のみを格納しているだけですね。<code>ALPHABETICAL</code>は全単語索引の情報で、定義されたすべての単語が登録されています。</p> <p>次に意味情報と関連語情報を抽出する関数を定義します。</p> <pre><code class="python">def generate_dict_content(dic: dict): chars = dic['header'].get( 'PHONETICAL_CHARS', 'a b c d e f g h i j k l m n o p q r s t u v w x y z').split(' ') defs = dic['defs'] contents = {i: [] for i in chars} comp = get_comparator(''.join(chars)) for word, data in dic['dict'].items(): contents[word[0]].append({ 'surface': word, 'meaning': [ (kind, meta['meaning'].split(' ')) for kind, meta in data.items() ], 'reference': [ref for d in data.values() for ref in d['reference']] }) for c in contents.values(): c.sort(key=lambda x: comp(x['surface'])) return contents </code></pre> <p>この関数についての解説は、<code>generate_indices</code>関数とほぼ同じ動作のため割愛します。</p> <p>これでTSVから単語情報を抽出する関数がそろいました。次はこれをmarkdownとして出力する関数を作っていきます。</p> <h2 id="抽出した情報をMarkdownにしてみる"><a href="#%E6%8A%BD%E5%87%BA%E3%81%97%E3%81%9F%E6%83%85%E5%A0%B1%E3%82%92Markdown%E3%81%AB%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">抽出した情報をMarkdownにしてみる</a></h2> <p>索引を生成する関数を考えてみました。こんな感じです。</p> <pre><code class="python">def generate_index_file(kind: str, defs: dict, index: list): ret = f"# {defs[kind]}\n" if kind == 'ALPHABETICAL': ret += "\n## 品詞別インデックス\n" for fp, kd in defs.items(): if fp != 'ALPHABETICAL': ret += f"* [{kd}](./{fp.lower()}.md)\n" for representative, content in index.items(): ret += f"\n## {representative.upper()}\n" for word in content: ret += f"* [{word}](./content/{word[0].upper()}.md#{word})\n" return ret </code></pre> <p>すごいシンプルにかけて満足しています。あまりPythnoicではないと思いますが、そこは気にしないことにします。あと、<code>ALPHABETICAL</code>のページに品詞別インデックスへのリンクを表示することにしました。引数の意味ですが、<code>kind</code>は品詞ID、<code>defs</code>は品詞の定義、<code>index</code>には単語のリストを渡します。</p> <p>続いて、単語の解説ページを生成する関数を考えてみました。</p> <pre><code class="python">def generate_content_file(representative: str, words: list, defs: dict): ret = f"# {representative.upper()}\n" for word in words: ret += f"\n## {word['surface']}\n" ret += "意味: \n" for i, (k, m) in enumerate(word['meaning']): ret += f"{i + 1}. <{defs[k]}> \n" for ml in m: ret += f" {ml} \n" refs = [i for i in word['reference'] if i] if refs: ret += "\n関連語: \n" for ref in refs: ret += f"* [{ref}](./{ref[0].upper()}.md#{ref})\n" return ret </code></pre> <p>引数の意味ですが、<code>representative</code>は代表の文字(ようするにそのページの単語に共通の頭文字)、<code>words</code>は単語とそのメタ情報、<code>defs</code>は品詞の定義をとります。</p> <p>さて、最後にこれらの関数の動作を連結する関数を書きましょう。それで完成です。</p> <pre><code class="python">def generate_markdown_files(dic: dict): indices = generate_indices(dic) content = generate_dict_content(dic) chars = dic['header'].get( 'PHONETICAL_CHARS', 'a b c d e f g h i j k l m n o p q r s t u v w x y z').split(' ') defs = dic['defs'].copy() defs['ALPHABETICAL'] = "全単語索引" return { 'content': { i: generate_content_file(i, content[i], defs) for i in chars }, 'indices': { ('index' if i == 'ALPHABETICAL' else i.lower()): generate_index_file(i, defs, content) for i, content in indices.items() } } </code></pre> <p>この関数はparseされたTSVをmarkdown形式の文字列に変換する関数です。ここまでに定義した関数を連結して整形された形にするのが役割ですね。</p> <p>さて、これをファイルにdumpする関数を書いて、それで本当に完成です。</p> <pre><code class="python">def dump_markdown(dic_path: str, dump_dir: dir): dic_path = abspath(dic_path) dump_dir = abspath(dump_dir) files = generate_markdown_files(parse_dict(open_dict(dic_path))) print('generating indices...') for rep, con in files['indices'].items(): path = join(dump_dir, f'{rep}.md') d = dirname(path) if not exists(d): md(d) print(f'writing file: {path}') with open(path, 'w', encoding='utf-8') as f: f.write(con) print('generating content...') for rep, con in files['content'].items(): path = join(dump_dir, 'content', f'{rep.upper()}.md') d = dirname(path) if not exists(d): md(d) print(f'writing file: {path}') with open(path, 'w', encoding='utf-8') as f: f.write(con) print('done.') </code></pre> <p>この関数は、コンソールコマンドとして実行されることを想定したものになっています。</p> <p>最後に、ここまで書いたスクリプトの全体を示しておきます。</p> <pre><code class="python">#-*- coding: utf-8;-*- from os import makedirs as md from os.path import join, exists, dirname, abspath import csv class ParseError(SyntaxError): pass def get_comparator(_order): class _Comparator(str): def __gt__(self, other): order = list(_order) for s, o in zip(self, other): oi = order.index(o) si = order.index(s) if oi > si: return True if si > oi: return False return len(self) > len(other) def __lt__(self, other): order = list(_order) for s, o in zip(self, other): oi = order.index(o) si = order.index(s) if oi < si: return True if si < oi: return False return len(self) < len(other) return _Comparator def open_dict(dic_path: str) -> list: with open(dic_path, encoding='utf-8') as f: reader = csv.reader(f, delimiter='\t') return list(reader) def parse_dict(dic: list) -> dict: ret = {} state = 'none' for i in dic: if i[0] == 'BEGIN_HEADER': if state != 'none': raise ParseError('Unexpected BEGIN_HEADER tag.') state = 'header' ret['header'] = {} continue if i[0] == 'END_HEADER': if state != 'header': raise ParseError('Unexpected END_HEADER tag.') state = 'none' continue if i[0] == 'BEGIN_DICTIONARY': if state != 'none': raise ParseError('Unexpected BEGIN_DICTIONARY tag.') state = 'dictionary' ret['dict'] = {} continue if i[0] == 'END_DICTIONARY': if state != 'dictionary': raise ParseError('Unexpected END_DICTIONARY tag.') state = 'none' continue if i[0] == 'BEGIN_DEFINITION': if state != 'none': raise ParseError('Unexpected BEGIN_DEFINITION tag.') state = 'definition' ret['defs'] = {} continue if i[0] == 'END_DEFINITION': if state != 'definition': raise ParseError('Unexpected END_DEFINITION tag.') state = 'none' continue if state == 'none': continue if state == 'header': ret['header'][i[0]] = i[1] continue if state == 'dictionary': if i[0] not in ret['dict']: ret['dict'][i[0]] = {} ret['dict'][i[0]][i[1]] = { 'meaning': i[2], 'reference': i[3].split(' ') } continue if state == 'definition': ret['defs'][i[0]] = i[1] if state != 'none': raise ParseError(f'A match pair tag of END_{state.upper()} not found.') return ret def generate_indices(dic: dict): chars = dic['header'].get( 'PHONETICAL_CHARS', 'a b c d e f g h i j k l m n o p q r s t u v w x y z').split(' ') nodes = {i: {c: [] for c in chars} for i in dic['defs']} nodes['ALPHABETICAL'] = {c: [] for c in chars} comp = get_comparator(''.join(chars)) for word, data in dic['dict'].items(): nodes['ALPHABETICAL'][word[0]].append(word) for kind in data: nodes[kind][word[0]].append(word) for i in nodes.values(): for j in i.values(): j.sort(key=comp) return nodes def generate_dict_content(dic: dict): chars = dic['header'].get( 'PHONETICAL_CHARS', 'a b c d e f g h i j k l m n o p q r s t u v w x y z').split(' ') defs = dic['defs'] contents = {i: [] for i in chars} comp = get_comparator(''.join(chars)) for word, data in dic['dict'].items(): contents[word[0]].append({ 'surface': word, 'meaning': [ (kind, meta['meaning'].split(' ')) for kind, meta in data.items() ], 'reference': [ref for d in data.values() for ref in d['reference']] }) for c in contents.values(): c.sort(key=lambda x: comp(x['surface'])) return contents def generate_content_file(representative: str, words: list, defs: dict): ret = f"# {representative.upper()}\n" for word in words: ret += f"\n## {word['surface']}\n" ret += "意味: \n" for i, (k, m) in enumerate(word['meaning']): ret += f"{i + 1}. <{defs[k]}> \n" for ml in m: ret += f" {ml} \n" refs = [i for i in word['reference'] if i] if refs: ret += "\n関連語: \n" for ref in refs: ret += f"* [{ref}](./{ref[0].upper()}.md#{ref})\n" return ret def generate_index_file(kind: str, defs: dict, index: list): ret = f"# {defs[kind]}\n" if kind == 'ALPHABETICAL': ret += "\n## 品詞別インデックス\n" for fp, kd in defs.items(): if fp != 'ALPHABETICAL': ret += f"* [{kd}](./{fp.lower()}.md)\n" for representative, content in index.items(): ret += f"\n## {representative.upper()}\n" for word in content: ret += f"* [{word}](./content/{word[0].upper()}.md#{word})\n" return ret def generate_markdown_files(dic: dict): indices = generate_indices(dic) content = generate_dict_content(dic) chars = dic['header'].get( 'PHONETICAL_CHARS', 'a b c d e f g h i j k l m n o p q r s t u v w x y z').split(' ') defs = dic['defs'].copy() defs['ALPHABETICAL'] = "全単語索引" return { 'content': { i: generate_content_file(i, content[i], defs) for i in chars }, 'indices': { ('index' if i == 'ALPHABETICAL' else i.lower()): generate_index_file(i, defs, content) for i, content in indices.items() } } def dump_markdown(dic_path: str, dump_dir: dir): dic_path = abspath(dic_path) dump_dir = abspath(dump_dir) files = generate_markdown_files(parse_dict(open_dict(dic_path))) print('generating indices...') for rep, con in files['indices'].items(): path = join(dump_dir, f'{rep}.md') d = dirname(path) if not exists(d): md(d) print(f'writing file: {path}') with open(path, 'w', encoding='utf-8') as f: f.write(con) print('generating content...') for rep, con in files['content'].items(): path = join(dump_dir, 'content', f'{rep.upper()}.md') d = dirname(path) if not exists(d): md(d) print(f'writing file: {path}') with open(path, 'w', encoding='utf-8') as f: f.write(con) print('done.') if __name__ == '__main__': from sys import argv dump_markdown(argv[1], argv[2]) </code></pre> <p>はー。疲れました。はい、これでおそらくどの方面にも需要がないツールの完成です。「欲しかったから作った」の真骨頂ですね。最後までお付き合いいただき、ありがとうございました。</p> frodo821