小村のポートフォリオサイト開発(14) DjangoRestFramework ここまでのリファクタリング

  • こんばんは、小村だよ!

  • [DRF レスポンスデータ]みたいな感じで検索したらこのブログがヒットしてびっくりしたよ!

  • じゃー今日も下記のポートフォリオサイトを構築していくよ

  • 前回はAPIのレスポンスのフォーマットを決めたね

  • 今日はここまで書いてきたAPIリファクタリングしていこうと思います!



目次

  1. 現状整理
  2. utils.hatena.py作成
  3. views.entry.py修正



記録

現状整理

  • まずは現状のviews.entry.pyの中身が下記

  • fat viewもいいところだね!

  • 基本的にviewはコントローラー部分だけ書くべきなのでめちゃんこよくない例です。

  • このソースをリファクタリングしていきたいと思います

# coding: utf-8
import time
import os
import requests
import xmltodict


from rest_framework import status
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from ..models import Entry
from ..serializer import EntryAllSerializer, EntryCreateAndUpdateSerializer


class EntryViewSet(viewsets.ModelViewSet):
    queryset = Entry.objects.all()
    serializer_class = EntryAllSerializer

    @action(detail=False, methods=['post'])
    def capture(self, request):
        url = self.getHatenaApiUrl('entry')
        auth = self.getHatenaApiAuth()
        hatena_entry_ids = []
        response = {
            'created_count': 0,
            'updated_count': 0,
            'deleted_count': 0,
            'failed_count': 0,
            'failed_hatena_entry_ids': [],
        }

        while url != '':
            # 連続で呼び出すの怖いので0.5秒間間を空ける
            time.sleep(0.5)

            hatena_list = requests.get(url, auth=auth)
            dict_data = xmltodict.parse(hatena_list.text, encoding='utf-8')
            entries = []
            if 'feed' in dict_data:
                if 'entry' in dict_data['feed']:
                    entries = dict_data['feed']['entry']
            for entry in entries:
                if not isinstance(entry, dict):
                    continue

                # hatena_entry_id取得
                hatena_entry_id = entry['id'][
                    entry['id'].rfind('-') + 1:] if 'id' in entry else ''

                hatena_entry_ids.append(hatena_entry_id)

                # category取得
                if 'category' in entry:
                    if isinstance(entry['category'], list):
                        category = entry['category'][0]['@term']
                    else:
                        category = entry['category']['@term']
                else:
                    category = ''

                # title取得
                title = entry['title'] if 'title' in entry else ''

                # summary取得
                summary = entry['summary']['#text'] if 'summary' in entry else ''

                # content_md取得
                content_md = entry['content']['#text'] if 'content' in entry else ''

                # content_html取得
                content_html = entry['hatena:formatted-content']['#text'] if 'hatena:formatted-content' in entry else ''

                # draft取得
                draft = entry['app:control']['app:draft'] if 'app:control' in entry else ''

                # updated_at取得
                updated_at = entry['updated'] if 'updated' in entry else None

                # edited_at取得
                edited_at = entry['app:edited'] if 'app:edited' in entry else None

                # 更新用パラメータ
                param = {
                    'hatena_entry_id': hatena_entry_id,
                    'category': category,
                    'title': title,
                    'summary': summary,
                    'content_md': content_md,
                    'content_html': content_html,
                    'draft': draft,
                    'updated_at': updated_at,
                    'edited_at': edited_at,
                }

                entry = Entry.objects.filter(
                    hatena_entry_id=hatena_entry_id).first()

                if not entry:
                    # 新規作成
                    mode = 'create'
                    serializer = EntryCreateAndUpdateSerializer(data=param)

                else:
                    # 更新
                    mode = 'update'
                    serializer = EntryCreateAndUpdateSerializer(
                        entry, data=param)

                if serializer.is_valid():
                    serializer.save()
                    if mode == 'create':
                        response['created_count'] = response['created_count'] + 1
                    else:
                        response['updated_count'] = response['updated_count'] + 1
                else:
                    response['failed_count'] = response['failed_count'] + 1
                    response['failed_hatena_entry_ids'].append(hatena_entry_id)

            # 次のURLを取得
            url = ''
            if 'link' in dict_data['feed']:
                if isinstance(dict_data['feed']['link'], list):
                    for link in dict_data['feed']['link']:
                        if isinstance(link, dict):
                            if '@rel' in link:
                                if link['@rel'] == 'next':
                                    url = link['@href']

        # はてなIDが存在しなければ論理削除
        response['deleted_count'] = Entry.objects.exclude(
            hatena_entry_id__in=hatena_entry_ids).delete()

        return Response(response, status.HTTP_200_OK)

    def getHatenaApiUrl(self, action):
        HATENA_API_URL_HEADER = 'https://blog.hatena.ne.jp'
        HATENA_API_USER = os.environ.get('HATENA_API_USER')
        HATENA_API_BLOG = os.environ.get('HATENA_API_BLOG')
        HATENA_API_URL_FUTTER = 'atom'

        url = [
            HATENA_API_URL_HEADER,
            HATENA_API_USER,
            HATENA_API_BLOG,
            HATENA_API_URL_FUTTER,
            action
        ]
        return os.path.join(*url)

    def getHatenaApiAuth(self):
        HATENA_API_USER = os.environ.get('HATENA_API_USER')
        HATENA_API_KEY = os.environ.get('HATENA_API_KEY')

        return (HATENA_API_USER, HATENA_API_KEY)



utils.hatena.py作成

  • そもそもはてなブログAPIに関する処理はEntryと直接関係ないですね。

  • ということでutilsフォルダ内にhatena.pyを作成して、はてなブログAPI関係の処理は全てここにまとめようと思います

  • これによりHatenaApiをインスタンス化してgetAllEntriesを実行するといい感じに加工された記事データが取得できるようになりました

f:id:kom314_prog:20210830234554p:plain
import os
import requests
import xmltodict


class HatenaApi():
    HATENA_API_USER = os.environ.get('HATENA_API_USER')
    HATENA_API_BLOG = os.environ.get('HATENA_API_BLOG')
    HATENA_API_KEY = os.environ.get('HATENA_API_KEY')

    HATENA_API_URL_HEADER = 'https://blog.hatena.ne.jp'
    HATENA_API_URL_FUTTER = 'atom'

    def getAllEntries(self):

        all_entries = []
        url = self.getHatenaApiFirstUrl('entry')

        while url != '':
            hatenaApiData = self.getHatenaApi(url)

            for entry in self.getApiEntries(hatenaApiData):
                all_entries.append(self.formatEntry(entry))
            url = self.getHatenaApiNextUrl(hatenaApiData)

        return all_entries

    def getHatenaApi(self, url):
        auth = self.getHatenaApiAuth()
        hatena_list = requests.get(url, auth=auth)
        dict_data = xmltodict.parse(hatena_list.text, encoding='utf-8')
        return dict_data

    def getApiEntries(self, hatenaApiData):
        return self.getDictValue(hatenaApiData, ['feed', 'entry'])

    def formatEntry(self, entry):
        format_entry = {}
        format_entry['hatena_entry_id'] = self.getHatenaEntryId(entry)
        format_entry['category'] = self.getCategory(entry)
        format_entry['title'] = self.getDictValue(entry, ['title'])
        format_entry['summary'] = self.getDictValue(
            entry, ['summary', '#text'])
        format_entry['content_md'] = self.getDictValue(
            entry, ['content', '#text'])
        format_entry['content_html'] = self.getDictValue(
            entry, ['hatena:formatted-content', '#text'])
        format_entry['draft'] = self.getDictValue(
            entry, ['app:control', 'app:draft'])
        format_entry['updated_at'] = self.getDictValue(
            entry, ['updated'], None)
        format_entry['edited_at'] = self.getDictValue(
            entry, ['app:edited'], None)

        return format_entry

    def getHatenaEntryId(self, entry):
        id_all = self.getDictValue(entry, ['id'])
        id = id_all[id_all.rfind('-') + 1:] if id_all else ''
        return id

    def getCategory(self, entry):
        category_all = self.getDictValue(entry, ['category'])
        if isinstance(category_all, list):
            category = category_all[0]
        else:
            category = category_all
        return self.getDictValue(category, ['@term'])

    def getHatenaApiFirstUrl(self, action):

        url = [
            self.HATENA_API_URL_HEADER,
            self.HATENA_API_USER,
            self.HATENA_API_BLOG,
            self.HATENA_API_URL_FUTTER,
            action
        ]
        return os.path.join(*url)

    def getHatenaApiNextUrl(self, hatenaApiEntries):
        url = ''
        links = self.getDictValue(hatenaApiEntries, ['feed', 'link'])
        if isinstance(links, list):
            for link in links:
                if self.getDictValue(link, ['@rel']) == 'next':
                    url = self.getDictValue(link, ['@href'])
        return url

    def getHatenaApiAuth(self):

        return (self.HATENA_API_USER, self.HATENA_API_KEY)

    def getDictValue(self, dictData, keys, emptyValue=''):
        for key in keys:
            if isinstance(dictData, dict):
                if key in dictData:
                    dictData = dictData[key]
            else:
                return emptyValue
        return dictData



views.entry.py修正

  • あとは今までのentry.pyから上記処理を呼び出して、登録・更新・削除を行うようにします

  • もうちょっと短くなると思ってたのですが、思いのほか処理減らなかったな……

  • でもまぁかなり読みやすくなったのでよし!!!

f:id:kom314_prog:20210830235143p:plain
# coding: utf-8
from rest_framework import status
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from ..models import Entry
from ..utils import HatenaApi
from ..serializer import EntryAllSerializer, EntryCreateAndUpdateSerializer


class EntryViewSet(viewsets.ModelViewSet):
    queryset = Entry.objects.all()
    serializer_class = EntryAllSerializer

    @action(detail=False, methods=['post'])
    def capture(self, request):
        hatena_api = HatenaApi()
        all_entries = hatena_api.getAllEntries()
        all_entries_sort = sorted(all_entries, key=lambda x: x['edited_at'])
        response = {
            'created_count': 0,
            'updated_count': 0,
            'deleted_count': 0,
            'failed_count': 0,
            'failed_hatena_entry_ids': [],
        }
        hatena_entry_ids = []
        for param in all_entries_sort:
            hatena_entry_id = param['hatena_entry_id']
            hatena_entry_ids.append(hatena_entry_id)
            # 更新用パラメータ
            entry = Entry.objects.filter(
                hatena_entry_id=hatena_entry_id).first()

            if not entry:
                # 新規作成
                mode = 'create'
                serializer = EntryCreateAndUpdateSerializer(data=param)

            else:
                # 更新
                mode = 'update'
                serializer = EntryCreateAndUpdateSerializer(
                    entry, data=param)

            if serializer.is_valid():
                serializer.save()
                if mode == 'create':
                    response['created_count'] = response['created_count'] + 1
                else:
                    response['updated_count'] = response['updated_count'] + 1
            else:
                response['failed_count'] = response['failed_count'] + 1
                response['failed_hatena_entry_ids'].append(
                    hatena_entry_id)

        # はてなIDが存在しなければ論理削除
        response['deleted_count'] = Entry.objects.exclude(
            hatena_entry_id__in=hatena_entry_ids).delete()

        return Response(response, status.HTTP_200_OK)



おわりに

  • リファクタリングが完了しました!

  • 取込系の処理に関してはひとまずこれでよさそうかなーと思います

  • あ、でもテスト作ってないや。ユニットテスト作らなきゃだね

  • 次回当たりユニットテスト作っていきたいと思います!

  • ではでは、ちゃお~~~!



小村のポートフォリオサイト開発(13) DjangoRestFramework レスポンスデータフォーマット

  • こんばんは、小村だよ!

  • 下記のポートフォリオサイトを構築していくよ

  • 前回記事の論理削除対応をしたね

  • 今回はAPIのレスポンスのフォーマットを決めていきたいと思います



目次

  1. APIのレスポンスのフォーマットを決める
  2. 記事取込処理の正常系レスポンスを決める
  3. 全体共通のエラー時のレスポンスを決める



記録

APIのレスポンスのフォーマットを決める

参考:Udemy REST WebAPI サービス 設計 26. データの内部構造

参考:WebAPIでエラーをどう表現すべき?15のサービスを調査してみた」についてまとめた

  • APIのレスポンスは、業務上では下記のようなフォーマットで返してました
{
  result: 1 or 0
  data: { ... }
}
  • 改めて今回調べたところ、resultなどは返さないのが一般的のようですね?

  • 何が一番いいのか色々見ましたが最終的にUdemyで上記講座に書かれていた方針でいきます

    • エンベロープは使わない

      • エンベロープ=レスポンスボディ内のメタ情報。ヘッダ情報と役割がかぶるため不要
    • オブジェクトはできるだけフラット

      • レスポンス容量を減らすため
    • エラー詳細はレスポンスボディに追加する

      • 下記の形式とする
{
  code: xx
  detail: ''
}



記事取込処理の正常系レスポンスを決める

  • ではでは、今回の記事取込処理のレスポンスを決めていきましょう

  • まず、正常に処理が行われたときは下記を返すようにしましょうか

http_status: 200

{
  created_count: x,
  updated_count: x,
  deleted_count: x,
  failed_count: x,
  failed_ids: [x, x, x]
}
  • 正常かどうかはレスポンスヘッダで確認します。

  • HTTP 200 OK はリクエストが成功した場合に返すレスポンスコードですね

  • それから作成、更新、削除件数を返します。

  • 最後に入力チェックではじかれた件数とIDを返すようにしましょうか

  • そんな感じにViewsを修正します

  • 修正して実行した結果が最後の図。問題なさそうですね!

f:id:kom314_prog:20210828171542p:plain
f:id:kom314_prog:20210828171628p:plain
f:id:kom314_prog:20210828171728p:plain



全体共通のエラー時のレスポンスを決める


f:id:kom314_prog:20210828194753p:plain
  • 参考サイトを参考に、utiles.handler.pyを作成して図のように記載


f:id:kom314_prog:20210828194437p:plain
  • これをsettingsに上記画像のように書くのだけど、そこで2時間くらいエラーと戦いました。

  • 結果として、上記の書き方です。

    • エラーで詰まったら飯食うの大事。一瞬で解けた。フォルダパスの書き方が原因だった


f:id:kom314_prog:20210828203342p:plain
  • 実装してエラー吐いた結果がこれ

  • 本当は出力をmessageにしたかったのにdetailで出てしまうのは自分の実力不足でする……

  • 業務の方のシステムだとdetailなんて吐いてないので、実装覗いてきて改良します



おわりに

  • やっとこさ取込機能に関する機能の実装は一通りおしまい!

  • あとはリファクタリングしたらweb側の実装に戻れます!

  • あともうちょいだ!がんばろー!

  • ではでは、ちゃお~~~!



小村のポートフォリオサイト開発(12) DjangoRestFramework はてなブログの記事 論理削除対応

  • こんばんは、小村だよ!

  • 下記のポートフォリオサイトを構築していくよ

  • 前回はてなブログ記事をすべてDBに書き込むことに成功しました

  • 今回は論理削除対応と銘打って進めていきます



目次

  1. 削除日時を追加
  2. 削除日時を入れる処理追加



記録

削除日時を追加

  • 自前のDBはバックアップとしての機能も期待しています。

  • そのため、はてなブログの記事が消えたとしても、削除はしません

  • そのかわりに、削除されていたら削除日を更新して記事の表示はしないようにします

  • ではまずmodelに削除日を用意しましょう

  • 追加した後のマイグレーションファイルは下記のようになりました

f:id:kom314_prog:20210828092700p:plain



削除日時を入れる処理追加


f:id:kom314_prog:20210828105053p:plain
f:id:kom314_prog:20210828105130p:plain
  • まずはviews

  • 取込処理をしているなかで存在するhatena_entry_idを蓄え、最後になければdeleteかけてます

  • ここは特に問題ないですね


f:id:kom314_prog:20210828105522p:plain
f:id:kom314_prog:20210828105550p:plain
  • 本題のmodels

  • あらたにQuerySetModelManagerの概念が加わってます。

  • こちら業務で使ってますが詳しくは調べたことないのであとで調べてみますね

  • 動作としては

    • delete()を実行すると論理削除する

    • delete_hard()を実行すると物理削除する

    • Entry.objectsでデータを覗くと論理削除データは省かれる

    • Entry.entireでデータを覗くと論理削除データも含む

  • という動きになります。どちゃくそ便利!



動作確認

f:id:kom314_prog:20210828112054p:plain
f:id:kom314_prog:20210828112129p:plain


  • そしたらはてなブログで記事を消して、ふたたび取り込みます

  • 再取り込み後のデータが下記。ちゃんと削除日時が入ってますね!

f:id:kom314_prog:20210828112540p:plain



DjangoQuerySetModelManagerについて

  • Modelsで出てきたQuerySetModelManagerについて調べてみます

  • 業務で使ってるのでなんとなくは使えてますが、改めて意味を調べてみます


Manager

  • 参考:Django ドキュメント マネージャ

  • 今までなんとなしにEntry.objects.all()とかでデータを取得できてたのはこのManagerのおかげ

  • デフォルトでobjectsにアクセスした際、動くようになっているようですね。

  • 今回のようにentireの中でManagerを入れると、Entry.entireでアクセスできるようになります。

  • そうしてアクセスした際の動きを、下記のQuerySetなどで制御するようですね。


QuerySet

  • 参考:Django ドキュメント QuerySet

  • QuerySetは実際にDBにアクセスせずにデータを加工したりする仕組みのようです。へー!

  • models.QuerySetが用意されていて、その中に基本のgetやfilterやexcludeなどが用意されてます

  • そのうえで、今回のような決まりきった削除などの更新処理も定義できるようですね。

  • DBにアクセスする際の抽出条件や処理条件を定義する役目ということでしょうね。



おわりに

  • 論理削除の処理と合わせて、Djangoの基礎知識についても知れました

  • 上記記事について熟読ができてないので、改めて後で読んでみようと思います。

  • 次は、APIの返り値の設定をしていきたいと思います。

  • ではでは、ちゃお~~~!



小村のポートフォリオサイト開発(11) DjangoRestFramework はてなブログの記事のデータ格納(3)

  • こんばんは、小村だよ!

  • 下記のポートフォリオサイトを構築していくよ

  • 前回ひとまずはてなブログ記事をひとまずDBに書き込むことに成功しました

  • 今回はそれを実働レベルに持っていきます!



目次

  1. 管理メニューのタイトル表示を修正
  2. はてな記事全件取得するまで回す



記録

管理メニューのタイトル表示を修正

  • 前回、はてな記事を自前のDBに格納することができました

  • それを管理ページから確認した結果が下記

f:id:kom314_prog:20210821151236p:plain


  • なにがなんだかわからないね!

  • というわけで管理画面表記を変更します

  • admin.pyを下記の通り変更

from django.contrib import admin

from .models import Entry


@admin.register(Entry)
class Entry(admin.ModelAdmin):
    list_display = (
        'entry_id',
        'hatena_entry_id',
        'title',
        'updated_at',
        'edited_at',
    )


  • はてなブログAPIで取得できる項目が、

    • edited:更新日時
    • updated:公開日時
  • という謎な仕様が判明したので合わせてmodelも変更してますが割愛

  • 最終的な管理画面が下記。見やすくなったね!

f:id:kom314_prog:20210821161712p:plain



はてな記事全件取得するまで回す

  • はてな記事API~~/entryにアクセスすると10件分の記事を取得します

  • 次の10件は、下図のnextにあるリンクにより次のページ分が見れます

f:id:kom314_prog:20210821171215p:plain


  • このURLが存在する間、ぐるぐる回るようにコードを修正します。

  • そして間違えて永久に(2分ぐらい)APIを呼び続けてしまった/(^q^)\

  • 垢BANされないか心配だ!

  • そんなわけで、今後に備えて念のためAPIを呼ぶ感覚に0.5秒の感覚を設けました

  • 修正箇所がこちら

f:id:kom314_prog:20210821172530p:plain
f:id:kom314_prog:20210821172559p:plain


  • これにより下記の通り過去投稿した全記事を格納できました!

  • 振られるIDが投稿日時の降順なの気になるな……後日修正しよう

f:id:kom314_prog:20210821172835p:plain



おわりに

  • 記事の取得処理はもう大詰め!

  • とはいえ今のままだとすべての処理をViewsに書いてるから汚い汚い

  • リファクタリングちゃんとしなくちゃね!

  • ではでは今日はこの辺で!ちゃお~~~!



小村のポートフォリオサイト開発(10) DjangoRestFramework はてなブログの記事のデータ格納(2)



目次

  1. はてな記事をentryに書き込み
  2. 出たエラーとその対策



記録

はてな記事をentryに書き込み

  • 前回、はてなブログAPIで取得してたデータを、格納する形に変換してたんだよね

  • 引き続きもくもくとコーディングして下記になりました!

  • はてなブログのIDが一致するものがあれば更新をかけて、なければ新規作成します

# coding: utf-8

import os
import requests
import xmltodict

from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from ..models import Entry
from ..serializer import EntryAllSerializer, EntryCreateAndUpdateSerializer


class EntryViewSet(viewsets.ModelViewSet):
    queryset = Entry.objects.all()
    serializer_class = EntryAllSerializer

    @action(detail=False, methods=['post'])
    def capture(self, request):
        url = self.getHatenaApiUrl('entry')
        auth = self.getHatenaApiAuth()
        hatena_list = requests.get(url, auth=auth)
        dictData = xmltodict.parse(hatena_list.text, encoding='utf-8')
        entries = dictData['feed']['entry']
        for entry in entries:
            if not isinstance(entry, dict):
                continue

            # hatena_entry_id取得
            hatena_entry_id = entry['id'][
                entry['id'].rfind('-') + 1:] if 'id' in entry else ''

            # category取得
            if 'category' in entry:
                if isinstance(entry['category'], list):
                    category = entry['category'][0]['@term']
                else:
                    category = entry['category']['@term']
            else:
                category = ''

            # title取得
            title = entry['title'] if 'title' in entry else ''

            # summary取得
            summary = entry['summary']['#text'] if 'summary' in entry else ''

            # content_md取得
            content_md = entry['content']['#text'] if 'content' in entry else ''

            # content_html取得
            content_html = entry['hatena:formatted-content']['#text'] if 'hatena:formatted-content' in entry else ''

            # draft取得
            draft = entry['app:control']['app:draft'] if 'app:control' in entry else ''

            # published_at取得
            published_at = entry['published'] if 'published' in entry else None

            # edited_at取得
            edited_at = entry['app:edited'] if 'app:edited' in entry else None

            # updated_at取得
            updated_at = entry['updated'] if 'updated' in entry else None

            # 更新用パラメータ
            param = {
                'hatena_entry_id': hatena_entry_id,
                'category': category,
                'title': title,
                'summary': summary,
                'content_md': content_md,
                'content_html': content_html,
                'draft': draft,
                'published_at': published_at,
                'edited_at': edited_at,
                'updated_at': updated_at,
            }

            entry = Entry.objects.filter(
                hatena_entry_id=hatena_entry_id).first()

            if not entry:
                # 新規作成
                serializer = EntryCreateAndUpdateSerializer(data=param)
            else:
                # 更新
                serializer = EntryCreateAndUpdateSerializer(entry, data=param)

            if serializer.is_valid():
                serializer.save()
                print('valid-OK')
            else:
                print('valid-NG')

        return Response(entries)

    def getHatenaApiUrl(self, action):
        HATENA_API_URL_HEADER = 'https://blog.hatena.ne.jp'
        HATENA_API_USER = os.environ.get('HATENA_API_USER')
        HATENA_API_BLOG = os.environ.get('HATENA_API_BLOG')
        HATENA_API_URL_FUTTER = 'atom'

        url = [
            HATENA_API_URL_HEADER,
            HATENA_API_USER,
            HATENA_API_BLOG,
            HATENA_API_URL_FUTTER,
            action
        ]
        return os.path.join(*url)

    def getHatenaApiAuth(self):
        HATENA_API_USER = os.environ.get('HATENA_API_USER')
        HATENA_API_KEY = os.environ.get('HATENA_API_KEY')

        return (HATENA_API_USER, HATENA_API_KEY)
  • serializerは下記
# coding: utf-8

from rest_framework import serializers

from ..models import Entry


class EntryAllSerializer(serializers.ModelSerializer):
    class Meta:
        model = Entry
        fields = '__all__'


class EntryCreateAndUpdateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Entry
        fields = (
            'entry_id',
            'hatena_entry_id',
            'title',
            'summary',
            'content_md',
            'content_html',
            'draft',
            'published_at',
            'edited_at',
            'updated_at',
        )



f:id:kom314_prog:20210821151236p:plain
  • entryモデルに10件のレコードが登録されることが確認できました。

  • 再度実行して更新されることも確認。ひゃっほ!



出たエラーとその対策

  • やっていく中でエラーとの格闘が勃発したのでそのメモ


TypeError: 'in <string>' requires string as left operand, not collections.OrderedDict

  • 下記のコードが原因で発生。辞書型のentrycategoryというkeyがあるかを確認しようとしている

  • しむらー!ぎゃくぎゃく!!!

            if entry in 'category':
  • 正解は下記
            if 'category' in entry:



おわりに

  • APIの第一歩がようやく踏み出せた感ありますね!

  • この先やっていくこととして

    • はてな記事全件取得するまで回す
    • content_mdを正しく取得
    • 記事の削除処理
    • リファクタリング
    • デプロイ
    • 毎日0:00に自動キック
  • あたりの作業がまっております。がんばるぞい!

  • ではでは。ちゃお~~~!



小村のポートフォリオサイト開発(9) DjangoRestFramework はてなブログの記事のデータ格納



目次

  1. 不要なmodel削除
  2. models作成
  3. views.blog.pyの内容をviews.entry.pyに移植
  4. はてな記事をentryに書き込み



記録

運用ルールの決定

  • APIで取得したデータを眺めながら、いくつか運用ルールを決めました

  • 今後の記事を書く際に気を付けたいと思います

    • 記事タイトルは最大100文字

      • それ以上の場合は切って格納
    • カテゴリは1記事につき1つ

      • 複数ある場合は最初の1つのみDBに格納



不要なmodels削除

  • これから本格的にmodelを用意するので、これまでテストで使用していたmodelを用済みです!

  • さくっと削除しましょう!

  • 下記の通り、Userモデルを削除し、紐づいていた処理を全て削除しました!

f:id:kom314_prog:20210817225300p:plain



models作成

  • ではでははてなブログの記事を格納するモデルを作っていくよ!

  • 一般的にこういった記事はEntryという名前が多いのでそれに習います

  • もともと存在していたしていたEntryモデルを下記の通り修正

from django.db import models

class Entry(models.Model):
    CHOICE_DRAFT = (('yes', '下書き'), ('no', '公開'))
    entry_id = models.AutoField(verbose_name='記事Id', primary_key=True)
    hatena_entry_id = models.BigIntegerField(verbose_name='はてな記事Id', unique=True)
    category = models.CharField(verbose_name='カテゴリ', max_length=100, null=True, blank=True, default='')
    title = models.CharField(verbose_name='タイトル', max_length=200, null=True, blank=True, default='')
    summary = models.CharField(verbose_name='サムネイル文', max_length=1000, null=True, blank=True, default='')
    content_md = models.TextField(verbose_name='内容_MarkDown', null=True, blank=True, default='')
    content_html = models.TextField(verbose_name='内容_HTML', null=True, blank=True, default='')
    draft = models.CharField(verbose_name='下書き区分', max_length=10, choices=CHOICE_DRAFT, null=True, blank=True, default='yes')
    published_at = models.DateTimeField(verbose_name='公開日時', null=True, blank=True)
    edited_at = models.DateTimeField(verbose_name='作成日時', null=True, blank=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', null=True, blank=True)

  class Meta:
        db_table = 'entry'


  • それからserializers/entry.pyも下記の通り修正
# coding: utf-8

from rest_framework import serializers

from ..models import Entry


class EntrySerializer(serializers.ModelSerializer):
    class Meta:
        model = Entry
        fields = '__all__'


f:id:kom314_prog:20210817225611p:plain



views.blog.pyの内容をviews.entry.pyに移植

  • はてなブログAPIを呼び出すviews.blog.pyの処理をentry.pyに移植します

  • そして用済みになったviews.blog.pyはおさらば!!!

  • だいぶ構成がすっきりしました!

f:id:kom314_prog:20210817230414p:plain
f:id:kom314_prog:20210817230517p:plain



はてな記事をentry用に加工

  • メインの処理!

  • 取得したはてな記事をEntryモデル用に加工するよ

  • entry_idを取得した時点で時間が来てしまったので続きは次に回すよ!

f:id:kom314_prog:20210817233946p:plain



おわりに

  • 中途半端!!!

  • entryに登録までやりたかったけどできませんでした!

  • 次がんばるぞ~~~!

  • ではでは、ちゃお~~~!



小村のポートフォリオサイト開発(8) DjangoRestFramework はてなブログの記事の必要データ選定



目次

  1. xml形式のデータを辞書形式に加工
  2. json形式のデータから必要な情報を選定



記録

xml形式のデータをjson形式に加工

f:id:kom314_prog:20210815172514p:plain


  • 参考サイトのxmltodictを使用してDict型に変換したところで止めるよ!

  • 内容を見るとこんな感じ。

f:id:kom314_prog:20210815172615p:plain



辞書形式のデータから必要な情報をピックアップ

  • 参考:はてなブログAtomPub

  • ではでは、必要なデータを実際のデータと公式ページ見ながら考えていきます

  • ふむふむ!!

  • まずentryの中身がリスト型になっていて、最新7件分のブログデータが入ってるね

  • 必要そうなものをピックアップするとこんな感じ

項目 内容 理由
id ユニークキー おそらく20桁以内の数値。ブログ記事のユニークキー
title 記事タイトル
summary.#text 改行とかない本文 トップページ用。詳細だとなし
content.#text markdown形式の本文 文字数制限につき一覧ではなく詳細で取得する必要あり
hatena:formatted-content.#text html形式の本文 一覧でも全部取得可能
category カテゴリ
app:control.app:draft 下書きか否か
published 公開日
app:edited 作成日
updated 更新日
  • こんな感じかな!



おわりに

  • 今回はちょっと短いですがこの辺で!

  • 次回は実際にmodelsを作成して必要なデータを取得していきたいと思います!

  • ではでは!ちゃお~~~!