diadia

興味があることをやってみる。自分のメモを残しておきます。

python:画像の保存

どうすれば画像を保存できるか書いておく。

データの種類

画像の保存にはバイナリタイプのデータが求められる。urllib3でurlにアクセスするとデフォルトでバイナリタイプの戻り値なので、画像を保存するには向いている。

保存方法

保存方法はファイルをバイナリ形式でバイナリデータを書き込めばよい。したがって以下のような形をとる。

# requestsを使った場合
import requests

res = requests.get(url).content
with open(IMAGE_FILE, "wb") as file:
    file.write(res)

# urllib3を使った場合    
import urllib3 
res = urllib3.urlopen(url)
with open(IMAGE_FILE, "wb") as file:
    file.write(res)

seleniumでもバイナリデータで情報を取得できるので画像取得はできる。 https://kurozumi.github.io/selenium-python/api.html#selenium.webdriver.remote.webdriver.WebDriver.get_screenshot_as_png

python: windowsとmacから同一のスクリプトを利用する方法

前提

dropboxにデータやスクリプトを配置している。

問題点

windowsmacではcsvモジュールを使う場合以下の差異が存在する。
1.ファイルパスが異なる。
2.csvファイル書き込みの際にlineterminatorが必要か否か。
1.2も今まではwindows用、mac用のスクリプトを別々に準備し使うOSによってスクリプトを使い分けてきた。今回はこの問題を解決する。

import csv, platform

pl = platform.system()
if pl == 'Windows':
    # windowsファイルパス
    CSV_FILE = "C*************.csv"
elif pl == 'Darwin':
    # macファイルパス
    CSV_FILE = "Users/********.csv"



test_list = ["ロシア", "アメリカ", "中国", "ブラジル"]
with open("path/test.csv", "a", encoding="utf-8") as f:
    if pl == 'Windows':
        writer = csv.writer(f, lineterminator="\n")
    elif pl == 'Darwin':
        writer = csv.writer(f)
        
    writer.writerow(test_list)

解決方法

見ればわかる。上記のようにplatformモジュールを使えばよい。
https://docs.python.org/ja/3/library/platform.html#platform.system

Python:CSVモジュール使い方

関連記事

csvモジュールの使い方

リストデータをファイルとしてアウトプットしたい場合にcsvファイルにするのは便利。 csvモジュールの使い方をメモしておく。
csvモジュールはpythonの標準ライブラリなのでpip install csvのようなことはせずに利用することができる。
https://docs.python.org/ja/3/library/csv.html

import csv
使用例

import csv

test_list = ["ロシア", "アメリカ", "中国", "ブラジル"]
with open("path/test.csv", "a", encoding="utf-8") as f:
    writer = csv.writer(f, lineterminator="\n")
    writer.writerow(test_list)

csv.writer()関数を使うとwriterオブジェクトを返す。このwriterオブジェクトにはcsvファイルに書き込むためのメソッドがある。wirerow()メソッド又はwriterows()メソッドである。これらのメソッドに与えた引数がcsvファイルに書き込まれることになる。

writerオブジェクトを作成する

1行書くか複数行書くかのメソッドがある。そのメソッドを使うためのオブジェクトがwriterオブジェクト。 writerで返されるのがwriterオブジェクトである。

 csv.writer(ファイルオブジェクト) 

1行書く場合

writer で返されたオブジェクトにwriterowメソッドを使う。

 w.writerow(シーケンス)  

複数行書く場合

writer で返されたオブジェクトにwriterowsメソッドを使う。

 w.writerows(シーケンス)  

csvファイルの読み込み

csvファイルの一行目がカラムの説明になる場合がある。読み込みの際にこの一行目を省いて2行目以降のデータのみ読み込む方法を調べた。 https://docs.python.jp/3/library/csv.html#reader-objects http://www.bokupy.com/detail/67

 import csv 
with open("book1.csv", "r") as f:
    reader = csv.reader(f)
    for row in reader:
        if reader.line_num == 1:
            continue print(row) 

重要な点として、reader.line_numを使うことのほか、continueも重要である。continueはfor構文中にcontinueに出くわしたとき、今の現在のループを中断し、次の繰り替し処理を行う動作になる。

urllib3についてメモ

document

https://urllib3.readthedocs.io/en/latest/index.html

コードの流れを追う

import urllib3  #1

http = urllib3.PoolManager()  #2
r = http.request('GET', 'http://httpbin.org/robots.txt')   #3

#1

import urllib3

https://github.com/urllib3/urllib3/blob/master/src/urllib3/__init__.py#L11
urllib3をインポートしたときにディレクトリ内の__init__.pyでPoolManagerもインポートされる。

#2

http = urllib3.PoolManager()

urllib3.PoolManagerのインスタンスhttpを生成する。これでrequestメソッドの呼び出しの準備になる。

#3

r = http.request('GET', 'http://httpbin.org/robots.txt')

PoolManager.request()を実行しているが、正確にはPoolManagerの親クラスRequestMethod.request()メソッドを実行している。
https://github.com/urllib3/urllib3/blob/master/src/urllib3/request.py#L10

 

urllib3.request.RequestMethods(object).request()

    def request(self, method, url, fields=None, headers=None, **urlopen_kw):
        """
        Make a request using :meth:`urlopen` with the appropriate encoding of
        ``fields`` based on the ``method`` used.
        This is a convenience method that requires the least amount of manual
        effort. It can be used in most situations, while still having the
        option to drop down to more specific methods when necessary, such as
        :meth:`request_encode_url`, :meth:`request_encode_body`,
        or even the lowest level :meth:`urlopen`.
        """
        method = method.upper()

        urlopen_kw["request_url"] = url

        if method in self._encode_url_methods:
            return self.request_encode_url(
                method, url, fields=fields, headers=headers, **urlopen_kw
            )
        else:
            return self.request_encode_body(
                method, url, fields=fields, headers=headers, **urlopen_kw
            )

request()メソッドでは、methodが_encode_url_methodsに含まれているものならばurllib3.request.RequestMethods(object).request_encode_url()メソッドを実行する。具体的に言えばリクエストメソッドが"DELETE", "GET", "HEAD", "OPTIONS"のどれかであれば、RequestMethods(object).request_encode_url()メソッドを実行する。一方で_encode_url_methodsにmethodがない場合は、urllib3.request.RequestMethods(object).request_encode_body()メソッドを実行する。
_encode_url_methodsはセット型データでRequestMethods(object)の属性である。_encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"}
https://github.com/urllib3/urllib3/blob/master/src/urllib3/request.py#L39

 

urllib3.request.RequestMethods(object).request_encode_url()

    def request_encode_url(self, method, url, fields=None, headers=None, **urlopen_kw):
        """
        Make a request using :meth:`urlopen` with the ``fields`` encoded in
        the url. This is useful for request methods like GET, HEAD, DELETE, etc.
        """
        if headers is None:
            headers = self.headers

        extra_kw = {"headers": headers}
        extra_kw.update(urlopen_kw)

        if fields:
            url += "?" + urlencode(fields)

        return self.urlopen(method, url, **extra_kw)

 

urllib3.poolmanager.PoolManager(RequestMethods).urlopen()

    def urlopen(self, method, url, redirect=True, **kw):
        """
        Same as :meth:`urllib3.connectionpool.HTTPConnectionPool.urlopen`
        with custom cross-host redirect logic and only sends the request-uri
        portion of the ``url``.
        The given ``url`` parameter must be absolute, such that an appropriate
        :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it.
        """
        u = parse_url(url)
        conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)

        kw["assert_same_host"] = False
        kw["redirect"] = False

        if "headers" not in kw:
            kw["headers"] = self.headers.copy()

        if self.proxy is not None and u.scheme == "http":
            response = conn.urlopen(method, url, **kw)
        else:
            response = conn.urlopen(method, u.request_uri, **kw)

        redirect_location = redirect and response.get_redirect_location()
        if not redirect_location:
            return response

        # Support relative URLs for redirecting.
        redirect_location = urljoin(url, redirect_location)

        # RFC 7231, Section 6.4.4
        if response.status == 303:
            method = "GET"

        retries = kw.get("retries")
        if not isinstance(retries, Retry):
            retries = Retry.from_int(retries, redirect=redirect)

        # Strip headers marked as unsafe to forward to the redirected location.
        # Check remove_headers_on_redirect to avoid a potential network call within
        # conn.is_same_host() which may use socket.gethostbyname() in the future.
        if retries.remove_headers_on_redirect and not conn.is_same_host(
            redirect_location
        ):
            headers = list(six.iterkeys(kw["headers"]))
            for header in headers:
                if header.lower() in retries.remove_headers_on_redirect:
                    kw["headers"].pop(header, None)

        try:
            retries = retries.increment(method, url, response=response, _pool=conn)
        except MaxRetryError:
            if retries.raise_on_redirect:
                raise
            return response

        kw["retries"] = retries
        kw["redirect"] = redirect

        log.info("Redirecting %s -> %s", url, redirect_location)
        return self.urlopen(method, redirect_location, **kw)

 conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)のconnはHTTP(s)ConnectionPoolインスタンスである。
HTTP(s)ConnectionPool.urlopen()を実行するとresponseが得られる。このconnオブジェクトを得るには以下のような複数のメソッドを実行してreturn poolとして得る。

 

https://github.com/urllib3/urllib3/blob/master/src/urllib3/poolmanager.py#L213

    def connection_from_host(self, host, port=None, scheme="http", pool_kwargs=None):
        """
        Get a :class:`ConnectionPool` based on the host, port, and scheme.
        If ``port`` isn't given, it will be derived from the ``scheme`` using
        ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is
        provided, it is merged with the instance's ``connection_pool_kw``
        variable and used to create the new connection pool, if one is
        needed.
        """

        if not host:
            raise LocationValueError("No host specified.")

        request_context = self._merge_pool_kwargs(pool_kwargs)
        request_context["scheme"] = scheme or "http"
        if not port:
            port = port_by_scheme.get(request_context["scheme"].lower(), 80)
        request_context["port"] = port
        request_context["host"] = host

        return self.connection_from_context(request_context)

 

 

https://github.com/urllib3/urllib3/blob/master/src/urllib3/poolmanager.py#L236

    def connection_from_context(self, request_context):
        """
        Get a :class:`ConnectionPool` based on the request context.
        ``request_context`` must at least contain the ``scheme`` key and its
        value must be a key in ``key_fn_by_scheme`` instance variable.
        """
        scheme = request_context["scheme"].lower()
        pool_key_constructor = self.key_fn_by_scheme[scheme]
        pool_key = pool_key_constructor(request_context)

        return self.connection_from_pool_key(pool_key, request_context=request_context)

 

 

https://github.com/urllib3/urllib3/blob/master/src/urllib3/poolmanager.py#L249

    def connection_from_pool_key(self, pool_key, request_context=None):
        """
        Get a :class:`ConnectionPool` based on the provided pool key.
        ``pool_key`` should be a namedtuple that only contains immutable
        objects. At a minimum it must have the ``scheme``, ``host``, and
        ``port`` fields.
        """
        with self.pools.lock:
            # If the scheme, host, or port doesn't match existing open
            # connections, open a new ConnectionPool.
            pool = self.pools.get(pool_key)
            if pool:
                return pool

            # Make a fresh ConnectionPool of the desired type
            scheme = request_context["scheme"]
            host = request_context["host"]
            port = request_context["port"]
            pool = self._new_pool(scheme, host, port, request_context=request_context)
            self.pools[pool_key] = pool

        return pool

戻り値はpolであるがこれは、HTTPConnectionPool又はHTTPSConnectionPoolクラスのインスタンスである。poolを新しく生成する場合は# Make a fresh ConnectionPool of the desired type以下の部分で生成される。もっと言えば_new_poolメソッドである。

 

 

https://github.com/urllib3/urllib3/blob/master/src/urllib3/poolmanager.py#L177

    def _new_pool(self, scheme, host, port, request_context=None):
        """
        Create a new :class:`ConnectionPool` based on host, port, scheme, and
        any additional pool keyword arguments.
        If ``request_context`` is provided, it is provided as keyword arguments
        to the pool class used. This method is used to actually create the
        connection pools handed out by :meth:`connection_from_url` and
        companion methods. It is intended to be overridden for customization.
        """
        pool_cls = self.pool_classes_by_scheme[scheme]
        if request_context is None:
            request_context = self.connection_pool_kw.copy()

        # Although the context has everything necessary to create the pool,
        # this function has historically only used the scheme, host, and port
        # in the positional args. When an API change is acceptable these can
        # be removed.
        for key in ("scheme", "host", "port"):
            request_context.pop(key, None)

        if scheme == "http":
            for kw in SSL_KEYWORDS:
                request_context.pop(kw, None)

        return pool_cls(host, port, **request_context)

requestsを見てみる

そもそも

requestsはurllib3をもとに作られている。requests.__init__.pyを見ればわかる。それでHTTPリクエストを送る関係のライブラリがpythonには複数ある。
urllib2, urllib(.request), urllib3. requests。これらについて理解するのも後々役に立つだろう。

  • urllib2...python2で使われていたもの。python3では使えない。
  • urllib(.request)...python3でurllib2の関数を編成して新しい単位にした。
  • urllib3...上記とは違い非標準ライブラリ、つまり3rdパーティ製ライブラリ。
  • requests... urllib3をもとにさらに使いやすくしたもの。

requestsの中身はどうなっているのか

一つのライブラリを使えるようになるだけなくどのように動いているか調べてみる。

ソース

https://github.com/kennethreitz/requests/tree/master/requests

クラスの整理

requests/models.py

https://github.com/kennethreitz/requests/blob/master/requests/models.py
requests/models.pyでクラスが定義されている。

class RequestEncodingMixin(object)
class RequestHooksMixin(object)
class Request(RequestHooksMixin)
class PreparedRequest(RequestEncodingMixin, RequestHooksMixin)
class Response(object)

requests/session.py

class SessionRedirectMixin(object)
class Session(SessionRedirectMixin)

requests/adapters.py

class BaseAdapter(object)
class HTTPAdapter(BaseAdapter)

requests/cookies.py

class MockRequest(object)
class MockResponse(object)
class CookieConflictError(RuntimeError)
class RequestsCookieJar(cookielib.CookieJar, MutableMapping)

requests.get(URL)に限って言えば、各クラスは以下の関係がある。
再考中。

コードの流れの整理

 import requests
 requests.get(URL)
1. [ import requests ]  =======>> [ requests.__init__.pyにてapi.get関数をインポート ]
2. [ requests.get(URL) ]      ============ [ get関数を実行 ]
3. [ get関数を実行 ]          ===============>>   戻り値[ api.request関数 (の実行) ]
4. [ api.request関数 (の実行)  ]  =====>> 戻り値[ Sessionインスタンス生成+requestメソッド実行 ]
5. [ Sessionインスタンス生成+requestメソッド実行]  ======>> 戻り値[ Responseインスタンス作成 ]
1. [ import requests ] =========>> [ requests.__init__.pyにてapi.get関数をインポート ]

https://github.com/kennethreitz/requests/blob/master/requests/__init__.py#L115

from .api import request, get, head, post, patch, put, delete, options
2. [ requests.get(URL) ] ============ [ get関数を実行 ]
 
3.[ get関数を実行 ] =========>> 戻り値[ api.request関数 (の実行) ]
def get(url, params=None, **kwargs):

    kwargs.setdefault('allow_redirects', True)
    return request('get', url, params=params, **kwargs)
4.[ api.request関数 (の実行) ] ====>> 戻り値[Sessionインスタンス生成+requestメソッド実行]
def request(method, url, **kwargs):

    with sessions.Session() as session:
        return session.request(method=method, url=url, **kwargs)
5.[ Sessionインスタンス生成+requestメソッド実行] ======>> 戻り値[ Responseインスタンス作成 ]
(( requestメソッド実行 ))
    
    def request(self, method, url,
            params=None, data=None, headers=None, cookies=None, files=None,
            auth=None, timeout=None, allow_redirects=True, proxies=None,
            hooks=None, stream=None, verify=None, cert=None, json=None):

        # Create the Request.
        req = Request(
            method=method.upper(),
            url=url,
            headers=headers,
            files=files,
            data=data or {},
            json=json,
            params=params or {},
            auth=auth,
            cookies=cookies,
            hooks=hooks,
        )
        prep = self.prepare_request(req)

        proxies = proxies or {}

        settings = self.merge_environment_settings(
            prep.url, proxies, stream, verify, cert
        )

        # Send the request.
        send_kwargs = {
            'timeout': timeout,
            'allow_redirects': allow_redirects,
        }
        send_kwargs.update(settings)
        resp = self.send(prep, **send_kwargs)

        return resp

Constructs a :class:`Request <Request>`, prepares it and sends it.Returns :class:`Response <Response>` object.
まず最初にRequestインスタンスを作成し、RequestインスタンスからPreparedRequestインスタンスに変換し、それを送信する。その結果Responseインスタンスを得る。
Responseインスタンスを得るのはSession.send()メソッドの実行によってであり、このsend()メソッドはさらに複数のクラスメソッド、関数から構成されている。

requestメソッドに含まれるメソッド、関数を軽く説明

Session.send()

Session.send()

    
        def send(self, request, **kwargs):
        """Send a given PreparedRequest.
        :rtype: requests.Response
        """
        # Set defaults that the hooks can utilize to ensure they always have
        # the correct parameters to reproduce the previous request.
        kwargs.setdefault('stream', self.stream)
        kwargs.setdefault('verify', self.verify)
        kwargs.setdefault('cert', self.cert)
        kwargs.setdefault('proxies', self.proxies)

        # It's possible that users might accidentally send a Request object.
        # Guard against that specific failure case.
        if isinstance(request, Request):
            raise ValueError('You can only send PreparedRequests.')

        # Set up variables needed for resolve_redirects and dispatching of hooks
        allow_redirects = kwargs.pop('allow_redirects', True)
        stream = kwargs.get('stream')
        hooks = request.hooks

        # Get the appropriate adapter to use
        adapter = self.get_adapter(url=request.url)

        # Start time (approximately) of the request
        start = preferred_clock()

        # Send the request
        r = adapter.send(request, **kwargs)

        # Total elapsed time of the request (approximately)
        elapsed = preferred_clock() - start
        r.elapsed = timedelta(seconds=elapsed)

        # Response manipulation hooks
        r = dispatch_hook('response', hooks, r, **kwargs)

        # Persist cookies
        if r.history:

            # If the hooks create history then we want those cookies too
            for resp in r.history:
                extract_cookies_to_jar(self.cookies, resp.request, resp.raw)

        extract_cookies_to_jar(self.cookies, request, r.raw)

        # Redirect resolving generator.
        gen = self.resolve_redirects(r, request, **kwargs)

        # Resolve redirects if allowed.
        history = [resp for resp in gen] if allow_redirects else []

        # Shuffle things around if there's history.
        if history:
            # Insert the first (original) request at the start
            history.insert(0, r)
            # Get the last request made
            r = history.pop()
            r.history = history

        # If redirects aren't being followed, store the response on the Request for Response.next().
        if not allow_redirects:
            try:
                r._next = next(self.resolve_redirects(r, request, yield_requests=True, **kwargs))
            except StopIteration:
                pass

        if not stream:
            r.content

        return r

# Get the appropriate adapter to use adapter = self.get_adapter(url=request.url)の部分ではprefixがhttps://なのかhttp://一つに絞っている。

Session.send()メソッド

    def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
        """Sends PreparedRequest object. Returns Response object.
        :param request: The :class:`PreparedRequest ` being sent.
        :param stream: (optional) Whether to stream the request content.
        :param timeout: (optional) How long to wait for the server to send
            data before giving up, as a float, or a :ref:`(connect timeout,
            read timeout) ` tuple.
        :type timeout: float or tuple or urllib3 Timeout object
        :param verify: (optional) Either a boolean, in which case it controls whether
            we verify the server's TLS certificate, or a string, in which case it
            must be a path to a CA bundle to use
        :param cert: (optional) Any user-provided SSL certificate to be trusted.
        :param proxies: (optional) The proxies dictionary to apply to the request.
        :rtype: requests.Response
        """

        try:
            conn = self.get_connection(request.url, proxies)
        except LocationValueError as e:
            raise InvalidURL(e, request=request)

        self.cert_verify(conn, request.url, verify, cert)
        url = self.request_url(request, proxies)
        self.add_headers(request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies)

        chunked = not (request.body is None or 'Content-Length' in request.headers)

        if isinstance(timeout, tuple):
            try:
                connect, read = timeout
                timeout = TimeoutSauce(connect=connect, read=read)
            except ValueError as e:
                # this may raise a string formatting error.
                err = ("Invalid timeout {}. Pass a (connect, read) "
                       "timeout tuple, or a single float to set "
                       "both timeouts to the same value".format(timeout))
                raise ValueError(err)
        elif isinstance(timeout, TimeoutSauce):
            pass
        else:
            timeout = TimeoutSauce(connect=timeout, read=timeout)

        try:
            if not chunked:
                resp = conn.urlopen(
                    method=request.method,
                    url=url,
                    body=request.body,
                    headers=request.headers,
                    redirect=False,
                    assert_same_host=False,
                    preload_content=False,
                    decode_content=False,
                    retries=self.max_retries,
                    timeout=timeout
                )

            # Send the request.
            else:
                if hasattr(conn, 'proxy_pool'):
                    conn = conn.proxy_pool

                low_conn = conn._get_conn(timeout=DEFAULT_POOL_TIMEOUT)

                try:
                    low_conn.putrequest(request.method,
                                        url,
                                        skip_accept_encoding=True)

                    for header, value in request.headers.items():
                        low_conn.putheader(header, value)

                    low_conn.endheaders()

                    for i in request.body:
                        low_conn.send(hex(len(i))[2:].encode('utf-8'))
                        low_conn.send(b'\r\n')
                        low_conn.send(i)
                        low_conn.send(b'\r\n')
                    low_conn.send(b'0\r\n\r\n')

                    # Receive the response from the server
                    try:
                        # For Python 2.7, use buffering of HTTP responses
                        r = low_conn.getresponse(buffering=True)
                    except TypeError:
                        # For compatibility with Python 3.3+
                        r = low_conn.getresponse()

                    resp = HTTPResponse.from_httplib(
                        r,
                        pool=conn,
                        connection=low_conn,
                        preload_content=False,
                        decode_content=False
                    )
                except:
                    # If we hit any problems here, clean up the connection.
                    # Then, reraise so that we can handle the actual exception.
                    low_conn.close()
                    raise

        except (ProtocolError, socket.error) as err:
            raise ConnectionError(err, request=request)

        except MaxRetryError as e:
            if isinstance(e.reason, ConnectTimeoutError):
                # TODO: Remove this in 3.0.0: see #2811
                if not isinstance(e.reason, NewConnectionError):
                    raise ConnectTimeout(e, request=request)

            if isinstance(e.reason, ResponseError):
                raise RetryError(e, request=request)

            if isinstance(e.reason, _ProxyError):
                raise ProxyError(e, request=request)

            if isinstance(e.reason, _SSLError):
                # This branch is for urllib3 v1.22 and later.
                raise SSLError(e, request=request)

            raise ConnectionError(e, request=request)

        except ClosedPoolError as e:
            raise ConnectionError(e, request=request)

        except _ProxyError as e:
            raise ProxyError(e)

        except (_SSLError, _HTTPError) as e:
            if isinstance(e, _SSLError):
                # This branch is for urllib3 versions earlier than v1.22
                raise SSLError(e, request=request)
            elif isinstance(e, ReadTimeoutError):
                raise ReadTimeout(e, request=request)
            else:
                raise

        return self.build_response(request, resp)

エラー:django.core.exceptions.ImproperlyConfigured: The included URLconf '***.urls' does not appear to have any patterns in it. If you see valid patterns in the file then the issue is probably caused by a circular import.

django.core.exceptions.ImproperlyConfigured: The included URLconf '***.urls' does not appear to have any patterns in it. If you see valid patterns in the file then the issue is probably caused by a circular import.

djangoのエラーでa circular import.と表示されることがよくある。このエラーパターンの原因をメモしておく。

大抵このエラーはurls.pyに不適切なコードが書かれていると表示される。

urlpatterns = [
    url('', TemplateView.as_view(template_name="diary/home.html"), name="home"),
    
    ]

上記のようにurlpatternsに含める要素はpath(***)である。しかしurl(***)と書いてしまうとエラーが出る。これはconfig.urls.pyよりもアプリケーション側で起こすことが多い。(なぜならconfig側ならadminの例があるのでpathと書く動機になるが、アプリ側はurls.pyをゼロから作り自分で書いてるとミスを起こす確率が高いから)

...apps/views.pyにて

    from django.core.pagenator import Paginator, Page, PageNotAnInteger
    

ずっとurls.pyにエラーが有るはずだと考えるとこのミスに気づけない。
上記の例はページネーション関係のクラスをimportする際に記述ミスしている。from django.core.paginator にしないとエラーが出てしまう。

サーバーに画像を送信する

djangoのimagefieldはメディアのファイルパスを記録するだけとも言えるので、ファイルパスをフィールドに記録したらあとはメディアのディレクトリに画像を配置すればちゃんと表示される。
で今の段階はサーバーに画像を送信する段階だ。調べてみるとzipファイルで送るのがよさそうだと分かった。
当初は画像を一件ずつ送って時間もデータ容量も大きなコストになると思われていたが、zipにすると時間が節約できるらしい。またzipにしたからと言って画像の質が下がるわけでもないらしい。そういうことでzipで送ろうと思う。

今疑問はzip化するとどれだけデータ容量が削減されるのか。サーバーに送るプランとしては、画像をzip化してscpで送る。windowsってscpコマンドを使うためにはgit for windowsを利用するとよい。

DRF ImageFieldのファイルパスを登録したい

DRFでImageFieldにデータを登録したい

なんとなくだけどModelSerializerを使ってもimageFieldに登録できないような気がしてきた。その辺を調べてみる。

資料:https://www.django-rest-framework.org/api-guide/fields/#file-upload-fields
なんかファイルやイメージに関しては特別な扱いを要する、ということが分かった。DRFで準備したものは使わずdjangoで準備したFILE_UPLOAD_HANDLERSを使ってファイル類はアップロードしろ、と。それ以外よくわからないが。。。

ドキュメントをゴリゴリ読んでいくのが良いのか、先陣を切った方のサンプルコードを参考にするべきか、どうしよう。。。

https://www.techiediaries.com/django-rest-image-file-upload-tutorial/
これのAPIViewのサブクラスにparser_class = (FileUploadParser,)とアトリビュートを定めていた。ドキュメントで"The FileField and ImageField classes are only suitable for use with MultiPartParser or FileUploadParser. Most parsers, such as e.g. JSON don't support file uploads. Django's regular FILE_UPLOAD_HANDLERS are used for handling uploaded files."と書いてあるのでファイル系を使うときはMultiPartParser or FileUploadParserをアトリビュートに定めることが必要なのかもしれない。まだ仮説段階。

資料:https://murabo.hatenablog.com/?_ga=2.75217554.1072598020.1559131999-1895290581.1558032931&page=1521708738

DRF JSONデータの成形

成形する必要性

django rest framework の場合request.POSTには注意事項がある。
それはキーに対する値が空(ブランク?null?)の項目はDRFはエラーを吐き出す。
辞書型データに例えて説明すると、ex_dict["key1"]=""の要素を含むデータをrequests.POSTするとエラーが出るという意味だ。
したがってJSONデータをPOSTする前に空データを削除する工程を加える必要がある。これはJSONデータに変換してから該当要素を削除するよりも、辞書型データの段階で空の値を検出して当該データから削除し、JSONデータに変換する方法をとるのがおすすめだ。

以下のコードは実装例になる。

実装例

前提はcsvファイルからJSONを生成する。

fieldnames = ("id","title","length","datetime")

with open(INPUT_FILE,"r",encoding="utf-8") as f:
	reader = csv.DictReader(f, fieldnames)
	for row in reader:           # rowはdictデータ
		for key, value in list(row.items()):
			if value == "":                 # valueが空の場合にdictデータからその要素を削除する
				del(row[key])

		headers = {'content-type': 'application/json'}
		URL     = "http://127.0.0.1:8000/api/"

        data    = json.dumps(row)          # dict型データをJSONに変換する
		res     = requests.post(URL, data=data, headers=headers)
		print(res)

エラー:AssertionError: You cannot call `.save()` on a serializer with invalid data.

AssertionError: You cannot call `.save()` on a serializer with invalid data.

上記のエラーが表示された。
エラーが出た状況:
インスタンスを生成することを目的にdjango rest apiでrequests.post()をする。するとdjango アプリケーションで上記のエラーが表示された。
詳しくは、csvファイルをcsv.DictReaderを使ってdict型に変換する。変換したものをjson.dumps()関数を使った戻り値をdataとし、requests.post(URL, data=data)を投げる。しかしながらエラー発生。

具体的なコード

import csv, sqlite3, json, requests
INPUT_FILE   = "/Users/*****/*****/myjson_test.csv"


fieldnames = ("id","title","length","datetime")
with open(INPUT_FILE,"r",encoding="utf-8") as f:
	reader = csv.DictReader(f, fieldnames)
	for row in reader:
		headers = {'content-type': 'application/json'}
		URL = "http://127.0.0.1:8000/api/"
		res = requests.post(URL, data=json.dumps(row), headers=headers)

エラーの原因を探る

REST apiの設計にエラー原因があるか?

適切にRest APIの設計はできているか。不適切だからエラーが発生しているのではないか?
この問に対して、django rest frameworkの設計によるエラーではないことが判明した。

test = {}
test["id"] = "1234567890123"
test["title"] = "test_title"
headers = {'content-type': 'application/json'}
URL = "http://127.0.0.1:8000/api/"

res = requests.post(URL, data=json.dumps(test), headers=headers)
print(res)

上記のコードが通り、django adminにおいてインスタンスが作成されたのを確認できた。したがって他が原因であることが判明した。

csvファイルをcsv.DictReader()で辞書型にするのに原因があるのか?

上記のデータtestは実際にdict型のデータをjsonのデータに変換している。しかしながらエラーが出たコードはcsvをdict化しているのであってdict型データではない。dict型データでないモノをjson化した際にエラーが発生の原因となってしまう可能性が考えられる。そこで直接的な原因検証ではないがtype()関数を使いデータ型を確認してみた。

test = {}
test["id"] = "1234567890123"
test["title"] = "test_title"
headers = {'content-type': 'application/json'}
URL = "http://127.0.0.1:8000/api/"
res = requests.post(URL, data=json.dumps(test), headers=headers)
print(type(test))
print(type(json.dumps(test))
#結果
<class 'dict'>
<class 'str'>

つづいてエラー原因のコードを検討する。

import csv, sqlite3, json, requests
INPUT_FILE   = "/Users/*****/*****/myjson_test.csv"

fieldnames = ("id","title","length","datetime")
with open(INPUT_FILE,"r",encoding="utf-8") as f:
	reader = csv.DictReader(f, fieldnames)
	for row in reader:
		headers = {'content-type': 'application/json'}
		URL = "http://127.0.0.1:8000/api/"
		res = requests.post(URL, data=json.dumps(row), headers=headers)
		print(type(row))
		print(type(json.dumps(row)))
# 結果
<class 'collections.OrderedDict'>
<class 'str'>

やはりdict()関数とcsv.DictReader()関数でデータ型は厳密には異なるようだ。しかしながらjson.dumps()の返り値はstr型なので適切にjson型のデータに変換したと暫定的にみなす。

jsonデータに空の値がある場合どのような挙動になるか?

REST apiにおいて受け渡されるJSONデータのキーに対する値が空である場合、適切にインスタンスが生成されるか検証していなかった。空の値の場合を検証する。

test = {}
test["id"] = "1234567890123"
test["title"] = "test_title"
test["length"] = ""    # 空の値を設置
headers = {'content-type': 'application/json'}
URL = "http://127.0.0.1:8000/api/"

res = requests.post(URL, data=json.dumps(test), headers=headers)
print(res)

上記のようにしたところエラーが発生した。

AssertionError: You cannot call `.save()` on a serializer with invalid data.

当初と同じエラーが出てきたので、これが原因だと推定できる。したがって値が空の場合についてどう対処するか考えていく。

jsonデータから空の値のデータは削除してrequests.postしたところインスタンスを生成することができた。対処法はここを参照。しかしながらできないものも出てきた。
結論から言うとmodelsのCharField max_lengthが120に対しそれ以上のものを登録しようとしていた時に同じエラーが起きた。これに対してはmax_lengthをさらに大きくしたところエラーをはかずすべてインスタンスを生成することができた。

 

まとめ:エラーAssertionError: You cannot call `.save()` on a serializer with invalid data.の原因

まずJSONのキーに対して値が空の場合にこのエラーが出てしまう。
さらに加えてCharFieldのmax_lengthを超えた文字数を登録しようとした場合に同じエラーが出てしまうことが分かった。

追記:その他の原因としてImgaeFieldにファイルパスだけ登録しようと、文字列を渡しても同じようにAssertionError: You cannot call `.save()` on a serializer with invalid data.が出る。

エラー:json.decoder.JSONDecodeError: Extra data

エラー内容とエラーが出た経緯

Traceback (most recent call last):
  File "execute.py", line 38, in <module>
    json.load(f)
  File "/anaconda3/lib/python3.6/json/__init__.py", line 299, in load
    parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
  File "/anaconda3/lib/python3.6/json/__init__.py", line 354, in loads
    return _default_decoder.decode(s)
  File "/anaconda3/lib/python3.6/json/decoder.py", line 342, in decode
    raise JSONDecodeError("Extra data", s, end)
json.decoder.JSONDecodeError: Extra data: line 20 column 2 (char 3707)
</module>

経緯:JSONモジュールをいじった際に生じた。
csvファイルからcsv.DictReader()によりdict型のデータを生成した。そのdict型データをjson.dump()関数を用いてJSONファイルに変換した。このファイルは複数のデータがJSON形式で格納されている。そしてこのJSONファイルをjson.load()関数で読み込もうとした。その際に上記のエラーが生じてしまった。

同じエラーに対処しているもの:
https://qiita.com/takugenn/items/b78f3c3bb34c5de5b5f8#json

自己検証の結果、自分の場合json.load()に渡すjsonデータが多すぎるとExtra dataとなるようだ。json.load()に渡す引数をJSONデータ一つずつイテレートするとエラーを避けることができた。

結論

json.load関数を使う場合一つずつ読み込まなければならない。

csvファイル 辞書型データ jsonモジュールメモ

https://qiita.com/motoki1990/items/0274d8bcf1a97fe4a869 https://docs.python.org/ja/3/library/csv.html#csv.DictReader https://qiita.com/pscreator/items/6040bddbbf9e9c05f77a http://hktech.hatenablog.com/entry/2018/09/10/235725

djangoRest APIを使う場合にはインプット情報はJSON形式でなければならない。そして現在sqlite3を使っている。調べてみたらsqlite3からjson形式で直接ファイルの出力は用意されていない?ようでcsvに出力したデータをjson形式に変換しようと試みている。

そういうわけで上記のURLがcsvからjsonに変換する資料を控えておいた。

 

jsonモジュールを使えるようにしてみる

jsonモジュールをある程度使えるようにならないといけないことが発覚したので、ここに使い方で気になった点をメモする。

つまりjsonモジュールとは?

jsonモジュールはpythonの組み込み型のデータとJSONデータの相互変換する機能を提供する。つまり、JSONデータをpythonで扱いたい時とpythonで組み立てたデータをJSONデータとして出力したい時に使われるモジュールだ。

JSONpythonのデータ型に変換する
json.loads(JSON文字列)
#または、
json.load(ファイル)

引数によって使う関数が異なることに注意すること。
apiでPOSTするために使いたいのであまりこの関数は使わないかもしれない。apiのgetを使う際もrequestsオブジェクトにjson()メソッド使うし本当に使うか不明...。

 

pythonのデータ型をJSONに変換する
json.dumps(pythonオブジェクト)
# または
json.dump(pythonオブジェクト)

dumpsの場合はJSON文字列に変換するだけである。dumpの場合はJSON文字列に変換してファイルに書き出す。この違いがある。

django migration

makemigrations でpostgresql上にテーブルが作られるのか、またはmigrateでテーブルが作られるのか?

 

該当するドキュメント

疑問に思うテーマについてdjangoドキュメントがある。こちらを見てみる。
https://docs.djangoproject.com/ja/2.2/topics/migrations/#workflow

python manage.py migrate

migrateコマンドをたたいた時に初めてデータベースのテーブルのカラム構成に変更が加えられるようだ。
この変更は各アプリのmigrationsディレクトリに存在するマイグレーションファイルをもとに行われる。言い換えればマイグレーションファイルはテーブルの設計書の位置づけだ。

python manage.py makemigrations

このコマンドによってマイグレーションファイルが作成される。
マイグレーションファイルが作成されるときは、models.pyのモデルと最新のマイグレーションファイルが不一致の時でモデルに即したマイグレーションファイルが作成される。

メモ:https://stackoverflow.com/questions/28035119/should-i-be-adding-the-django-migration-files-in-the-gitignore-file

migrationsファイルについて理解を深める

特に調べる必要もなかったのでスルーしてきたが、調べる機会があったのでメモしておく。
ドキュメント:https://docs.djangoproject.com/ja/2.2/ref/migration-operations/#module-django.db.migrations.operations

マイグレーション操作は大別して3つの種類ある。

  • スキーマ操作
  • 特別な操作
  • 自分で操作を書く

マイグレーション操作はどう使う?

結論はoperationsリスト内の一つの要素としてマイグレーション操作を記述する

スキーマ操作の一つCreateModelを見れば理解が深まる。

https://docs.djangoproject.com/ja/2.2/ref/migration-operations/#createmodel

新たにContactモデルを作成後 python manage.py makemigrationsコマンドを実行する。するとcontacts/migrations以下に0001_initial.pyが作成される。中身は以下になる。

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Contact',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('client_name', models.CharField(max_length=40)),
                ('subtitle', models.CharField(max_length=100)),
                ('message', models.TextField()),
                ('telephone_number', models.CharField(max_length=30)),
                ('email_address', models.EmailField(max_length=254)),
                ('datetime', models.DateTimeField(auto_now=True)),
            ],
        ),
    ]

このファイルに基づいて実際のテーブルが作成される。0001_initial.pyにはoperations内にCreateModelが書かれていることから、要するに実際にデータベースやテーブルに加える操作はoperationsに書くことだと推定することは容易だろう。 つまりデータベースのテーブルを変更をしたいなら、operationsのリストに、変更内容(migrations内の各クラスオブジェクト)を記述する。