iPad Pro を注文しました
妻が還暦のお祝いにiPadを買ってくれるというので、速攻で (気が変わらないうちに) 発売したばかりの iPad Air (第5世代) を注文しました。
iPad Air はストレージのサイズが 64GB と256GB の 2 種類しかないので必然的に 256GB にしました。あと、WiFi+Cellular モデルにしました。これは、バッテリーが劣化して最近極端に使用頻度が落ちた Xperia X Compact に付けている IIJmio のsim を流用しようと思ったからです。
これで、価格は ¥110,800 です。念の為、妻に確認後、注文してから、落ち着いてレビューサイトなどで iPad Air のレビューを見ていたところ、iPad Air とiPad Pro を比較している記事を沢山みかけました。
そもそも、iPad Air を選んだ理由は「新しいものはいいものだ」、「iPad Pro は高いし、俺にはオーバースペック」という思い込みだけで選んだのでスペックなどを吟味したわけではありませんでした。
そういうわけで、実は私にとってはいつものことなのですが、買ってから詳細を調べるという、ある意味残念なことをしていたわけです。そこでわかった衝撃の事実 iPad Pro (128GB, WiFi+Cellular) との価格差はなんと ¥2,000。う〜ん、ちょっと悩んで、まずは iPhone で Apple Store を開いて注文をキャンセル出来るか確認。よし、キャンセルはできそうだ。
さらにレビューサイトで Air と Pro 比較記事を色々と読む。読めば読むほど、Air を買う理由がなくなくっていく。まぁこれは私の個人的な理由なので、Air の方がいい人は沢山いらっしゃると思います。わたしが、Pro の方が良いと思ったところは、
- 256GBのストレージは私には必要ない(たぶん)
- ディスプレイは Pro の方が良さそう
- ProはUSB-C/Thunderbolt搭載
と、いったところです。あと、あまり写真は撮らないのてカメラの違いは気になりません。
結局、2 時間くらい悩んで、Air をキャンセルして、Pro の方をあらためて注文しました。納期は 3 週間位かかりそうです。
新型コロナワクチンを接種してきました
1時間ほど前に新型コロナワクチンを接種してきました。前回 2 回は、ファイザー製で今回はあえてモデルナ製にしました。前回の副反応は少し発熱があったのと、一週間位倦怠感があった程度で生活や仕事に支障はありませんでしたが、今回はどうでしょう。モデルナ製は副反応がきついって言うし、ちょっとびびってます。
平熱が低いせいか、37 度位の熱でも相当きついのに、さて今回はどうなるでしょうか。
2022/3/13 8:13 追記
全体的に体がだるい。発熱も少しある。
2022/3/13 11:5 追記
解熱剤飲んで、寝てたせいかだいぶ楽になった
2022/3/13 14:13 追記
やっぱり、きついので 2 回目の解熱剤を飲んで、少し寝た。でもやっぱりだるい
2022/3/13 17:45 追記
微熱が続いている感じ、こりゃ噂通り2回目よりきつい
2022/3/13 19:1 追記
熱、上がってきた、38.4℃ ある。ロキソニン飲んだ
2022/03/13 20:56 追記
ロキソニン、恐るべし、熱も37℃位まで下がったし、ずいぶん楽になった
2022/03/14 17:30 追記
午前中はまぁまぁ大丈夫だっのだけれど、午後からまた熱が出てきた。37.4℃
2022/03/14 21:07 追記
19時ごろアセトアミノフェンの解熱剤飲んだけど、熱は下がらない。けど、なんか知らんけど少し楽になった気がする。
2022/03/15 12:33 追記
やっと、元に戻りました。
「北九州市 新型コロナウイルス感染症 陽性患者数」日毎集計プログラム
私が住んでいる北九州市では、市のホームページで市内の新型コロナウイルスの感染状況などが発信されています。
やはり、新型コロナウィルスの感染状況など気になるものですから、時々は見ていました。そんな中、上のページから「市内の最新感染動向」というページにたどり着き、そこから感染状況のデータがオープンデータとしてダウンロードできることを知りました。
GROWI へのデータ移行も終わり、せっかく Python に取り組み始めたので、また何か作ってみたいと思っていたこともあり、「これは良いデータを見つけた」と思い、このデータからカレンダー形式で日毎の感染者数を表示する HTML ファイルを出力するプログラムを作ってみることにしました。
先に完成形のスクリーンショットを載せておきます。
このように日毎の感染者数を集計して出力しています。ピンク色のセルは前週の同一曜日より感染者数が増えているところ、黄色のセルは減っているところです。
感染者数がいない (ゼロ) の日は白色にしています。
なんとなく、感染者数が減ってきているのがわかる気がします (2022/3/7 福岡県で実施中の「まん延防止等重点措置」が解除されました)
いきなり、スクリーンショットを載せましたが、実際にはこの段階では頭の中に完成形のイメージがあるだけですから、実現方法を考えていきます。
入力データ
まずは、どのような形式のデータがダウンロードできるのか確認します。対象とするデータは次のページからダウンロードできる「北九州市 新型コロナウイルス感染症 陽性患者属性」とします。
実際にダウンロードして中身を確認してみます。
1 行で陽性患者 1 名分となっています。[公表_年月日] があるので、これをキーにしてデータ件数を数えればよさそうです。
入力データの取得
GROWI データ移行で使用した requests モジュールが使えそうです。
入力データの読み込みと集計
入力データの形式が CSV なので、CSV パーサー探してきて日付をキーにしたディクショナリに集計すればよいと考えていていたところ pandas の入力に CSV データが指定できることがわかり、「読み込み→集計」が pandas で完結するんじゃないかと思い調べてみました。
まずは、簡単なテストプログラムを作ってみます。
import requests import io import pandas as pd url = 'https://ckan.open-governmentdata.org/dataset/aad66771-0e86-4d38-b08e-7b74d31f442e/resource/111b9476-bc80-4700-9551-3ba8a4ffcebc/download/401005_kitakyushu_covid19_patients.csv' res = requests.get(url) df = pd.read_csv(io.BytesIO(res.content), encoding='shift-jis', encoding_errors='ignore') print(df.head())
問題なさそうです。
encoding_errors='ignore' があるのは元データにデコードできないデータがあってエラーになってしまうので指定しています。
日毎の集計は DataFrame の size() メソッドでできそうなところめまでは、わりとすぐに突き止めたのですが、size() メソッドでちょっとハマりました。そもそも pandas は存在は知っていましたが、使うのは初めてなので DataFrame や Series などの特性もわかっていませんでした。
DataFrame に対して size() メソッド実行すると Series になるところまでは分かったのですが、Series から DataFrame に戻す方法がわからなかったりで、結構時間がかかりました。最終的にこのようなコードになっています。
number_of_patients = df.groupby('Date').size() \ .reset_index(name='Count') \ .set_index('Date')
- 'Date' をキーにして件数を数える
- reset_index() で Series から DataFrame に変換。この時、件数に 'Count' という名前をつける
- 'Date' (日付) をインデックスにする
Series から DataFrame に戻しているのは、この後各日のステータス (セルの色を変えるための「増えた」、「減った」の状態) の列を加えるためです。
HTML の出力
Python で使えるテンプレート エンジンを探したところ jinja2 を見つけましたので、これを使うことにします。
pandas 以外にも、いろいろなところでハマっていたので (pandas の使い方は、もっとしっかり勉強する必要がありそうです)、結構時間がかかりましたが、とりあえず完成しましたので、一日一回定時起動して生成した内容を公開しています。
データはあまり頻繁には更新されないみたいなので、今現在の状況はわかりませんが、おおよその傾向はわかるのではないかと思います。
たまには、ジャンクPCでも - ThinkPad X61
以前は、出張でよく東京に行っていたので、帰りに秋葉原に寄ってジャンクPCを買って帰ったりしていたのですが、ここ数年は出張に行かなくなり、ジャンクPCもしばらく買っていませんでした。
昨年の夏あたりだったと思うのですが、たまたまネットでジャンクのMacBookを再生してしている人のことを知り「あー、Macのジャンクいじっている人もいるんだなー」、「俺もやってみるかなー」と思ったのですが、地方に住んでいるので、ジャンクPCを手に入れるにはオークションかハードオフくらいしかありません。ハードオフも近所にはなく、以前もよく利用していたオークションでチェックするようになりました。
結局、MacBook Air のジャンクは 2, 3 台入手して楽しんで、仕掛かりの MacBook Air の部品を探しているうちに、手頃な値段の ThinkPad X61 を見つけました。もちろんジャンクです。以前から X61 は欲しかったので少し競りましたが、落札しました。
以前は、ThinkPad X60 や X61 はオークションに多く出品されていましたが、最近はめっきり少なくなりました。
送られてきた X61 の外観をざっとチェックしたところ大きなクラックはありませんでしたが、天板が傷だらけです。ただ、ThinkPad 特有のピーチスキンのベタつきがないのはよかったです。
次に電源入れて起動して、BIOS を確認します。メモリがどの程度載っているか気になっていましたので、確認したところ 4GB 載っていました。これは、嬉しい。仕様上の MAX 載っています。実際には 8GB まで載るらしいですが。
HDD には Windows10 がインストールされていたので、そのまま起動させたところ問題なく起動しました。ネットワークにも接続できたので H/W としては問題なさそうです。この Windows10 はなんか、問題ありそうなので、消して別の何かをインストールしましょう。
さて、この ThinkPad X61 の整備計画をたてます。
- まずはディスプレイから、右側 1/3 程度が暗くてちょっとつらい感じです。上の写真の右側が暗いのは影ではなく実際にこのような感じで暗いです。文字が読めないほどではないので、このままでもよいのですが、やはり気になるので、直したいと思っています。今のところバックライトを交換する方向で考えています。
- 天板はつや消しのクリア吹けばきれいになると思います。
- HDD は余ってる SSD に交換して、軽めの Linux でも入れるつもりです。
- バッテリーは完全にダメになっているのですが、このままにしておくことにします。
だいたいこんな感じでしょうか、完成がいつになるかはわかりませんが、ちょっとずつやっていこうと思います。
Visual Studio 2015 の UnitTest で Oracle.DataAccess が読み込めない
Visual Studio 2015 で Oracle.DataAccess を使った UnitTest を実行すると
System.BadImageFormatException: System.BadImageFormatException: ファイルまたはアセンブリ 'Oracle.DataAccess, Version=4.112.4.0, Culture=neutral, PublicKeyToken=89b483f429c47342'、またはその依存関係の 1 つが読み込めませんでした。間違ったフォーマットのプログラムを読み込もうとしました。
といった、エラーが発生してテストが実行できなかったが、意外なところに設定があった。
「既定のプロセッサ アーキテクチャ」を「X64」に変更すると動作するようになる。
OS も 64bit、Oracle.DataAccess.dll も 64bit なので当たり前といえば当たり前なのですが、設定箇所がわからずにずいぶん探しました。
Knowledge→GROWI 移行 (7)
移行実施
いよいよ、knowledge から GROWI にデータを移行します。移行に際して、下記のような移行用のプログラムを作成しました。使い捨てです。
import psycopg2 from psycopg2.extras import DictCursor from growiclient import GrowiClient # DB接続情報 DB_HOST = 'postgres' DB_PORT = '5432' DB_NAME = 'knowledgedb' DB_USER = 'kbadmin' DB_PASS = '**********' # GROWI 接続情報 GROWI_HOST = 'growi' GROWI_PORT = '3000' GROWI_SSL = False GROWI_API_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' GROWI_USER = 'tiger' # DB接続関数 def get_connection(): return psycopg2.connect('postgresql://{user}:{password}@{host}:{port}/{dbname}' .format(user=DB_USER, password=DB_PASS, host=DB_HOST, port=DB_PORT, dbname=DB_NAME)) # 添付ファイルの移行 def migrate_attachments(conn, growi_client, page, id): with conn.cursor(name='files_cursor', cursor_factory=DictCursor) as file_cur: id_col = 'knowledge_id' if growi_client.is_draft(): id_col = 'draft_id' file_cur.execute('select file_no, knowledge_id, comment_no, draft_id, file_name, file_binary ' 'from knowledge_files ' 'where {} = %s'.format(id_col), (id, )) for file_row in file_cur: print(file_row['file_name']) file_path = './knowledge_to_growi/attachments/' + file_row['file_name'] with open(file_path, 'wb') as f: f.write(file_row['file_binary']) file_url = growi_client.set_attachment(page, file_path) page.replace_attachment(file_row['file_name'], file_url) # Knowledge -> GROWI ページ移行 def migrage_knowledge(conn, growi_client): with conn.cursor(name='knowledges_cursor', cursor_factory=DictCursor) as kb_cur: if growi_client.is_draft(): kb_cur.execute('select draft_id, tag_names, title, content from draft_knowledges ' 'where delete_flag = 0 ' ' and (knowledge_id is null or knowledge_id not in (select knowledge_id from knowledges)) ' 'order by draft_id') else: kb_cur.execute('select knowledge_id, tag_names, title, content ' 'from knowledges ' 'where delete_flag = 0 ' 'order by knowledge_id') titles = {} for kb_row in kb_cur: if growi_client.is_draft(): id = kb_row['draft_id'] else: id = kb_row['knowledge_id'] print(str(id) + ' : ' + kb_row['title']) page_tags = [] if kb_row['tag_names']: page_tags = kb_row['tag_names'].replace(chr(0xa0), '').split(',') if len(page_tags) == 1: page_tags.append('') title = kb_row['title'] if title in titles: titles[title] += 1 title += '({})'.format(str(titles[title])) else: titles[title] = 1 content = kb_row['content'] if len(content) == 0: content = "## {}".format(title) page = growi_client.create_page(title, page_tags, content) migrate_attachments(conn, growi_client, page, id) growi_client.update_page(page) with get_connection() as conn: # 並び順を後ろの方にするためにドラフトページを先に移行 growi_client = GrowiClient( GROWI_HOST, GROWI_PORT, GROWI_API_KEY, GROWI_USER, GROWI_SSL, True) migrage_knowledge(conn, growi_client) # 公開ページの移行 growi_client = GrowiClient( GROWI_HOST, GROWI_PORT, GROWI_API_KEY, GROWI_USER, GROWI_SSL) migrage_knowledge(conn, growi_client)
実行して、簡単に内容を確認してみましたが大丈夫そうです。
移行元のデータベースは Docker コンテナの状態で保管しておくので、問題が見つかれば、その時に対応することにします。
実はもっと簡単に済むんじゃないかと思っていたのですが、意外に時間がかかりました。しかし、Python から使える GROWI のクライアントを手に入れたので、今後何かに使えるんじゃないかと思っています。
Knowledge→GROWI 移行 (6)
移行用の GROWI アクセス クラスを作る
GROWI API の使い方もだいたいわかってきたので、Knowledge→GROWI データ移行のプログラムは書けると思います。データの移行さえしてしまえばいいので、ダラダラと書いてもいいと思いましたが、Python の学習も兼ねて GROWI にアクセスするためのクラスを作ることにしました。
で、作ったのがこれです。
import os import requests import json import mimetypes import re class GrowiClient: """ GROWI クライアント """ def __init__(self, growihost, port, apitoken, username, ssl=False, draft=False): """ Parameters ---------- growihost : str GROWI ホスト名 apitoken : str GROWI API Token username : str GROWI ユーザ名 ssl : bool true : Yes false : No draft : bool true ドラフト false 公開 """ self.base_url = 'http{}://{}'.format('s' if ssl else '', growihost) if port: self.base_url += ':{}'.format(port) self.base_url += '/_api' self.base_path = '/{}'.format(username) self.params = {"access_token": apitoken, "user": username} self.draft = draft self.cur_pages = {} growi_res = self.__get('pages.list', {"limit": -1}) for page in growi_res['pages']: self.cur_pages[page['path']] = GrowiPage(page['_id'], page['path'], page['revision'], None, None, None) def create_page(self, title, tags, content): """ GROWI のベージを作製する Parameters ---------- title : str ページ・タイトル tags : array タグ content : str ページ本文 Retruns ------- page : GrowiPage GROWI のページを表すオブジェクト """ path = self.__to_path(title) if path in self.cur_pages: page = self.cur_pages[path] page.title = title page.tags = tags page.content = content self.__initialize_attachments_info(page) self.update_page(page) return page payload = {"body": content, "path": path} res = self.__post('v3/pages', payload) self.cur_pages[path] = GrowiPage(res['page']['id'], res['page']['path'], res['page']['revision'], title, tags, content) return self.cur_pages[path] def set_attachment(self, page, file_path): """ 指定されたファイルを指定された GROWI ページの添付ファイルとして設定する Parameters ---------- page : GrowiPage 移行対象の GROWI ページを表すオブジェクト file_path : str 移行するファイルのパス Retruns ------- file_url : 追加した添付ファイルの参照 url """ file_name = os.path.basename(file_path) attachment_info = page.get_attachment_info(file_name) if attachment_info: self.__remove_attachment(page, attachment_info) mime_type = mimetypes.guess_type(file_name)[0] file = {'file': (file_name, open(file_path, 'rb'), mime_type)} payload = {"page_id": page.id, "path": page.path} res = self.__post('attachments.add', payload, file) page.add_attachment_info(res['attachment']['id'], res['attachment']['originalName'], res['attachment']['filePathProxied']) return res['attachment']['filePathProxied'] def update_page(self, page): """ GROWI ページを更新する Parameters ---------- page : GrowiPage GROWI ページを表すオブジェクト """ payload = {"body": page.content, "pageTags": page.tags, "page_id": page.id, "revision_id": page.revision} res = self.__post('pages.update', payload) page.revision = res['page']['revision'] def is_draft(self): """ DRAFT ページに対する処理かどうかを返す Retruns ------- draft : bool True Yes False No """ return self.draft def __initialize_attachments_info(self, page): """ 指定された GROWI ページ情報の添付ファイル情報を初期化する Parameters ---------- page : GrowiPage GROWI ページを表すオブジェクト """ page_no = 1 while True: growi_res = self.__get('v3/attachment/list', {"pageId": page.id, "page": page_no}) if len(growi_res['paginateResult']['docs']) == 0: break page.initialize_attachments_info(growi_res) page_no += 1 def __remove_attachment(self, page, attachment_info): """ 添付ファイルを削除する Parameters ---------- page : GrowiPage GROWI ページを表すオブジェクト attachment_info : GrowiAttachment 削除する添付ファイル情報 """ print("Remove attachment : {}".format(attachment_info.original_name)) payload = {"attachment_id": attachment_info.id} self.__post('attachments.remove', payload) page.remove_attachment_info(attachment_info.id) def __post(self, verb, payload, file=None): """ GROWI サーバーに POST リクエストを行う Parameters ---------- verb : str GROWI API payload : dict リクエストボディ file : dict アップロードファイル情報 {'name': ('filename', fileobj)} Retruns ------- growi_res : json リクエストのレスポンス """ url = self.base_url + '/{}'.format(verb) res = requests.post(url, data=payload, files=file, params=self.params) res.raise_for_status growi_res = res.json() # print(json.dumps(growi_res, indent=4)) if 'errors' in growi_res: print(json.dumps(growi_res, indent=4)) return growi_res def __get(self, verb, params=None): """ GROWI サーバーに GET リクエストを行う Parameters ---------- verb : str GROWI API params : dict GET のパラメタ Retruns ------- growi_res : json リクエストのレスポンス """ url = self.base_url + '/{}'.format(verb) req_params = self.params.copy() if params: req_params.update(params) res = requests.get(url, params=req_params) res.raise_for_status growi_res = res.json() # print(json.dumps(growi_res, indent=4)) if 'errors' in growi_res: print(json.dumps(growi_res, indent=4)) return growi_res def __to_path(self, title): """ GROWI ページのバスを返す Parameters ---------- title : str ページ・タイトル Retruns ------- path : str GROWI のページのバス """ path = '{}/'.format(self.base_path) if self.draft: path += 'draft/' path += title.replace('^', '^') \ .replace('$', '$') \ .replace('*', '*') \ .replace('%', '%') \ .replace('?', '?') \ .replace('/', '/') return path class GrowiPage: """ GROWI ページを表す Attributes ---------- id : str ページ ID path : str パス revision : str リビジョン title : str タイトル tags : array タグ content : str 本文 """ def __init__(self, id, path, revision, title, tags, content): """ Parameters ---------- id : str ページ ID path : str パス revision : str リビジョン title : str タイトル tags : array タグ content : str 本文 """ self.id = id self.path = path self.revision = revision self.title = title self.tags = tags self.content = content self.attachments = {} def replace_attachment(self, file_name, file_path_proxied): """ 本文の指定されたファイルの参照 (リンク) を指定された参照 (リンク) に置き換える Parameters ---------- file_name : str 置き換えるフアイル名 file_path_proxied : str 置き換える参照 (リンク) """ self.content = re.sub(r'(!\[' + file_name + r'\])\(.+\)', r'\1(' + file_path_proxied + r')', self.content) def initialize_attachments_info(self, attachments_list_res): """ GROWI ページの添付ファイル情報を設定する Parameters ---------- attachments_list_res : json _api/v3/attachment/list の返却データ """ for attachment in attachments_list_res['paginateResult']['docs']: self.attachments[attachment['id']] \ = GrowiAttachment(attachment['id'], attachment['originalName'], attachment['filePathProxied']) def add_attachment_info(self, attachment_id, original_name, file_path_proxied): """ 添付ファイル情報を追加する Parameters ---------- id : str 添付ファイル ID original_name : str オリジナルのファイル名 file_path_proxied : str 添付ファイルの参照パス (リンク) """ self.attachments[attachment_id] = GrowiAttachment(attachment_id, original_name, file_path_proxied) def get_attachment_info(self, file_name): """ この GROWI ページが指定されたファイル名の添付ファイル情報を返す Parameters ---------- file_name : str ファイル名 Retruns ------- attachment : GrowiAttachment 添付ファイル情報 添付ファイルが存在しない場合は None """ attachment = [self.attachments[id] for id in self.attachments if self.attachments[id].original_name == file_name] if attachment: return attachment[0] return None def remove_attachment_info(self, attachment_id): """ この GROWI ページから指定された添付ファイル ID の添付ファイル情報を削除する Parameters ---------- attachment_id : str 添付ファイル ID """ del self.attachments[attachment_id] class GrowiAttachment: """ GROWI 添付ファイルを表す Attributes ---------- id : str 添付ファイル ID original_name : str オリジナルのファイル名 file_path_proxied : str 添付ファイルの参照パス (リンク) """ def __init__(self, id, original_name, file_path_proxied): """ Parameters ---------- id : str 添付ファイル ID original_name : str オリジナルのファイル名 file_path_proxied : str 添付ファイルの参照パス (リンク) """ self.id = id self.original_name = original_name self.file_path_proxied = file_path_proxied
使い方は
- GrowiClient のインスタンスを作る
- create_page で新しい記事を作成する
- 添付ファイルがあれば set_attachment で追加する
- 必要なら記事内の添付ファイルの参照 url を書き換える
- 添付ファイルの数分実施する
- 添付ファイルの url を書き換えたり、タグがあるなら update_page で記事を更新する
- 記事の数分、2〜4 を繰り返す
次のコードは、新しく記事を作って、ファイルを添付して、さらにそれを更新するものです。
from growiclient import GrowiClient growi_client = GrowiClient('growi', '3000', 'rMVCHsrPDuN7wlZfVOn9lWgqC5flSd2yjtqffO4T4aw=', 'tiger') title = 'GlowiClientで作成したページ' content = """\ このページは Python で作成した GLOWI Client で作成しました。 ![tora.png](/tora.png) """ page = growi_client.create_page(title, ['GlowiClient', 'TEST'], content) file_url = growi_client.set_attachment(page, './knowledge_to_growi/attachments/tora.png') page.replace_attachment('tora.png', file_url) growi_client.update_page(page) content = page.content + \ """ \ \n ページを更新します。 ![ojiisan.png](/ojiisan.png) """ page = growi_client.create_page(page.title, page.tags, content) file_url = growi_client.set_attachment(page, './knowledge_to_growi/attachments/ojiisan.png') page.replace_attachment('ojiisan.png', file_url) growi_client.update_page(page)
こんな、記事が作成されます。