小村のポートフォリオサイト開発(11) DjangoRestFramework はてなブログの記事のデータ格納(3)
こんばんは、小村だよ!
下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
前回ひとまずはてなブログ記事をひとまずDBに書き込むことに成功しました
今回はそれを実働レベルに持っていきます!
目次
- 管理メニューのタイトル表示を修正
- はてな記事全件取得するまで回す
記録
管理メニューのタイトル表示を修正
前回、はてな記事を自前のDBに格納することができました
それを管理ページから確認した結果が下記
なにがなんだかわからないね!
というわけで管理画面表記を変更します
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', )
-
edited
:更新日時updated
:公開日時
という謎な仕様が判明したので合わせてmodelも変更してますが割愛
最終的な管理画面が下記。見やすくなったね!
はてな記事全件取得するまで回す
このURLが存在する間、ぐるぐる回るようにコードを修正します。
そして間違えて永久に(2分ぐらい)APIを呼び続けてしまった/(^q^)\
垢BANされないか心配だ!
そんなわけで、今後に備えて念のためAPIを呼ぶ感覚に0.5秒の感覚を設けました
修正箇所がこちら
これにより下記の通り過去投稿した全記事を格納できました!
振られるIDが投稿日時の降順なの気になるな……後日修正しよう
おわりに
記事の取得処理はもう大詰め!
とはいえ今のままだとすべての処理をViewsに書いてるから汚い汚い
リファクタリングちゃんとしなくちゃね!
ではでは今日はこの辺で!ちゃお~~~!
小村のポートフォリオサイト開発(10) DjangoRestFramework はてなブログの記事のデータ格納(2)
こんばんは、小村だよ!
下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
目次
- はてな記事を
entry
に書き込み - 出たエラーとその対策
記録
はてな記事をentry
に書き込み
引き続きもくもくとコーディングして下記になりました!
はてなブログの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', )
entry
モデルに10件のレコードが登録されることが確認できました。再度実行して更新されることも確認。ひゃっほ!
出たエラーとその対策
- やっていく中でエラーとの格闘が勃発したのでそのメモ
TypeError: 'in <string>' requires string as left operand, not collections.OrderedDict
下記のコードが原因で発生。辞書型の
entry
にcategory
というkeyがあるかを確認しようとしているしむらー!ぎゃくぎゃく!!!
if entry in 'category':
- 正解は下記
if 'category' in entry:
おわりに
APIの第一歩がようやく踏み出せた感ありますね!
この先やっていくこととして
あたりの作業がまっております。がんばるぞい!
ではでは。ちゃお~~~!
小村のポートフォリオサイト開発(9) DjangoRestFramework はてなブログの記事のデータ格納
こんばんは、小村だよ!
下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
よろしくね!
目次
- 不要なmodel削除
- models作成
views.blog.py
の内容をviews.entry.py
に移植- はてな記事を
entry
に書き込み
記録
運用ルールの決定
APIで取得したデータを眺めながら、いくつか運用ルールを決めました
今後の記事を書く際に気を付けたいと思います
記事タイトルは最大100文字
- それ以上の場合は切って格納
カテゴリは1記事につき1つ
- 複数ある場合は最初の1つのみDBに格納
不要なmodels削除
これから本格的にmodelを用意するので、これまでテストで使用していたmodelを用済みです!
さくっと削除しましょう!
下記の通り、Userモデルを削除し、紐づいていた処理を全て削除しました!
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__'
この状態でhttp://localhost:8000/api/entries/にアクセスし、更新できることを確認
問題なっしん!
views.blog.py
の内容をviews.entry.py
に移植
はてな記事をentry
用に加工
メインの処理!
取得したはてな記事を
Entry
モデル用に加工するよentry_id
を取得した時点で時間が来てしまったので続きは次に回すよ!
おわりに
中途半端!!!
entryに登録までやりたかったけどできませんでした!
次がんばるぞ~~~!
ではでは、ちゃお~~~!
小村のポートフォリオサイト開発(8) DjangoRestFramework はてなブログの記事の必要データ選定
こんばんは、小村だよ!
下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
よろしくね!
目次
記録
xml形式のデータをjson形式に加工
参考サイトの
xmltodict
を使用してDict型に変換したところで止めるよ!内容を見るとこんな感じ。
辞書形式のデータから必要な情報をピックアップ
ではでは、必要なデータを実際のデータと公式ページ見ながら考えていきます
ふむふむ!!
まず
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を作成して必要なデータを取得していきたいと思います!
ではでは!ちゃお~~~!
小村のポートフォリオサイト開発(7) DjangoRestFramework はてなブログの記事取得
こんばんは、小村だよ!
下記のポートフォリオサイトを構築していくよ
- サイト:Little Village
DjangoRestFramework(以下DRF)関連の設定が完了したので、さっそく触ります!
よろしくね!
目次
記録
はてなブログの記事を取得するためのAPIの調査
まずはこれを知らなきゃどうしようもないので、検索していきます
ふむふむふむふむ!!!!
だいたい以下のことが必要そうですね
ぐちってても始まらないのでやっていきましょう!
認証方法を決める
はてなブログAPIを利用するのに認証が必要だが、認証方法に下記がある
WSSE認証
-
- 参考:OAuth認証をBasic認証と比較してみる
- あれのことか!最近よくあるログイン機能!
- GoogleアカウントとかTwitterアカウントでログインとかするやつ
- あれのこと。外部サービスを利用した認証だね
- クライアント自体にパスワードを保持する必要がないのがメリット
調べてみて、Basic認証でいいなと判断しました
はてなブログAPI呼び出し用のAPIを用意(空処理)
まずは
blogs/capture
にPOSTでアクセスすれば処理が走るようにしますやることは下記
urls
でblogs
をルーティングviews
にblogpy
を用意し、空処理を実行
views
ではまだblog用のmodelを用意してませんが、ModelViewset
を使います- 将来的にDBを使うことは確定なので
ひとまず仮のmodelとして、テスト用に作成していた
User
モデルを拝借しますその後
api/blogs/capture
にpostでアクセスし、正常に動作することを確認しました
はてなブログAPIに必要な情報を環境変数に追加
まずははてなブログの設定画面より、
AtomPub
のAPI Key
を確認そしてUbuntuとHerokuの環境変数に
HATENA_API_KEY
を設定します- (秘密情報なのでスクショなし)
ついでに
HATENA_API_USER
とHATENA_API_BLOG
も設定しておきましょう
はてなブログAPIを呼び出す
# coding: utf-8 import os import requests from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response from ..models import User from ..serializer import UserSerializer class BlogViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer @action(detail=False, methods=['post']) def capture(self, request, pk=None): url = self.getHatenaApiUrl('entry') auth = self.getHatenaApiAuth() res = requests.get(url, auth=auth) return Response(res.text) 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)
views/blog.py
のコードを上記のように変更しました。これで
api/blogs/capture
にpostでアクセスすると下記レスポンスになります
おわりに
小村の開発環境構築(21) GCP無料期間終了!!
ぎゃーーーす!!!
恐れていた事態が発生だよ!!!
GCPの無料トライアル期間が終了してしまいました!!!
というわけで、無料期間終了した際のメモを残しておきます
やること
- 過去の利用金額の確認
- 予算アラートの作成
- GCEの自動シャットダウン機能作成
手順
過去の利用金額の確認
まずは無料期間中の3か月間で利用した金額について調べましょうか
GCPの支払金額は、
お支払い
-レポート
から確認します
無料期間中なのでグラフはありませんが、金額はちゃんと確認できますね。
3カ月合計で1374円分の利用をしていたようです。
何度もGCEを消し忘れたりしたうえ平均500円以下ですし、でかい出費になる問題はなさそうですね。
予算アラートの作成
なんやかやで急激に支払料金が増えるリスクに備えて、予算アラートを設定しておきましょう
まず基本として、月額500円以内に抑えたいとおもっております
設定するアラートとしては、下記くらいでよさそうですかね
- 250円(50%)時点
- 400円(80%)時点
- 500円(100%)時点
- 700円(140%)時点
- 1000円(200%)時点
では設定していきます!
終わりました!はや!めちゃ簡単!
設定した内容は下記の通り。後はメールが届いてのお楽しみですね
GCEの自動シャットダウン機能作成
有料化にあたり、GCEの消し忘れもばかにならなくなってきますね
なんなら過去の料金の半分以上は消し忘れの時間だと思います
というわけで、自動でGCEを落とす仕組みを導入しましょう!
公式チュートリアル通り進める
上記の公式チュートリアル通りに進めるので詳細な手順は割愛します
ちなみにこのチュートリアル通り進めると、新たに
- Cloud Scheduler 月額 $0.10
- Cloud Functions 1回 $0.0000004 * 日数(仮に30)
- Cloud Pub/Sub 無料枠圏内
- Cloud Build おそらく無料枠圏内
を利用することとなりそうです。月額20円程度かな?
1回の消し忘れを防止するだけでおつりが出そうです。導入しましょう!
導入完了
手順通りに実装し(おそらく)問題なく完了!
変えたのはほんとにスケジュールの実行時間ぐらい
毎日午前1時にシャットダウンのPubSubを実行するようにしました!
導入にかかった時間は丁度1時間くらいかな?
あとは問題なく稼働することを確認するだけ。今日あたりつけっぱにしておこう
おわりに
やらなきゃなーと思っていたGCEの自動シャットダウンが導入できたー!
なんでもそうだけど、着手してしまえば案外こんなものかとなりますよね
- 逆もめっちゃありますが!
とはいえ使わない時にこまめに消すのが一番の節約なのは間違いない……
気にしてこまめにシャットダウンしていきたいと思います!
ではでは今日はこのへんで!ちゃお~~~!
小村の開発環境構築(20) DRF ディレクトリ構成変更、テスト機能設定
こんばんは!小村だよ!
前回まででDjangoRestFrameworkのHerokuへのデプロイが無事完了しました!
今回はDjangoRestFrameworkのテストが実行できるようにしていくよ!
よろしくね!
やること
- DRFフォルダ構成変更
- Pytestの導入
前提条件
- Ubuntuにリモート接続していること(これまでの環境構築参照)
手順
DRFフォルダ構成変更
現状のフォルダ構成は上記の通り
今後ファイルが増えることを見越して、フォルダ構成を変えていきます
変更後のディレクトリ構成は下記のとおりです
下記4つのファイルをディレクトリに変更し、クラスをそれぞれファイルに分けています
- models
- serializer
- tests
- views
これで無事稼働することを確認!
これを稼働させるためのポイントは3つ
フォルダ構成が変わったので、
from .models
となっていたのをfrom ..models
に変更フォルダ直下に
__init__.py
にを記述して、ファイルをインポートする- この際何もしないとlintエラーが出るため、
# noqa
を記述しエラーを発生させない
- この際何もしないとlintエラーが出るため、
models内で他のmodelを使うときはダブルクォートで囲う(下図参照)
- 通常なら
from user import User
を書いたうえでダブルクォートで囲まずUserを使う - しかし今回のように
__init__.py
で記載があるときはimportが不要となる
- 通常なら
Pytestの導入
では、Pytestを導入します!
下記を実行
pip install pytest pip install pytest-django pip freeze > requirements.txt
pytest.ini
を作成して、apiフォルダ配下に設置
あとはテストファイルを作成して動作確認
pytest
を実行して無事に動作することを確認!本格的な記述は後回しにさせてもらうぜ!