ほぼ老人のプログラミング日記

定年後の平凡なサラリーマンの趣味の日記

Knowledge→GROWI 移行 (5)

GROWI API の使い方を調べる

GROWI API の使い方を調べます。ドキュメントは下記のページにあります。

docs.growi.org

私にはドキュメント読んでも具体的なイメージがわかなかったので、やはり Google で検索すると、私にピッタリなサイトが見つかりました。

kapibara-sos.net

まずは、そのまま使わせていただきます。requests モジュールが必要なので、.devcontainer/python/requirement.txt に requests を追加してコンテナを再ビルドしました。
そういえば、以前 PostgreSQL にアクセスするのに psycopg2 モジュールを使いました。この時も .devcontainer/python/requirement.txt に psycopg2 を追加してコンテナを再ビルドしました。

簡単に動かしてみます

from crclient import CrClient

crclient = CrClient('growi', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'tiger', './knowledge_to_growi/contents')
crclient.fetch()

確認用に記事を 1 件だけ登録してあるのですが、なんか取得できているようです。

#  cd /workspace ; /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher 43313 -- /workspace/knowledge_to_growi/crclient_sample.py 
skip item: {'_id': '61f9120a32a52003888290b5', 'status': 'published', 'grant': 1, 'grantedUsers': [], 'liker': [], 'seenUsers': ['61f7bcc6834bc4803f0e2bfa'], 'commentCount': 0, 'createdAt': '2022-02-01T10:57:14.468Z', 'updatedAt': '2022-02-01T10:57:14.488Z', 'path': '/user/tiger/自分自身のこと', 'creator': '61f7bcc6834bc4803f0e2bfa', 'lastUpdateUser': {'_id': '61f7bcc6834bc4803f0e2bfa', 'isGravatarEnabled': False, 'isEmailPublished': True, 'lang': 'ja_JP', 'status': 2, 'admin': False, 'createdAt': '2022-01-31T10:41:10.624Z', 'username': 'tiger', 'email': 'tiger@example.com', 'lastLoginAt': '2022-02-01T10:49:07.549Z', 'imageUrlCached': '/images/icons/user.svg', 'name': 'tiger62shin'}, 'redirectTo': None, 'grantedGroup': None, '__v': 1, 'revision': '61f9120a32a52003888290b9'}
skip item: {'_id': '61f7bd73834bc4803f0e2c25', 'status': 'published', 'grant': 1, 'grantedUsers': ['61f7bcc6834bc4803f0e2bfa'], 'liker': [], 'seenUsers': ['61f7bcc6834bc4803f0e2bfa'], 'commentCount': 0, 'createdAt': '2022-01-31T10:44:03.214Z', 'updatedAt': '2022-01-31T10:44:03.239Z', 'path': '/user/tiger', 'creator': '61f7bcc6834bc4803f0e2bfa', 'lastUpdateUser': {'_id': '61f7bcc6834bc4803f0e2bfa', 'isGravatarEnabled': False, 'isEmailPublished': True, 'lang': 'ja_JP', 'status': 2, 'admin': False, 'createdAt': '2022-01-31T10:41:10.624Z', 'username': 'tiger', 'email': 'tiger@example.com', 'lastLoginAt': '2022-02-01T10:49:07.549Z', 'imageUrlCached': '/images/icons/user.svg', 'name': 'tiger62shin'}, 'redirectTo': None, 'grantedGroup': None, '__v': 1, 'revision': '61f7bd73834bc4803f0e2c2b'}```

このソースを読んで、動きを確認したことで、なんとなく感覚はつかめたような気がします。API である URL にパラメタを付けて GET / POST でリクエストすると結果が JSON で帰ってくるという一般的な Web API と理解しました。

当初、この CrClient をそのままか、若干の修正で使わせていただこうと思っていたのですが、

  • データ移行なので更新の機能は必要ない
  • 1 回だけ新規登録できれば良い。失敗したらマニュアルで削除して再実行すれば良い
  • 添付ファイルが付けられない

ということで、やはり自分で作ることにしました。
必要な機能は多くはなく

  • 記事の新規登録
  • 記事へのファイル添付
    添付したファイルの url が変わるはずなので、下記のような手順になると思う
    1. 記事を新規登録する
    2. 記事にファイルを添付する
    3. 記事の添付ファイル参照 URL を書き換える

以下では機能ごとに調べていきますが、先にこれまで記載したサイトの他に参考にさせて頂いたのですがサイトを載せておきます。

s-densan.hatenablog.com

あと、GROWI API のドキュメントのサイトにある OpenAPI specification はよく参照しました。
以下の GROWI の API 利用の例では、今回のデータ移行に必要なパラメータやレスポンスのみ記載しました。

記事の新規登録

項目
URL _api/v3/pages
Method POST
Query Paramaters access_token : API Token
user : ユーザID
Payload body : 本文
path : 記事のパス
Response id : 記事のID
path : 記事のパス
revision : 記事のリビジョン
import requests

url = 'http://growi:3000/_api/v3/pages'
params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
          "user": "tiger"}

path = "新規記事登録のサンプル"
content = "この記事は GROWI API を使って登録した記事です。"
payload = {"body": content, "path": "/tiger/{}".format(path)}
res = requests.post(url, data=payload, params=params)
pages_res = res.json()
print('id : ' + pages_res['page']['id'])
print('path : ' + pages_res['page']['path'])
print('revision : ' + pages_res['page']['revision'])

このプログラムを実行するとコンソールに下記のように出力されます。

root@8949de830c08:/workspace#  /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher 39727 -- /workspace/knowledge_to_growi/growiapi_sample1.py 
id : 61fa6161a03e4c6720a5f36a
path : /tiger/新規記事登録のサンプル
revision : 61fa6161a03e4c6720a5f36e

ブラウザで確認してみます。

f:id:tiger62shin:20220202195004p:plain

ちゃんと登録されました。

記事へのファイル添付

項目
URL _api/attachments.add
Method POST
Query Paramaters access_token : API Token
user : ユーザID
Payload page_id : 記事のID
path : 記事のバス
記事の新規登録で戻ってきた値を指定
file ファイル名, ファイル本体
Response id : 添付ファイルのID
originalName : ファイル名
filePathProxied : 添付ファイルのパス

先に作成した記事に下記の画像を添付してみます。

f:id:tiger62shin:20220202195818p:plain

import requests

url = 'http://growi:3000/_api/attachments.add'
params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
          "user": "tiger"}

file = {'file': ('setsubun_mamemaki.png',
                 open('./knowledge_to_growi/attachments/setsubun_mamemaki.png', 'rb'),
                 'image/png')}
payload = {'page_id': '61fa6161a03e4c6720a5f36a',
           'path': '/tiger/新規記事登録のサンプル'}
res = requests.post(url, data=payload, files=file, params=params)
res.raise_for_status
attachments_res = res.json()
print('id : {} , originalName : {} , filePathProxied : {} '
      .format(attachments_res['attachment']['id'],
              attachments_res['attachment']['originalName'],
              attachments_res['attachment']['filePathProxied']))

このプログラムを実行するとコンソールに下記のように出力されます。

  cd /workspace ; /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2022.0.1786462952/pythonFiles/lib/python/debugpy/launcher 40267 -- /workspace/knowledge_to_growi/growiapi_sample2.py 
id : 61fe69c0f6ed08f016d32137 , originalName : setsubun_mamemaki.png , filePathProxied : /attachment/61fe69c0f6ed08f016d32137 
root@83fe8d69003a:/workspace# 

ブラウザで確認してみます。

f:id:tiger62shin:20220202201534p:plain

ちゃんと添付されました。

記事の更新

項目
URL _api/pages.update
Method POST
Query Paramaters access_token : API Token
user : ユーザID
Payload body : 本文
pageTags : タグのリスト
page_id : 記事のID (*)
path : 記事のバス (*)
revision_id : リビジョンID(*)
(*) 記事の新規登録で戻ってきた値を指定
Response revision : 記事のリビジョン (*)
(*)更新するたびにリビジョンが変わるので何度も更新する場合は注意が必要

データ移行では本来、記事を更新する必要はないのですが、記事内の添付ファイル参照の url を書き換える必要があります。
下のソースは最初に登録した記事に添付ファイルの画像を表示するように更新するものです。

import requests

url = 'http://growi:3000/_api/pages.update'
params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
          "user": "tiger"}

content = 'この記事は GROWI API を使って登録した記事です。' \
          '\n\n' \
          '![setsubun_mamemaki.png](/attachment/61fe69c0f6ed08f016d32137)'
payload = {'body': content,
           'page_id': '61fa6161a03e4c6720a5f36a',
           'path': '/tiger/新規記事登録のサンプル',
           'revision_id': '61fa6161a03e4c6720a5f36e'}
res = requests.post(url, data=payload, params=params)
res.raise_for_status

ブラウザで確認してみます。

f:id:tiger62shin:20220202211747p:plain

記事が更新され、画像が表示されました。


以上で、データ移行に必要な GROWI API の使い方はわかったのですが、その他にも以下のような API について使い方を調べました。

記事の一覧

項目
URL _api/pages.list
Method GET
Query Paramaters access_token : API Token
user : ユーザID
limit : 最大取得件数、-1 を指定すると全件
Payload N/A
Response _id : 記事のID
path : 記事のパス
revision : 記事のリビジョン

今回のデータ移行では、最初に全ての記事のリストを取得しておき、移行対象記事の存在チェックに利用しました。

import requests

url = 'http://growi:3000/_api/pages.list'
params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
          'user': 'tiger',
          'limit': -1}

res = requests.get(url, params=params)
res.raise_for_status
pages_list_res = res.json()
for page in pages_list_res['pages']:
    print('id : {} / path : {} / revision : {} '
          .format(page['_id'], page['path'], page['revision']))

このプログラムを実行するとコンソールに下記のように出力されます。

#  /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2022.0.1786462952/pythonFiles/lib/python/debugpy/launcher 41573 -- /workspace/knowledge_to_growi/growiapi_sample4.py 
id : 61fa6161a03e4c6720a5f36a / path : /tiger/新規記事登録のサンプル / revision : 61fa7643a03e4c6720a5f4f2 
id : 61f9120a32a52003888290b5 / path : /user/tiger/自分自身のこと / revision : 61f9120a32a52003888290b9 
id : 61f7bd73834bc4803f0e2c25 / path : /user/tiger / revision : 61f7bd73834bc4803f0e2c2b

添付ファイルの一覧

項目
URL _api/v3/attachment/list
Method GET
Query Paramaters access_token : API Token
user : ユーザID
pageid : 記事の ID (*)
page : 添付ファイル一覧のページ番号
(*) 記事の新規登録 / 記事の一覧の取得で戻ってきた値を指定
Payload N/A
Response id : 添付ファイルのID
originalName : 添付ファイル名
filePathProxied : 添付ファイルの url パス

この API は少し注意が必要です。取得できる添付ファイル情報は画面に表示される添付ファイル一覧の 1 ページ分しか取得できません。幸い page パラメーターで取得する一覧のページ番号を指定できますので、データが取得できなくなるまでページ番号をインクリメントしながら取得します。
今回のデータ移行では、すでに存在している記事に対して移行する際の添付ファイルの存在チェックに利用しました。

import requests

url = 'http://growi:3000/_api/v3/attachment/list'
params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
          'user': 'tiger',
          'pageId': '61fa6161a03e4c6720a5f36a',
          'page': 1}

res = requests.get(url, params=params)
res.raise_for_status
attachment_list_res = res.json()
for attachment in attachment_list_res['paginateResult']['docs']:
    print('id : {} , originalName : {} , filePathProxied : {} '
          .format(attachment['id'], attachment['originalName'],
                  attachment['filePathProxied']))

このプログラムを実行するとコンソールに下記のように出力されます。

#  cd /workspace ; /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2022.0.1786462952/pythonFiles/lib/python/debugpy/launcher 41445 -- /workspace/knowledge_to_growi/growiapi_sample5.py 
id : 61fa6799a03e4c6720a5f490 , originalName : setsubun_mamemaki.png , filePathProxied : /attachment/61fa6799a03e4c6720a5f490 

添付ファイルの削除

項目
URL _api/attachments.remove
Method POST
Query Paramaters access_token : API Token
user : ユーザID
Payload attachment_id : 添付ファイルID
記事へのファイル添付 / 添付ファイルの一覧取得で戻ってきた値を指定
Response -

今回のデータ移行では、すでに存在している添付ファイルを置き換える際の削除に利用しました。

import requests

url = 'http://growi:3000/_api/attachments.remove'
params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
          'user': 'tiger'}

payload = {'attachment_id': '61fa6799a03e4c6720a5f490'}
res = requests.post(url, data=payload, params=params)
res.raise_for_status

このサンプルでは「記事へのファイル添付」で添付したファイルを削除しています。

f:id:tiger62shin:20220204212349p:plain

添付ファイルが削除されたので表示されなくなりました。


以上です。
あと、既存の記事の取得とかあればよかったのでしょうが、今回のデータ移行には必要なかったので調べませんでした。

Knowledge→GROWI 移行 (4)

Visual Studio Code に GROWI の Docker コンテナを追加

GROWI は元々が Docker コンテナで導入するのが基本のようなので、それを利用します。

docs.growi.org

テスト用だし git clone する必要はないので、ここ から growi-docker-compose を zip ファイルでダウンロードします。
ダウンロードしたファイルを作業ディレクトリに解凍します。

% unzip growi-docker-compose-master.zip
% ls -l growi-docker-compose-master
total 64
-rw-r--r--@ 1 tiger  staff   412 12 30 23:02 Dockerfile
-rw-r--r--@ 1 tiger  staff   980 12 30 23:02 Dockerfile.v42x
-rw-r--r--@ 1 tiger  staff  1069 12 30 23:02 LICENSE
-rw-r--r--@ 1 tiger  staff  3612 12 30 23:02 README.md
-rw-r--r--@ 1 tiger  staff  1266 12 30 23:02 docker-compose.dev.yml
-rw-r--r--@ 1 tiger  staff  2217 12 30 23:02 docker-compose.v42x.yml
-rw-r--r--@ 1 tiger  staff  2305 12 30 23:02 docker-compose.v43x-v446.yml
-rw-r--r--@ 1 tiger  staff  2429 12 30 23:02 docker-compose.yml
drwxr-xr-x@ 4 tiger  staff   128 12 30 23:02 elasticsearch
drwxr-xr-x@ 7 tiger  staff   224 12 30 23:02 examples
drwxr-xr-x@ 5 tiger  staff   160 12 30 23:02 hackmd

かるく(?) docker-compose.yml を覗いてみます。growi 本体と mongodb, elasticsearch の 3 つのコンテナで構成されているようです。
.devcontainer フォルダの直下に GROWI 用の Dockerfile を置くフォルダ growi を作成して growi-docker-compose-master/Dockerfile, LICENSE, README.md をコビーします (LICENSE, README.md は動作には必要ないですが)。
以下の例はカレントディレクトがプロジェクトフォルダでプロジェクトフォルダ直下に growi-docker-compose-master.zip を展開しています。

% mkdir -p .devcontainer/growi
% cp growi-docker-compose-master/Dockerfile .devcontainer/growi
% cp growi-docker-compose-master/LICENSE .devcontainer/growi
% cp growi-docker-compose-master/README.md .devcontainer/growi

次に growi-docker-compose-master/elasticsearch をフォルダごと .devcontainer にコピーします。

% cp -R growi-docker-compose-master/elasticsearch .devcontainer

.devcontainer/docker-compose.yml に GROWI の設定を記述するのですが、基本的には growi-docker-compose-master/docker-compose.yml の内容を追記すれば良いはずです。

version: '3'
services:
  python38:
    restart: always
    build: python
    container_name: 'python38'
    working_dir: '/workspace'
    tty: true
    extra_hosts:
      - "fluorine.kt.asasystems.co.jp:192.168.100.78"
    environment:
      - DISPLAY=${IP_ADDR}:0.0
    volumes:
      - ..:/workspace:cached
      - /tmp/.X11-unix:/tmp/.X11-unix

  postgres:
    restart: always
    build: postgres
    container_name: 'postgres12'
    ports:
      - 5433:5433
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - ./postgres/initdb:/docker-entrypoint-initdb.d

  growi:
    build: growi
    container_name: 'growi'
    ports:
      - 127.0.0.1:3000:3000    # localhost only by default
    links:
      - mongo:mongo
      - elasticsearch:elasticsearch
    depends_on:
      - mongo
      - elasticsearch
    environment:
      - MONGO_URI=mongodb://mongo:27017/growi
      - ELASTICSEARCH_URI=http://elasticsearch:9200/growi
      - PASSWORD_SEED=changeme
      - FILE_UPLOAD=mongodb   # activate this line if you use MongoDB GridFS rather than AWS
      # - FILE_UPLOAD=local     # activate this line if you use local storage of server rather than AWS
      # - MATHJAX=1             # activate this line if you want to use MathJax
      # - PLANTUML_URI=http://  # activate this line and specify if you use your own PlantUML server rather than public plantuml.com
      # - HACKMD_URI=http://    # activate this line and specify HackMD server URI which can be accessed from GROWI client browsers
      # - HACKMD_URI_FOR_SERVER=http://hackmd:3000  # activate this line and specify HackMD server URI which can be accessed from this server container
      # - FORCE_WIKI_MODE='public'    # activate this line to force wiki public mode
      # - FORCE_WIKI_MODE='private'   # activate this line to force wiki private mode

    entrypoint: "dockerize
                  -wait tcp://mongo:27017
                  -wait tcp://elasticsearch:9200
                  -timeout 60s
                  /docker-entrypoint.sh"
    command: ["yarn migrate && node -r dotenv-flow/config --expose_gc dist/server/app.js"]

    restart: unless-stopped
    volumes:
      - growi_data:/data

  mongo:
    image: mongo:4.4
    container_name: 'mongo'
    restart: unless-stopped
    volumes:
      - mongo_configdb:/data/configdb
      - mongo_db:/data/db

  elasticsearch:
    build: elasticsearch
    container_name: 'elasticsearch'
    environment:
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms256m -Xmx256m"  # increase amount if you have enough memory
      - LOG4J_FORMAT_MSG_NO_LOOKUPS=true # CVE-2021-44228 mitigation for Elasticsearch <= 6.8.20/7.16.0
    ulimits:
      memlock:
        soft: -1
        hard: -1
    restart: unless-stopped
    volumes:
      - es_data:/usr/share/elasticsearch/data
      - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml

volumes:
  growi_data:
    driver_opts:
      type: none
      device: /Users/user/workspace/project/ToolsByPython/docker_volumes/growi_data
      o: bind
  mongo_configdb:
    driver_opts:
      type: none
      device: /Users/user/workspace/project/ToolsByPython/docker_volumes/mongo_configdb
      o: bind
  mongo_db:
    driver_opts:
      type: none
      device: /Users/user/workspace/project/ToolsByPython/docker_volumes/mongo_db
      o: bind
  es_data:
    driver_opts:
      type: none
      device: /Users/user/workspace/project/ToolsByPython/docker_volumes/es_data
      o: bind
  es_plugins:
    driver_opts:
      type: none
      device: /Users/user/workspace/project/ToolsByPython/docker_volumes/es_plugins
      o: bind

変更点は以下の通りです。

  • growi コンテナ
    • サービス名が app になっているので、growi に変更
    • カレントディレクトリの Dockerfile をビルドする様になっているで growi ディレクトリ下の Dockerfile をビルドするように変更
    • コンテナ名を "growi" に設定
    • FILE_UPLOAD に "mongodb" を指定して、添付ファイルを MongoDB に格納するよう設定
  • mongo コンテナと elasticsearch コンテナは変更なし
  • volumes
    デフォルトのままでもよかったが、なんとなくローカルのディレクトリを指定した。
    最初、.devcontainer ディレクトリ下のディレクトリを指定したところ、パスに隠しディレトリがあるとコンテナのビルドでエラーになってしまいました。パスに隠しディレクトリを含まない場所にディレクトリを作成して指定しました。


設定は以上で終了です。Visual Studio Code を起動します。Docker コンテナが正常に起動されたならブラウザから http://localhost:3000 にアクセスして GROWI が正しく起動されていることを確認します。

f:id:tiger62shin:20220131193511p:plain

上の画面から管理者ユーザを登録して GROWI の初期設定を行います。データ移行のテスト用なので、最低限の設定しか行いませんでした。

f:id:tiger62shin:20220131193954p:plain

上の図のように「サイトURL」のみ設定しました。
次に実際に記事の移行先となる一般ユーザを登録して、プログラムから GROWI API を利用するための API Token を取得します。

  • 管理者により「ユーザー管理」から一般ユーザの仮パスワードを発行
  • 一般ユーザのメールアドレスと仮パスワードでログイン
  • ユーザ情報の入力
  • API Token を発行
    ページ右上に表示されているユーザ ID をクリックして [設定] ボタンをクリックします。

    f:id:tiger62shin:20220131194634p:plain

    API設定」タブの [API Token を更新] ボタンをクリックして API Token を発行します。


これで、Knowledge→GROWI データ移行のためのテスト環境が整いました。長かった。
毎度のことですが、プログラミングって環境作るのが大変で、経験したことない人は環境作るところで挫折するっていう話をどこかで聞いた (読んだ) ことがありますが、本当な気がします。

Knowledge→GROWI 移行 (3)

Python で Knowledge のデータベース (PostgreSQL) にアクセスしてみる

PythonPostgreSQL にアクセスするする方法はさっぱり分かりませんから、いつものように Google で検索します。

たくさんヒットしますが、下記のサイトを参考にさせていただきました。

chusotsu-program.com

qiita.com

まずは、knowledges テーブルから knowledge_id と title を取り出してみます。全部取り出すと大変なので、条件をつけて 5 件だけ取り出します。

import psycopg2
from psycopg2.extras import DictCursor

# DB接続情報
DB_HOST = 'postgres'
DB_PORT = '5432'
DB_NAME = 'knowledgedb'
DB_USER = 'kbadmin'
DB_PASS = '**********'


# 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))


with get_connection() as conn:
    with conn.cursor(cursor_factory=DictCursor) as cur:
        cur.execute('select knowledge_id, title from knowledges '
                    'where knowledge_id in (217, 227, 229, 236, 240)')
        for row in cur:
            print(str(row['knowledge_id']) + ' : ' + row['title'])

参考にさせてもらったサイトのサンプルを幾つか組み合わせただけです。取得した結果から列のデータを辞書式で取得したかったので、DictCursor を使いました。

結果はこんな感じです。

root@ced5581a8a4a:/workspace#  cd /workspace ; /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher 41367 -- /workspace/knowledge_to_growi/postgres_sample.py 
240 : Mac 関連のメモ
229 : Visual Studio Code 設定
227 : Docker
236 : POP3 サーバ内メールを取得して IMAP メールボックスに配信
217 : ORACLE メモ

続けて、添付ファイルを取得してみます。

        cur.execute('select file_no, knowledge_id, comment_no, draft_id, file_name, file_binary '
                    'from knowledge_files where file_no = 556')
        for row in cur:
            print(row['file_name'])
            file_name = './knowledge_to_growi/attachments/' \
                        + row['file_name']
            with open(file_name, 'wb') as f:
                f.write(row['file_binary'])

f:id:tiger62shin:20220131151018p:plain 添付ファイルもちゃんと取得できました。ところが、このままではちょっと問題があります。
私が実現したいのは「見出しや本文を取り出しつつ、その添付ファイルも取り出す」ことなので、上のコーディングだと cursor が 1 つしかないので knowledges から「見出し」や「本文」を一旦全部取り出してから、knowledge_files から添付ファイルを取り出すようにしないと上手くいかないと思います。それでもいいのですが、あまり好きな方法ではないので knowledges 用と knowledge_files 用の 2 つのカーソルを使います。ついでに目的の記事に添付されているファイルのみを取り出すようにします。

    with conn.cursor(cursor_factory=DictCursor) as knowledges_cur:
        knowledges_cur.execute('select knowledge_id, title from knowledges '
                               'where knowledge_id in (217, 227, 229, 236, 240)')
        for knowledge_row in knowledges_cur:
            print(str(knowledge_row['knowledge_id']) + ' : ' + knowledge_row['title'])

            with conn.cursor(cursor_factory=DictCursor) as file_cur:
                file_cur.execute('select file_no, knowledge_id, comment_no, draft_id, file_name, file_binary '
                                 'from knowledge_files '
                                 'where knowledge_id = %s', (knowledge_row['knowledge_id'], ))
                for file_row in file_cur:
                    print(file_row['file_name'])
                    file_name = './knowledge_to_growi/attachments/' + file_row['file_name']
                    with open(file_name, 'wb') as f:
                        f.write(file_row['file_binary'])

だいぶ、ネストが深くなりましたが、これで knowledges テーブルから記事を取り出しつつ、knowledge_files テーブルからその記事の添付ファイルを取り出すことが出来ました。
draft_knowledges からも同様の方法で取得できるはずです。


これで、GROWI に移行するデータを取得する目処がたちました。

Knowledge→GROWI 移行 (2)

Visual Studio CodePostgreSQL の Docker コンテナを追加

コーディングまでの道のりは長い。

tiger62shin.hatenablog.com

上の記事で Visual Studio CodePython の実行環境である Docker コンテナに接続して Python のプログラムが実行できるところまでは設定しましたが、Knowledge から移行データを抜くためのデータベース環境を作っていませんでした。
今回は Visual Studio CodePostgreSQL の Docker コンテナを追加します。


まずは、GooglePostgreSQL の Dockerfile の雛形を探します。ありがたいことに手順が解説されているサイトがたくさん見つかります。私は下記のサイトを参考にさせていただきました。

crudzoo.com

まず、.devcontainer フォルダの直下に PostgreSQL 用の Dockerfile 等を置くフォルダを作成します。

.devcontainer/postgres

このフォルダに上のサイトを参考にして Dockerfile を作成します。

FROM postgres:12-alpine

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9

ENV は LANG だけあれば良いようですが、他のコンテナでも設定しているので、LANGUAGE, LC_ALL, TZ も設定しておきました。特に意味はないです。
このままでも PostgreSQL のコンテナは問題なく起動しますが、目的のデータベースも何もない状態なのでコンテナのビルド時に移行対象のデータベースをインポートしたい思います。ここでも Google 先生に頼ります。下記のようなサイトが見つかりました。

qiita.com

/docker-entrypoint-initdb.d に置かれた .sql / .sh ファイルが 1 度だけ実行されるそうです。では、/docker-entrypoint-initdb.d に置くファイルを準備します。
まず、移行元サーバーから移行元データベースのダンプを取得してきます。

$ pg_dump knowledgedb -U kbadmin >knowledgedb.dump

次にデータベース / ユーザを作成する SQL ファイルを準備します。ファイル名は createdb.sql とします。

CREATE ROLE kbadmin LOGIN PASSWORD '**********';
ALTER ROLE kbadmin SUPERUSER;
create database knowledgedb;

最後にデータベース / ユーザを作成してデータをインポートするシェル (01-initdb.sh) を作成します。

#!/bin/sh

PGPASSWORD=**********
psql < /docker-entrypoint-initdb.d/sql/createdb.sql
psql -U kbadmin -d knowledgedb < /docker-entrypoint-initdb.d/sql/knowledgedb.dump

これらのファイルを下図のように .devcontainer/postgres フォルダに配置します。

f:id:tiger62shin:20220129223535p:plain

docker-compose.yml に PostgreSQL の設定を追加します。

version: '3'
services:
  python38:
    restart: always
    build: python
    container_name: 'python38'
    working_dir: '/workspace'
    tty: true
    extra_hosts:
      - "fluorine.kt.asasystems.co.jp:192.168.100.78"
    environment:
      - DISPLAY=${IP_ADDR}:0.0
    volumes:
      - ..:/workspace:cached
      - /tmp/.X11-unix:/tmp/.X11-unix

  postgres:
    restart: always
    build: postgres
    container_name: 'postgres12'
    ports:
      - 5433:5433
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: **********
    volumes:
      - ./postgres/initdb:/docker-entrypoint-initdb.d

postgres の公式 docker image は envrionement の値を読みとり初期設定を行なう機能があるそうです。私は POSTGRES_USER と POSTGRES_PASSWORD を指定しました。POSTGRES_DB を省略すると POSTGRES_USER で指定した名前と同じ名前のデータベースが作成されるそうです。
あと、volumes でデータベースを初期化するシェルのあるフォルダを /docker-entrypoint-initdb.d に割り当てます。


これで、Docker コンテナの設定は完了しました。
Visual Studio Code を起動してみます。エラーなく起動できたら PythonPostgreSQL の Docker コンテナが起動しているか確認してみます。

% docker ps
CONTAINER ID   IMAGE                                      COMMAND                  CREATED         STATUS         PORTS                              NAMES
02ee35af85df   toolsbypython_devcontainer_postgres        "docker-entrypoint.s…"   22 hours ago    Up 6 minutes   5432/tcp, 0.0.0.0:5433->5433/tcp   postgres12
ced5581a8a4a   toolsbypython_devcontainer_python38        "python3"                22 hours ago    Up 6 minutes   6006/tcp                           python38

問題なさそうです。では、Knowledge のデータベースが作成されているかどうかを確認してみます。
まず、PostgreSQL の Docker コンテナにログインします。私は Docker コンテナにログインするのに下記のようなシェルを作成しています。
docker-shell.sh

#!/bin/sh

docker exec -i -t `docker ps | grep "$1" | awk '{print $1;}'` /bin/bash

では、ログイン

% docker-shell.sh postgres
bash-5.1# 

データベースが作成 (インポート) されているか確認するために psql を起動してテーブルの一覧でも表示してみます。

bash-5.1# psql -U kbadmin -d knowledgedb
psql (12.9)
Type "help" for help.

knowledgedb=# \d
                      List of relations
 Schema |             Name              |   Type   |  Owner  
--------+-------------------------------+----------+---------
 public | access_logs                   | table    | kbadmin
 public | access_logs_no_seq            | sequence | kbadmin
 public | account_images                | table    | kbadmin
 public | account_images_image_id_seq   | sequence | kbadmin
 public | activities                    | table    | kbadmin
 public | activities_activity_no_seq    | sequence | kbadmin
 public | badges                        | table    | kbadmin
 public | badges_no_seq                 | sequence | kbadmin
 public | comments                      | table    | kbadmin
 public | comments_comment_no_seq       | sequence | kbadmin
 public | confirm_mail_changes          | table    | kbadmin
 public | draft_item_values             | table    | kbadmin
 public | draft_knowledges              | table    | kbadmin
 public | draft_knowledges_draft_id_seq | sequence | kbadmin
 public | events                        | table    | kbadmin
 public | functions                     | table    | kbadmin
 public | groups                        | table    | kbadmin
 public | groups_group_id_seq           | sequence | kbadmin
 public | hash_configs                  | table    | kbadmin
 public | item_choices                  | table    | kbadmin
 public | knowledge_edit_groups         | table    | kbadmin
 public | knowledge_edit_users          | table    | kbadmin
 public | knowledge_files               | table    | kbadmin
 public | knowledge_files_file_no_seq   | sequence | kbadmin
 public | knowledge_groups              | table    | kbadmin
 public | knowledge_histories           | table    | kbadmin
 public | knowledge_item_values         | table    | kbadmin
 public | knowledge_tags                | table    | kbadmin
 public | knowledge_users               | table    | kbadmin
 public | knowledges                    | table    | kbadmin
 public | knowledges_knowledge_id_seq   | sequence | kbadmin
 public | ldap_configs                  | table    | kbadmin
 public | like_comments                 | table    | kbadmin
 public | like_comments_no_seq          | sequence | kbadmin
 public | likes                         | table    | kbadmin
 public | likes_no_seq                  | sequence | kbadmin
 public | locales                       | table    | kbadmin
 public | login_histories               | table    | kbadmin
 public | mail_configs                  | table    | kbadmin
 public | mail_hook_conditions          | table    | kbadmin
 public | mail_hook_ignore_conditions   | table    | kbadmin
 public | mail_hooks                    | table    | kbadmin
 public | mail_hooks_hook_id_seq        | sequence | kbadmin
 public | mail_locale_templates         | table    | kbadmin
--More-- 

なんか大丈夫そうです。2, 3のテーブルについて select してみましたが中身が入っているので問題ないでしょう。

Knowledge→GROWI 移行 (1)

移行元データを探る

さて、

tiger62shin.hatenablog.com

にも書きましたが、個人で利用している情報共有サービスを Knowledge - Free knowledge base system から OSS開発wikiツールのGROWI に移行する話を進めていきます。

まずは、Knowledge のデータベースから移行元のデータ (本文と添付ファイル) を探っていきます。

Knowledge のデータベースは PostgreSQL なのでクライアントアプリケーションである psql を使って接続します。

$ psql -U postgres -d knowledge
psql (11.14 (Debian 11.14-0+deb10u1))
"help" でヘルプを表示します。

knowledge=>

テーブルの一覧を確認してみます

knowledge=> \dt;
                      リレーション一覧
 スキーマ |            名前             |    型    | 所有者
----------+-----------------------------+----------+---------
 public   | access_logs                 | テーブル | kbadmin
 public   | account_images              | テーブル | kbadmin
 public   | activities                  | テーブル | kbadmin
 public   | badges                      | テーブル | kbadmin
 public   | comments                    | テーブル | kbadmin
 public   | confirm_mail_changes        | テーブル | kbadmin
 public   | draft_item_values           | テーブル | kbadmin
 public   | draft_knowledges            | テーブル | kbadmin
 public   | events                      | テーブル | kbadmin
 public   | functions                   | テーブル | kbadmin
 public   | groups                      | テーブル | kbadmin
 public   | hash_configs                | テーブル | kbadmin
 public   | item_choices                | テーブル | kbadmin
 public   | knowledge_edit_groups       | テーブル | kbadmin
 public   | knowledge_edit_users        | テーブル | kbadmin
 public   | knowledge_files             | テーブル | kbadmin
 public   | knowledge_groups            | テーブル | kbadmin
 public   | knowledge_histories         | テーブル | kbadmin
 public   | knowledge_item_values       | テーブル | kbadmin
 public   | knowledge_tags              | テーブル | kbadmin
 public   | knowledge_users             | テーブル | kbadmin
 public   | knowledges                  | テーブル | kbadmin
 public   | ldap_configs                | テーブル | kbadmin
 public   | like_comments               | テーブル | kbadmin
 public   | likes                       | テーブル | kbadmin
 public   | locales                     | テーブル | kbadmin
 public   | login_histories             | テーブル | kbadmin
 public   | mail_configs                | テーブル | kbadmin
 public   | mail_hook_conditions        | テーブル | kbadmin
 public   | mail_hook_ignore_conditions | テーブル | kbadmin
 public   | mail_hooks                  | テーブル | kbadmin
 public   | mail_locale_templates       | テーブル | kbadmin
 public   | mail_posts                  | テーブル | kbadmin
 public   | mail_properties             | テーブル | kbadmin
 public   | mail_templates              | テーブル | kbadmin
 public   | mails                       | テーブル | kbadmin
 public   | notices                     | テーブル | kbadmin
 public   | notification_status         | テーブル | kbadmin
 public   | notifications               | テーブル | kbadmin
 public   | notify_configs              | テーブル | kbadmin
 public   | notify_queues               | テーブル | kbadmin
 public   | participants                | テーブル | kbadmin
 public   | password_resets             | テーブル | kbadmin
 public   | pins                        | テーブル | kbadmin
 public   | point_knowledge_histories   | テーブル | kbadmin
 public   | point_user_histories        | テーブル | kbadmin
 public   | provisional_registrations   | テーブル | kbadmin
 public   | proxy_configs               | テーブル | kbadmin
 public   | read_marks                  | テーブル | kbadmin
 public   | role_functions              | テーブル | kbadmin
 public   | roles                       | テーブル | kbadmin
 public   | service_configs             | テーブル | kbadmin
 public   | service_locale_configs      | テーブル | kbadmin
 public   | stock_knowledges            | テーブル | kbadmin
 public   | stocks                      | テーブル | kbadmin
 public   | survey_answers              | テーブル | kbadmin
 public   | survey_choices              | テーブル | kbadmin
 public   | survey_item_answers         | テーブル | kbadmin
 public   | survey_items                | テーブル | kbadmin
 public   | surveys                     | テーブル | kbadmin
 public   | system_attributes           | テーブル | kbadmin
 public   | system_configs              | テーブル | kbadmin
 public   | systems                     | テーブル | kbadmin
 public   | tags                        | テーブル | kbadmin
 public   | template_items              | テーブル | kbadmin
 public   | template_masters            | テーブル | kbadmin
 public   | tokens                      | テーブル | kbadmin
 public   | user_alias                  | テーブル | kbadmin
 public   | user_badges                 | テーブル | kbadmin
 public   | user_configs                | テーブル | kbadmin
 public   | user_groups                 | テーブル | kbadmin
 public   | user_notifications          | テーブル | kbadmin
 public   | user_roles                  | テーブル | kbadmin
 public   | users                       | テーブル | kbadmin
 public   | view_histories              | テーブル | kbadmin
 public   | votes                       | テーブル | kbadmin
 public   | webhook_configs             | テーブル | kbadmin
 public   | webhooks                    | テーブル | kbadmin
(78 行)

knowledge=>

たくさんありますね。この中から名前から想像してめぼしいものをピックアップします。

  • knowledge_files
  • knowledges
  • draft_knowledges

一つ一つ、確認していきます。knowledge_files はたぶん添付ファイルだと思うので、後回しにします。
knowledges のカラムを確認

knowledge=> \d knowledges;
                                                テーブル "public.knowledges"
       列        |             型              | 照合順序 | Null 値を許容 |                    デフォルト
-----------------+-----------------------------+----------+---------------+--------------------------------------------------
 knowledge_id    | bigint                      |          | not null      | nextval('knowledges_knowledge_id_seq'::regclass)
 title           | character varying(1024)     |          | not null      |
 content         | text                        |          |               |
 public_flag     | integer                     |          |               |
 tag_ids         | character varying(1024)     |          |               |
 tag_names       | text                        |          |               |
 like_count      | bigint                      |          |               |
 comment_count   | integer                     |          |               |
 view_count      | bigint                      |          |               |
 type_id         | integer                     |          |               |
 notify_status   | integer                     |          |               |
 point           | integer                     |          | not null      | 0
 anonymous       | integer                     |          |               |
 insert_user     | integer                     |          |               |
 insert_datetime | timestamp without time zone |          |               |
 update_user     | integer                     |          |               |
 update_datetime | timestamp without time zone |          |               |
 delete_flag     | integer                     |          |               |
インデックス:
    "knowledges_pkc" PRIMARY KEY, btree (knowledge_id)

knowledge=>

knowledge_id, title, content, tag_names あたりが怪しそうなので、下の画像の記事を確認してみます

f:id:tiger62shin:20220128170301p:plain

knowledge=> select knowledge_id, title, content, tag_names from knowledges where knowledge_id = 235;

f:id:tiger62shin:20220128170638p:plain

移行に必要なデータ「見出し」、「本文」、「タグ」はここから取り出せそうです。
実はこのことは後から気付いたのですが、delete_flag をたてて論理削除されているデータがあるようです。まず、delete_flag がどのような値を取るかを確認します。

knowledge=> select delete_flag, count(*) from knowledges group by delete_flag;
 delete_flag | count 
-------------+-------
           0 |   234
           1 |     5
(2 行)

予想通り '1' で削除されているようです。なので、移行に必要なデータを取り出す SQL

select knowledge_id, title, content, tag_names from knowledges where delete_flag = 0;

のようになりそうです。


draft_knowledges は下書きと思います。同じようにして確認していきます。

knowledge=> \d draft_knowledges;
                                              テーブル "public.draft_knowledges"
       列        |             型              | 照合順序 | Null 値を許容 |                     デフォルト
-----------------+-----------------------------+----------+---------------+----------------------------------------------------
 draft_id        | bigint                      |          | not null      | nextval('draft_knowledges_draft_id_seq'::regclass)
 knowledge_id    | bigint                      |          |               |
 title           | character varying(1024)     |          | not null      |
 content         | text                        |          |               |
 public_flag     | integer                     |          |               |
 accesses        | text                        |          |               |
 editors         | text                        |          |               |
 tag_names       | text                        |          |               |
 type_id         | integer                     |          |               |
 insert_user     | integer                     |          |               |
 insert_datetime | timestamp without time zone |          |               |
 update_user     | integer                     |          |               |
 update_datetime | timestamp without time zone |          |               |
 delete_flag     | integer                     |          |               |
インデックス:
    "draft_knowledges_pkc" PRIMARY KEY, btree (draft_id)

knowledge=>

knowledges と同じように draft_id, title, content, tag_names があるのでここから取り出せそうです。下書きの ID は別になっていることに加え knowledge_id というカラムがあります。内容を確認してみます。

knowledge=> select knowledge_id from draft_knowledges;
 knowledge_id
--------------
            0
            0
            0
            0
            0
            0
            0
            0
            0
            0
            0




          230
(16 行)

たいていは 0 か null みたいですが、1 つだけ値が入ってます。draft_knowledges.knowledge_id の値で knowledges の方を確認してみます。

knowledge=> select knowledge_id, title, content, tag_names from knowledges where knowledge_id = 230;

つまり、「公開されている記事の下書き」の公開されている記事の ID が入っているようです。1 つしかないし、内容も忘れているのでこの下書きの記事は移行しないことにしました
このような SQL で下書きのデータは取り出せそうです。knowledges と同様に論理削除されているデータは除外しています。

knowledge=> select draft_id, title, content, tag_names from draft_knowledges where (knowledge_id is null or knowledge_id not in (select knowledge_id from knowledges)) and delete_flag = 0;


次は添付ファイルです
同様にカラムを確認します

knowledge=> \d knowledge_files;
                                              テーブル "public.knowledge_files"
       列        |             型              | 照合順序 | Null 値を許容 |                    デフォルト                    
-----------------+-----------------------------+----------+---------------+--------------------------------------------------
 file_no         | bigint                      |          | not null      | nextval('knowledge_files_file_no_seq'::regclass)
 knowledge_id    | bigint                      |          |               | 
 comment_no      | bigint                      |          |               | 
 draft_id        | bigint                      |          |               | 
 file_name       | character varying(256)      |          |               | 
 file_size       | double precision            |          |               | 
 file_binary     | bytea                       |          |               | 
 parse_status    | integer                     |          | not null      | 
 insert_user     | integer                     |          |               | 
 insert_datetime | timestamp without time zone |          |               | 
 update_user     | integer                     |          |               | 
 update_datetime | timestamp without time zone |          |               | 
 delete_flag     | integer                     |          |               | 
インデックス:
    "knowledge_files_pkc" PRIMARY KEY, btree (file_no)
    "idx_knowledge_files_knowledge_id" btree (knowledge_id)

knowledge=> 

knowledge_id または draft_id 毎に file_no で識別されるファイルが格納されているようです。file_name にファイル名、file_size にファイルのサイズ、file_binary にファイル本体があるようです。
試しに SQL だけでファイルを取りだしてみようとして、Google で検索した結果、下記のサイトを見つけました

kenpg.bitbucket.io

COPY コマンドでバイナリデータをファイルに出力することはできるが、ファイルの先頭に 25 byte 分のヘッダがついているようです。

copy (select file_binary from knowledge_files where file_name = 'clip-20210606104110.png') to '/var/lib/postgresql/clip-20210606104110.png' (format binary);

Mac で使用できるバイナリエディタを持っていなかったので Windows の Stirling というバイナリエディタを使ってファイルの先頭 25 byte を削ってみました。

f:id:tiger62shin:20220128214336p:plain

Postfix をインストールしたときのスクリーンショットのようです。
これで添付ファイルも取り出せることがわかりました。

本文と添付ファイルとの関係も確認しておこうと思います。上の添付ファイルは下図の記事の赤枠で囲った部分の添付ファイルと思います

f:id:tiger62shin:20220128224211p:plain

knowledge=> select file_no, knowledge_id, file_name from knowledge_files where file_name = 'clip-20210606104110.png';
 file_no | knowledge_id |        file_name
---------+--------------+-------------------------
     556 |          236 | clip-20210606104110.png
(1 行)

knowledge=>

#236 の記事に添付されている 556 番のファイルということで間違いなさそうです。実際の記事はこんな感じです。

f:id:tiger62shin:20220128224717p:plain


次は、これらの SQLPython プログラムから実行するサンプルを作ります。

開発環境

私が仕事を始めた頃のプログラマーは「紙と鉛筆」があれば仕事ができる時代でしたが (コーディングもコーディングシートに手書きしていた)、今時「紙と鉛筆」でのプログラミングなんてありえないのでパソコンや開発ツール等の道具が必要です。知識だけなら本を読むだけでもなんとかなるとは思いますが、身につかないと思いますし、やはり作ったものは動かしてみたいと思うのが人情でしょう

で、私の開発環境です。

パソコン

MacBook Pro を使っています。 2 年ほど前に整備済品を購入しました。Intel Mac です。整備済品なので特にカスタマイズもしてませんがメモリだけは沢山積んである (32GB) のを選んで購入しました。

ディスプレイは 13 インチで老眼が進んだ身としては少々つらいのですが、フットプリントが小さいほうがよかったのと、ディスプレイは外部ディスプレイをつなげればよいと思ったので 13 インチにしました。

仕事では、Let's Note だったり、ThinkPad だったりするので操作方法の違いになれないこともありますが、仕事と趣味で異なる種類のパソコンを使うことによる気持ちと頭の切り替えでできてよいような気がします。

開発環境とは無関係なのですが、実は整備済品とはいえ新品の Mac を買うのはこれが 4 台目です。最初に買ったのは 30 年位前に買った Macintosh LC です。

ja.wikipedia.org

私が初めて買ったパソコンです。その後、Macintosh LC630 に買い換えました。

ja.wikipedia.org

LC は人に譲ってしまいましたが、LC630 はまだ持ってます。インターネットに初めて接続したのも LC630 からでした。

LC630 以降は DynabookThinkPad だったり、自作デスクトップだったりで、MacBook Pro を買うまでは Windows PC がメインでした。

私自身が使った Mac はこんな感じですが、息子用に iMac DV を買っています。

ja.wikipedia.org

この iMac DV は、ロジックボードだけ形を変えてまだ持っています。iMac DV のロジックボードを PC/AT 用の電源で動かすようにして、確か、Maintosh IIcx の筐体に入れる改造記事に触発されて真似して作ったものです。私は IIci の筐体に入れました。

ブックマークに登録しておいた改造記事のサイトにアクセスしてみましたが、もうどこも残ってないようです。が、Internet Archive に少し残ってました。

このサイトでは iMac を Sun SPARC station IPX の筐体に入れています。

web.archive.org

IDE

Visual Studio Code です。これ以上の説明はいらないと思いますが、以下の機能拡張を導入しています。

正直、なんで入っているのかわからないものもあるのですが、とりあえず現在入っているのが上記のものです。

Remote - XXX が入っているのは Remote Development Extension Pack でまとめて導入されたものです。目的は Docker コンテナを使いたかったので、Remote - Containers が入っていればよかったはずです。

ファイルアイコンとかも綺麗に表示されていた方が嬉しいのでいくつか入れました。

Visual Studio Code の設定は未だ試行錯誤中ですが、主な設定を記載します。

まずは色関係

 "workbench.colorCustomizations": {
     "editor.background": "#1E1E1E",
     "editor.lineHighlightBorder": "#1E1E1E",
     "editorLineNumber.activeForeground": "#ffffff",
 },

背景色を黒にしているのとカレント行の行番号の色を白にしています。工夫したのは、editor.lineHighlightBorder を背景色と同じ色に設定してカレント行の枠線が表示されないようにしたところです。

Python 固有に下記のようなものを設定しています。

 "[python]": {
     "editor.fontFamily": "SourceHanCodeJP-Regular",
     "editor.fontSize": 12,
     "editor.insertSpaces": true,
     "editor.tabSize": 4,
     "editor.rulers": [128],
     "editor.renderIndentGuides": false,
     "files.insertFinalNewline": true,
     "files.trimTrailingWhitespace": true,
 },

Python 固有にしたのは他の言語もやってみたいので設定を別にしたかったからです。

あと、エクスプローラーのフォントサイズが大きすぎたので

 "window.zoomLevel": -0.7,

で、全体のズームレベルを下げて、エディタ部分は editor.fontSize で個別に設定しています。

他にも色々設定していますが、Python のコーディングに関係しているのはこんなところです。

こんな感じの見た目になっています。 f:id:tiger62shin:20220125213345p:plain

Python 実行環境

PythonPython そのものやライブラリのバージョン依存が厳しい (バージョンを適切に管理する必要がある) ので Anaconda 等でバージョン管理を行うと聞いていたので、なんとなく「面倒だなー」と思っていました。

Mac の環境に色々な Python のバージョンやライブラリを入れたくなかったのと、間違えたときに元に戻したり、クリーンにしたりするのが面倒なんじゃないかとも思っていました。

VM Ware や ViurtualBox 等の仮想環境も考えましたが、なんとなく大げさすぎる気もしていました。

このように色々と考えているうちに以前「Software Design」という雑誌で Docker の記事を読んで、とりあえずコンテナを動かしてみたことを思い出し、これが使えるんじゃないかと思い (この段階では Docker コンテナの中で Visual Studio Code 動かせないかなーと思ってました) 色々と調べたところ Visual Studio Code から Docker コンテナに接続して開発ができることがわかりました。

Visual Studio Code で Docker コンテナを利用する方法については Google で「vs code docker コンテナ」等のキーワードで検索するとさまざまなサイトがヒットするので、そのようなサイトを参照しながら設定しました。

ポイントは

  • Dockerfile をマニュアルで作成
  • 複数のコンテナを使いたいので、docker compose を使う

ところです

私が現時点で行き着いている手順です

  1. プロジェクトフォルダ直下に .devcontainer フォルダを作成
  2. 特定のコンテナ用のフォルダ (Dockerfile などを格納) を作成

    .devcontainer/python
    
  3. .devcontainer/python/Dockerfile を作成
    いらんもんも入っていますが、だいたいこんな感じです

    FROM python:3.8
    USER root
    
    RUN apt-get update
    RUN apt-get -y install locales && \
        localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
    
    ENV LANG ja_JP.UTF-8
    ENV LANGUAGE ja_JP:ja
    ENV LC_ALL ja_JP.UTF-8
    ENV TZ JST-9
    ENV TERM xterm
    
    ADD . /code
    WORKDIR /code
    RUN apt-get install -y vim less x11-xserver-utils python3-tk graphviz
    RUN pip install --upgrade pip
    RUN pip install --upgrade setuptools
    RUN pip install -r requirements.txt
    
  4. Dockerfile からは requirement.txt に記載されているパッケージをインストールするようになっているので .devcontainer/python/requirement.txt を作成。今はこんな感じ

    autopep8
    flake8
    psycopg2
    requests
    
  5. .devcontainer/docker-compose.yml を作成

    version: '3'
    services:
      python38:
        restart: always
        build: python
        container_name: 'python38'
        working_dir: '/workspace'
        tty: true
        volumes:
          - ..:/workspace:cached
    
  6. .devcontainer/devcontainer.json を作成

    {
      "name": "Tools by Python",
      "dockerComposeFile": "docker-compose.yml",
      "service": "python38",
      "workspaceFolder": "/workspace",
      "extensions": [
          "MS-CEINTL.vscode-language-pack-ja",
          "ms-python.python",
          "ms-toolsai.jupyter"
      ],
      "settings": {
        "python.formatting.autopep8Args": [
            "--max-line-length",
            "128"
        ],
        "python.linting.pylintEnabled": false,
        "python.linting.flake8Enabled": true,
        "python.linting.flake8Args": [
          "--max-line-length=128"
        ],
        "python.autoComplete.extraPaths": [
          "/usr/local/lib/python3.8/site-packages/"
        ],
      },
      "postCreateCommand": "",
    }
    

    特別な設定は行っていませんが、autopep8, flake8 を pip でインストールしているのでその設定を行なっています。ちょっと行儀の悪い設定です。


これで、Viaual Studio Code でプロジェクトフォルダを開くと Docker コンテナが開始して Python が実行できるようになりました。

自分自身のこと

ブログのタイトルに「ほぼ老人」とか「定年が近い」とか書いていますが、実際には今度の 3 月で 60 歳になり定年退職する会社員です。なので、「ほぼ」とか「近い」とかは削って「老人のプログラミング日記」でもよかったのですが、「老人」という言葉に抵抗があって「ほぼ老人」にしました。「定年が近い」部分は実際に定年退職したら書き換えようと思ってます。

一旦は定年退職するのですが、65 歳までは定年後再雇用の嘱託社員として仕事は続けるつもりです。

職種はプログラマーです。自分の実力を計ったことはありませんが、そんなに優秀じゃないと思います。現在、主に使用している言語は C# です。数年前までは Java (バージョンは 8) を主に使っていたのですが、客先が変わって C# になりました。

C# は 12, 3 年前に「C#エッセンシャルズ」という本を買って読んだ程度ですが、.NET Framework はバージョン 2.0 の頃に VB.NET で自社内のシステムを開発したので、まったくなじみがないわけではありませんでした。とはいえ、10 年以上も前のことなので不安はありましたが「まぁ、なんとかなるだろー」という感じで着任して、結果として何とかなってます。とりあえず本は「Effective C# 6.0/7.0」と「More Effective C# 6.0/7.0」を読みました。

使用言語が C# にかわって、特別な感想はないのですが、C# の方が LINQ とかあってらくちんなきがします。決して私が Java の機能を使いこなしていたわけではないので、個人的な感覚です。

 

職業はプログラマーなので、日常的にプログラミングは行っているのですが、仕事以外でプログラミングすることは就職以来、約 38 年の間ありませんでした。それが、2~3 カ月前から突然、趣味でもプログラミングを行うようになりました。きっかけを箇条書きにすると

  • 個人的に Knowledge - Free knowledge base system という情報共有サービスを使っていた
  • 目的はなかったが VPS を借りた (WebARENA)
  • 知人に OSS開発wikiツールのGROWI という情報共有ツールを教えてもらったので VPS に導入してみた
  • Knowledge はやめて GROWI に移行することにした
  • Knowledge の記事 (240 件程度) を GROWI に移行するのにツールを作る必要に迫られた。最初は Knowledge の記事をエクスポート / 加工して GROWI にインポートできるんじゃないかと考えたが Knowledge がエクスポートでコケるのでこの方法はあきらめた。240 件程度なのでマニュアルで移行することも考えたが、職業プログラマーとして「それはないんじゃないか」と思いツールを作ることにした
  • Python が流行ってるらしいので、Python でツールを作ったが、仕事とは異なる面白さがあり、今後も「趣味」としてプログラミングを行っていこうと考えた

プログラミングをしていると、色々と調べたり、勉強したりすることがありますが、たいていは「コーディングして終わり」というのが多いと思います。仕事なら成果として残りますが、趣味だと使い捨てだったり、ちょっと調べてみただけで記録が残らないと思いましたので、老後の趣味の一環としてプログラミング関係のブログも始めることにしました。

 

家庭や仕事もあるので 1 日にプログラミングできるのは 1~2 時間程度で、毎日できるわけでもないので、このブログも頻繁に更新はできないと思いますが、ボチボチでも続けていけたらと思っています。