utamaro’s blog

誰かの役に立つ情報を発信するブログ

pythonを使ってcsvのファイルを生成する方法

pythonを使ったcsvのファイルを生成する方法を紹介します。

この記事では、csvの読み込みは扱いません。csvファイルを作成する方法を紹介しています。

csvを扱うための便利なライブラリをインストールします。

pip install pandas

csvを読み書きするためにpandasをインストールします。

pandasをインストールするとnumpyもインストールされます。

csvファイルを生成する関数を作成します。

output_csv_root = 'csv/files'
def gen_csv(dataset, file_name):
    df = pd.DataFrame(dataset)
    file_path = os.path.join(output_csv_root, file_name + '.csv')
    df.to_csv(file_path)

gen_csvを実行すると、{output_csv_root}/{filename}.csvが作成されます。

datasetに指定する値は、二次元の配列を指定します。

[[1,2,3], [4,5,6]]

これがこうなります。

#, 1, 2, 3
1, 1, 2, 3
2, 4, 5, 6

一行目の#, 1, 2, 3は見出しです。

この見出しは、自分で指定することもできます。

指定する場合は、dataFrameを作成する際にcolumnsを指定します。

df = pd.DataFrame([rows], columns=['A', 'B', 'C'])

また、行番号が必要ない場合は、このようにindexをFalseにします。

df.to_csv(file_path, index=False)

mecabを使って解析する方法

mecabのインストール方法等はいろいろなサイトに書かれているので、pythonのプログラミング部分を紹介します。

意外と実装について書かれてる記事が少なくてびっくりしました。なので、help()を使って使い方を試しました。

以下の環境で作業しています。

mac osX
brewでmecabとmecab-ipadicをインストール
mecab-python3
mecab-ipadic-neologd

mecabインスタンスにはparseToNode(self, *args)parseToString(self, *args)があります。

このうち、parseToString(self, *args)は文字列にパースされるので少し使いづらいです。

なので、parseToNode(self, *args)を使用します。

mecab = MeCab.Tagger('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')
novel_data = novel_list.get(0)
node = mecab.parseToNode('Google Analytics(グーグルアナリティクス)は、Googleが無料で提供するWebページのアクセス解析サービス。')
while node:
    surface = node.surface
    feature = node.feature
    print(surface, feature)
    node = node.next

このようなデータを取得できます。

 BOS/EOS,*,*,*,*,*,*,*,*
Google Analytics 名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス
( 記号,括弧開,*,*,*,*,(,(,(
グーグルアナリティクス 名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス
) 記号,括弧閉,*,*,*,*,),),)
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
、 記号,読点,*,*,*,*,、,、,、
Google 名詞,固有名詞,一般,*,*,*,Google,グーグル,グーグル
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
無料 名詞,一般,*,*,*,*,無料,ムリョウ,ムリョー
で 助詞,格助詞,一般,*,*,*,で,デ,デ
提供 名詞,サ変接続,*,*,*,*,提供,テイキョウ,テイキョー
する 動詞,自立,*,*,サ変・スル,基本形,する,スル,スル
Webページ 名詞,固有名詞,一般,*,*,*,Webページ,ウェブページ,ウェブページ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
アクセス解析 名詞,固有名詞,一般,*,*,*,アクセス解析,アクセスカイセキ,アクセスカイセキ
サービス 名詞,サ変接続,*,*,*,*,サービス,サービス,サービス
。 記号,句点,*,*,*,*,。,。,。
 BOS/EOS,*,*,*,*,*,*,*,*

surfaceは単語を取得でき、featureではその解析結果が取得できます。

結果を見るとわかるかと思いますが、BOS/EOS,*,*,*,*,*,*,*,*というのが出てきています。

これを除きつつ、データを使いやすいように取得します。

def main():
    mecab = MeCab.Tagger('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')
    node = mecab.parseToNode('Google Analytics(グーグルアナリティクス)は、Googleが無料で提供するWebページのアクセス解析サービス。')

    node_list = []
    while node:
        surface = node.surface
        feature = node.feature
        c = feature.split(',')[0]
        if not c == 'BOS/EOS':
            node_list.append({
                'surface': surface,
                'feature': feature,
            })
        node = node.next
    for n in node_list:
        print(n)

if __name__ == '__main__':
    main()

実行結果がこちらです。

{'surface': '', 'feature': '名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス'}
{'surface': '\t', 'feature': '記号,括弧開,*,*,*,*,(,(,('}
{'surface': 'グーグルアナリティクス', 'feature': '名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス'}
{'surface': ')', 'feature': '記号,括弧閉,*,*,*,*,),),)'}
{'surface': 'は', 'feature': '助詞,係助詞,*,*,*,*,は,ハ,ワ'}
{'surface': '、', 'feature': '記号,読点,*,*,*,*,、,、,、'}
{'surface': 'Google', 'feature': '名詞,固有名詞,一般,*,*,*,Google,グーグル,グーグル'}
{'surface': 'が', 'feature': '助詞,格助詞,一般,*,*,*,が,ガ,ガ'}
{'surface': '無料', 'feature': '名詞,一般,*,*,*,*,無料,ムリョウ,ムリョー'}
{'surface': 'で', 'feature': '助詞,格助詞,一般,*,*,*,で,デ,デ'}
{'surface': '提供', 'feature': '名詞,サ変接続,*,*,*,*,提供,テイキョウ,テイキョー'}
{'surface': 'する', 'feature': '動詞,自立,*,*,サ変・スル,基本形,する,スル,スル'}
{'surface': 'Webページ', 'feature': '名詞,固有名詞,一般,*,*,*,Webページ,ウェブページ,ウェブページ'}
{'surface': 'の', 'feature': '助詞,連体化,*,*,*,*,の,ノ,ノ'}
{'surface': 'アクセス解析', 'feature': '名詞,固有名詞,一般,*,*,*,アクセス解析,アクセスカイセキ,アクセスカイセキ'}
{'surface': 'サービス', 'feature': '名詞,サ変接続,*,*,*,*,サービス,サービス,サービス'}
{'surface': '。', 'feature': '記号,句点,*,*,*,*,。,。,。'}

いい感じです。

使うときはforでループして、dictからデータを取得すると良いでしょう。

もしくはこのようにデータを追加して、tupleで使用してもよいと思います。

node_list.append(
    (surface, feature,)
)

使用するときはこんな感じです。

for s, f in node_list:
    print(s, f)

DjangoでCSRFを含んだリクエストをtagを使って実行する方法

CSRFトークンを含んだAPIの実行方法で、ドキュメントに載っているような方法ではなく、tagを使った方法を紹介します。

tagを使った方法というのは、{% cookie 'csrftoken' %}でhtml内にtokenを入れて、その値を使う方法です。

修正が必要な箇所は以下の3箇所です。

Djangoの対応

まずは、テンプレートから使用できるようにtagを作成します。

作成する場所は、プロジェクトディレクトリがある場所にtemplate_tags/tags.pyというファイルを作成しました。

@register.simple_tag(takes_context=True)
def cookie(context, cookie_name):  # could feed in additional argument to use as default value
    request = context['request']
    result = request.COOKIES.get(cookie_name, '')  # I use blank as default value
    return result

次にsettings.pyを編集します。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR,  'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
            'libraries': {  # ←これを追加
                'tags': 'template_tags.tags',
            }
        },
    },
]

tagのファイルを読み込むようにしています。

これでDjango側の対応は終わりです。

html(template)の対応

Djangoで使用するtemplate html内に以下のように書きます。

<meta name="csrf" content="{% cookie 'csrftoken' %}">

最終的にhtmlのmetaタグにcsrfを追加して、jsで読み込んで使用します。

<meta name="csrf" content="W3ni7NdZeaPWYgfoRShxFVNLHCimZWxpZsL8QMJ4VtlBguv9lo7NoYGO7Gl0eD0D">

javascriptの対応

javascriptの対応も簡単です。

function call(callback) {
    $.ajax({
        url:'/api/hoge',
        type:'POST',
        beforeSend: function(request) {
            let csrftoken = $("meta[name='csrf']").attr('content')
            request.setRequestHeader("X-CSRFToken", csrftoken);
        },
        data:{
            'limit': 100,
        }
    })
    .done(function(data) {
        console.log(data);
        callback(data);
    })
    .fail(function(data) {
        console.log(data);
    })
}

この部分を追加すると対応が可能です。

beforeSend: function(request) {
    let csrftoken = $("meta[name='csrf']").attr('content')
    request.setRequestHeader("X-CSRFToken", csrftoken);
},

また、状況に合わせて以下のコードを入れると良いとおもいます。

csrfが必要なリクエストと、不要なリクエストを分けて、必要ならcsrftokenありでリクエストするものです。

function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
function ajaxInit() {
    $.ajaxSetup({
        beforeSend: function(xhr, settings) {
            let csrftoken = getCookie('csrftoken');
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                xhr.setRequestHeader("X-CSRFToken", csrftoken);
            }
        }
    });
}

tagを使う方法はstackoverflowで見つけました。

python - Django read cookie in template tag - Stack Overflow

失敗パターン

csrf_tokenで対応しようとして失敗

Djangoではformでリクエストを実行する際に{% csrf_token %}というものを使えます。

これを使えばtoken値を上記みたいなコードを使わずに簡単に実装できるのではと考えました。

metaタグにcsrf_tokenを入れれると思ったのですが…

<meta name="csrf" content="{% csrf_token %}">

{% csrf_token %}が展開されると<input>タグが作成されるのでダメでした。

画面上に/>という文字列が表示されていたり、domがめちゃくちゃになったり(body直下にheadがあった)しました。

あえなく断念しました。

DjangoでSerializerを使ったバリデーション方法について

DjangoではRestApiを作成するための便利なライブラリとしてDjango REST frameworkというものがあります。

このライブラリを使うと、簡単にapiを作成することができます。

この記事ではDjango REST frameworkで用意されているserializerを使ったバリデーション方法を紹介します。

また、Django REST frameworkでviewを表示する方法についても紹介します。

views.py

views.pyではviewsetを使って実装しています。

actionを使って、getpostといったメソッドを作らないようにしました。

actionを使った場合はurlsを少し工夫する必要がありますが、それについてはurls.pyを見てください。

from django.http import JsonResponse
from django.shortcuts import render
# Create your views here.
from rest_framework import viewsets
from rest_framework.decorators import action

from apps.xxxxxxx.serializer import SampleSerializer


class SampleViewSet(viewsets.ModelViewSet):
    serializer_class = SampleSerializer

    @action(detail=False, name='sample_post_api')
    def sample_post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return JsonResponse({"status": 400}, status=404)
        return JsonResponse(serializer.data, status=200)

    @action(detail=False, name='sample_get_open_view')
    def sample_get(self, request, *args, **kwargs):
        return render(request, 'home.html', status=200)

saple_getではrenderというショートカットを使用してhome.htmlを表示しています。

restframeworkのサンプルでは以下のように実装されているので、以下の方法でも良いです。

    @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
    def highlight(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

serializer.py

countというフィールドを持っています。

countは-1か1の値を受け付けます。

IntegerFieldではmin_valuemax_valueを使えますが、0は許可してほしくありません。

そういった場合はvalidation_xxxxというようにバリデーションを追加できます。

from rest_framework import serializers

class SampleSerializer(serializers.Serializer):

    count = serializers.IntegerField(min_value=-1, max_value=1)

    def validate_count(self, count):
        if count == 0:
            raise serializers.ValidationError("count is not allowed zero.")
        return count

新しいシリアライザクラスを作成したほうが良いのかもしれません。

urls.py

views.pyで作成したapiを登録します。

restframeworkではrouterというものを使うのが一般的だと思いますが、この記事ではurlpatternsをそのまま使ってます。

routerについてはこちらを参照してください。

https://www.django-rest-framework.org/api-guide/routers/

また、検索するとよく見つかるurl()というのの代わりにpathre_pathを使用しています。

理由はurlが廃止される可能性があるからです。

from django.contrib import admin
from django.urls import path, re_path

from apps.xxxxxxxx.views import SampleViewSet

# admin
urlpatterns = [
    path('admin/', admin.site.urls),
]

# sample api and view
urlpatterns += {
    re_path(r'sample/?$', SampleViewSet.as_view(actions={
        'get': 'sample_get',
    })),
    path('api/sample/post', SampleViewSet.as_view(actions={
        'post': 'sample_post'
    })),
}

.as_viewの’actionを使って登録します。

見るだけでわかると思います。

getでリクエストが来たら、sample_getメソッドへ。

postでリクエストが来たら、sample_postメソッドへ。

ちなみに、以下のように実装しないのには理由があります。

urlpatterns += {
    re_path(r'sample/?$', SampleViewSet.as_view(actions={
        'get': 'sample_get',
        'post': 'sample_post'
    })),
}

このようにすると、/sampleにリクエストした際に、restframeworkが用意している管理画面を表示できません。

postをテストしたいのに、getが最初に表示されるので、管理画面が表示されないのです。

ハロウィンだしネタアプリを作ってみた話

タイトルの通り、ネタアプリを作ってみました。

作ったアプリケーションはこちらです。

http://www.everyday.work/

「進捗どうですか?」というアプリケーションで、作成時間は1~2時間ぐらいです。

f:id:miyaji-y26:20181101082657p:plain

使用したフレームワークや環境を箇条書きにするとこんな感じです。

  • Django2.1.2
  • heroku free
  • python3.6.4
  • heroku postgresql
  • domain 1円

これまで書いてきた記事を参照しつつ短時間で作ることができました。

ブログを書き続けてよかったなと感じた瞬間です。

あと、webサービスを作りたい人や、プログラミングの勉強をしている人は今回私が作ったような小さなアプリケーションから始めると良いと思います。

小さなアプリケーションでも、大きなアプリケーションでもやることは変わらないからです。

設計して、開発して、テストして、デプロイして、、あれこれと、リリースまでにやることが体験できます。

最初は時間がかかりますが、何度かやっているとやり方が分かるので、3回ぐらいやるとパパっと作れるようになります。

そして、慣れたら大きなサービスを作ってみましょう。

きっとうまくいきます

はみ出す文字列を三点リーダーにする方法

領域をはみ出したときに三点リーダー(…)にする方法を紹介します。

既出かと思いますが、詳しい内容とかあまり見つからない(こうやればできるというのは見つかる)ので、私なりに調べた内容を載せます。

領域をはみ出したときに三点リーダー(…)にする場合は以下のように設定します。

span.ellipsis {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

spanタグのclassにellipsisを設定した場合、はみ出した部分を三点リーダーにします。

spanタグが対象なのは特に意味はないです。強いて言うならellipsisの使い方をわかりやすくするためです。

※ このスタイルでは複数行の対応はできません。

overflow:hiddenについて

領域をはみ出した際に、はみ出したものを表示しないようにします。

scrollとか、autoにするとスクロールバーが出るやつです。

余談ですが、最近はscrollとかautoは使わなくてperfect-scrollbar.jsというのを使ってます。

white-space:nowrapについて

空白文字の扱いを決めています。

nowrapを指定すると、一行で表示されます。

preでもできると思いますが、改行やタブが残るのでうまくいかない場合があると思います。

こちらのページがわかりやすいと思います。

https://developer.mozilla.org/ja/docs/Web/CSS/white-space

text-overflow: ellipsisについて

はみ出した部分を三点リーダーにします。

おまけ

youtubeのサイドバーを見てみると、同じようなことをやってるのがわかります。

さらに、右側に24pxの余白をいれて、見やすくするというテクニックを発見できます。

24pxというのは1.5rem(1rem=16pxのとき)なので何か意味があるのだと思います。

なんの意味があるのかはわからないです。

margin-right: 24px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 1.4rem;
font-weight: 400;
line-height: 2.1rem;
-ms-flex: 1 1 0.000000001px;
-webkit-flex: 1;
flex: 1;
-webkit-flex-basis: 0.000000001px;
flex-basis: 0.000000001px;

lxmlを使ったxmlのパース方法

lxmlというライブラリを使ってxmlをパースしたときのメモを記事にしています。

lxmlはBeautifulSoupというスクレイピングのライブラリでも使われたりしなかったりします。

xmlファイルを取得する

pythonでファイルを読み込む方法はいろいろありますが、再帰的にファイルを取得しつつ、指定したディレクトリ以下のxmlファイルをすべて取得します。

from lxml import etree
from pathlib import Path

def get_xml_files(target):
    files = list(Path(target).glob('**/*.xml'))
    return files

get_xml_filesを実行すると、指定したディレクトリ以下のxmlファイルのパスがすべて取得できます。

このパスをlxmlで読み込んで、利用します。

lxmlでxmlをパースする

get_xml_filesを使って、ファイルまでのパスを取得後にパース処理をします。

xml_files = get_xml_files(xml_target)
for xml_file in xml_files:
    xml_file_name = str(xml_file)
    tree = etree.parse(xml_file_name)

xml_filePosixPothクラスのインスタンスなので、そのままetree.parseに渡せません。

一度strで文字列にしてからparseします。

コメントの削除方法

xmlのコメントが残っていると、forでループする際にcommentが引っかかります。

これを解消するために、removeしようと考えたのですが、remove系の関数がありませんでした。

理想ではtree.rm_comments()とかetree.rm_comments()があったらよかったです。

というわけで、対処方法が以下のプログラムです。

def remove_comments(tree):
    comments = tree.xpath('//comment()')
    for comment in comments:
        parent = comment.getparent()
        parent.remove(comment)

tagを判定する

以下のプログラムは、mybatisというjavaのormで使うxmlをパースする際に使った関数です。

xmlの中身はこのようなものです。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd">

<sqlMap namespace="jp.hoge.com">
    <select id="selectId" resultClass="Sample" parameterClass="string">
        SELECT
            id
        FROM
            sample
        WHERE
            id = 1
    </select>
</sqlMap>

このxmlをパースしてステートメントごとに配列にしています。

def get_statements(tree):
    statement_elements = []
    for element in tree.iter():
        if element.tag in ['select', 'insert', 'update', 'delete', 'procedure']:
            statement_elements.append(element)
    return statement_elements

属性を取得する際はelement.get('id')のようにします。

tree.iter()でelementを取得できますが、少し癖があります。

深さ優先探索のようにタグを取得していくので、書くのが難しかったです。

一度に欲しい情報を取得するのではなく、個別に取得する方法を採用しました。

例えば、以下のようなxmlがあった場合、sqlをdictに格納して、selectをパースしている最中にincludeを見つけたらdictを参照して、中身を置き換えます。

<sqlMap namespace="jp.hoge.com">
    <sql id="sampleInclude">
        where
            id = 2
    </sql>
    <select id="selectId" resultClass="Sample" parameterClass="string">
        SELECT
            id
        FROM
            sample
        <include refid="sampleInclude" />
    </select>
</sqlMap>