Kuzunoha-NEのブログ

pythonの勉強中。

【Discord】Botから役職へのメンション方法

こんばんは、葛の葉です。

ちょっと前に会社でDiscord使ってる旨お伝えしたかと存じますけど、そんな中でDiscord内の役割(権限)にメンションを飛ばしたくなりました。それが意外と日本語に乗っていなかったのでこちらに記載します。

役職にメンション飛ばすんだって

実は以下に記載の通り。でも、最初見たときよくわからなかったのでざっくりと説明すると役割に一回特殊なメンション飛ばすと、その飛ばしたときの書き込みがちょっと変わった文字<@&????????????>みたいなものになるので、Botを使うときはその<@&????????????>を使うようにします。

discordhelp.net

やってみた

まず役割の設定をします。サーバーを右クリックしてサーバー設定→役割をクリック。

f:id:Kuzunoha-NE:20190306184544j:plain

飛ばしたい役職を選択してこの役職に対して@メンションを許可するをオンにして保存

f:id:Kuzunoha-NE:20190306184851j:plain

どこでもいいので\@<役職名>と送信する。今回は\@ハンプティダンプティそうすると送信された文字がちょっと変わった物になっている。

f:id:Kuzunoha-NE:20190306185244j:plain

<@$????????????????>みたいなものが出ていると思うので、これをそのまま書き込めばその役職へのメンションになる。

botなんかに

botなんかで役職に送信したい場合は<@$????????????????>を文字列に付け加えてあげればOKなはず。

【GAS】Googleフォームで入力してもらった値をLogger.logに出力する

こんばんは、葛の葉です。

さて、Googleフォームという大変便利がいいWebアプリがありますね。

www.google.com

これとGASを連携し、GAS上のLogger.logに出力することが出来ます。

こんなGoogleフォームです

f:id:Kuzunoha-NE:20190214115619j:plain

問い合わせ内容の中身をGASで取得するようにします。

スクリプトエディタを開く

先ずはスクリプトエディタを開きます。開き方はGoogleフォームのエディタモードから右上のその他のボタン(黒点が縦に三つ並んでいるボタン)をクリックして、出てきたリストからスクリプト エディタをクリックしてください。

f:id:Kuzunoha-NE:20190214120204j:plain

そうしたらスクリプトエディタ画面に移行するはずです。下記のようなコードが記載されていると思います。

function myFunction() {
  
}

コードの記入

以下のように記入し、保存してください。(Ctrl + s)など。

function myFunction(event){
  var values = event.response.getItemResponses();
  var value = values[0].getResponse();
  Logger.log(value)
}

初めて保存する際はプロジェクトの名前を問われますので、お好みで。

トリガーの設定

スクリプトエディタ画面で「現在のプロジェクトのトリガー」ボタン(時計の形をしたボタン)があります。

f:id:Kuzunoha-NE:20190214122619j:plain

それをクリックしてください。そうしますと、G Suite Developper Hubというページに飛びます。右下の「トリガーを追加」ボタンをクリックします。

f:id:Kuzunoha-NE:20190214122702j:plain

実行する関数を選択myFunctionになっていることを確認した後、下部にスクロールした後イベントの種類を選択フォーム送信時に変更してください。

f:id:Kuzunoha-NE:20190214122841j:plain

保存をすればGoogleアカウントのことを聞かれますので、そこの対応をお願いします。

問題なければここで製作は完了しています。

実際にフォームに入れてみる。

f:id:Kuzunoha-NE:20190214121658j:plain

こんな内容のものを送信します。

スクリプトエディタに移動して表示-> ログとしてみてください。

f:id:Kuzunoha-NE:20190214121900j:plain

このようにログに出力されています。

Loggerlogの部分を変えてみましょう

例えば以前作成したDiscordに送信するための関数に渡してもよいでしょう。その他のチャットツールとかでも使えるんじゃないかなぁって思います。

kuzunoha-ne.hateblo.jp

kuzunoha-ne.hateblo.jp

【Python】受け取った配列に対し、Keyが1,2,3,4と連番した数字になるような辞書を返す関数 + Unittest

こんばんは、葛の葉です。

引数に配列を渡すと、連番をKeyにした辞書型を返してくれる関数を作りました。必要かどうかわかんないけど、一時、入用だったのですが、多分もういらない関数なのでここで供養させてください。

こんなんなのよ

関数numbering_dictionary()の引数に['nezumi', 'ushi', 'tora', 'usagi']のような配列を渡すと{1: 'nezumi', 2: 'ushi', 3: 'tora', 4: 'usagi'}という辞書を返します。だからなんだってレベルのものですな。

関数はこんな感じ

helper_def.pyという名前で以下のコードを保存してください。

def numbering_dictionary(arry_choices_answer, start_key=1):
    '''
    受け取った配列に対し、Keyが1,2,3,4と連番した数字になるような辞書を返します。
    ex: numbering_dictionary(['nezumi','ushi','tora'])
        -> {1: 'nemuzi', 2: 'ushi', 3: 'tora'}
    start_key(int)は連番開始の数字になります。デフォルトは1。
    '''
    numbring_dictionary = {}
    for index_number, choices_answer in enumerate(arry_choices_answer, start=start_key):
        numbring_dictionary[index_number] = choices_answer
    return numbring_dictionary

Unittestはこんな感じ

Unittest用プログラミングは以下になります。test_helper_def.pyみたいな名前でhelper_def.pyと同じところに保存してください。なお、動作させるだけならこのファイルは要らないです。

import pytest

from . import helper_def

def test_numbering_dictionary():
    array1 = ['nezumi', 'ushi', 'tora', 'usagi']
    array2 = ['serval', 'araisan', 'vulpes_zerda', 'silverfox']
    array3 = ['one', 'two', 'three', 'four']
    assert {1: 'nezumi', 2: 'ushi', 3: 'tora',
            4: 'usagi'} == helper_def.numbering_dictionary(array1)
    assert {1: 'serval', 2: 'araisan', 3: 'vulpes_zerda',
            4: 'silverfox'} == helper_def.numbering_dictionary(array2)
    assert {2: 'one', 3: 'two', 4: 'three',
            5: 'four'} == helper_def.numbering_dictionary(array3, start_key=2)

【kubernetes】SecretとRedisのパスワード設定

こんばんは、葛の葉です。

さて、今回はkubernetesでRedisをデプロイする際のパスワードの設定とSecretの設定方法を記載します。

環境

minikube ver 0.31.0

Redis(image) tag 5.0.3

簡単なRedisのパスワード設定のおさらい

Redisがインストールされている環境であれば、以下のコマンドでパスワードを設定できます。

redis-server --requirepass [password]

コマンドを打つとRedisが起動して、以下のような感じでメッセージが出力されると思います。今回のパスワードは1212です。

root@657cb0734634:/data# redis-server --requirepass 1212
15:C 20 Feb 2019 04:05:48.917 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
15:C 20 Feb 2019 04:05:48.918 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=15, just started
15:C 20 Feb 2019 04:05:48.918 # Configuration loaded
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 5.0.3 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 15
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           http://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

15:M 20 Feb 2019 04:05:48.919 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
15:M 20 Feb 2019 04:05:48.920 # Server initialized
15:M 20 Feb 2019 04:05:48.920 * DB loaded from disk: 0.000 seconds
15:M 20 Feb 2019 04:05:48.920 * Ready to accept connections

パスワードが設定されれば、そのRedisに接続したあと、操作を行うにはパスワードが必要になります。例えばredis-cliで接続した際は、auth [password]といった形で認証を得ないといけません。

root@657cb0734634:/data# redis-cli -h 192.168.99.100 -p 6379 
192.168.99.100:6379 > auth 1111
ERR invalid password
192.168.99.100:6379 > auth 1212
OK

ERR invalid passwordと出るとパスワードが間違っています。OKが出てれば接続できます。ここまでがRedisでパスワードを設定する方法とそのRedisに繋げる方法です。

redis.io

KubernetesのSecret

さて、KubernetesのSecretですが、詳細は以下のリンクを見てもらえればと思います。

kubernetes.io

KubernetesではこのSecretというオブジェクトを使ってパスワードを管理したほうが良いようです。先ほどの1212というパスワードを管理するSecretを作成します。

ファイル名test-secret.yaml

apiVersion: v1
kind: Secret
metadata:
    name: redis-password # secretの名称となり、k8s上で使用する名前
type: Opaque
data:
    RedisPassword: MTIxMg== #base64でないと受け付けません。

metadatanameを使って、このSecretをredis-password命名し、Kubernetes内ではこの名前でこのSecretを呼び出します。また、data内にあるRedisPasswordMTIxMg==はKeyとValueになっています。MTIxMg==base641212になります。Keyは複数作ることが出来ますので、一つのSecretに複数のパスワード等を含めることが出来ます。例えばusernamekeyとpasswordkeyを入れるなんてことも出来ます。

kubectl apply -f test-secret.yamlとすれば設定が反映されます。

kubectl get secretとすればSecretが表示されます。

NAME                       TYPE                                  DATA      AGE
default-token-57h4x        kubernetes.io/service-account-token   3         5d
redis-password             Opaque                                1         28m

default-token-57h4xはminikubeのものかなぁと。

Podの設定はこんな感じ

次にKubernetes上でRedisの入ったPodを作成します。今回はSecretの設定はPodで行いますが、DeployMentやStatefulSetでも設定可能です。

ファイル名test-pod.yaml

apiVersion: v1  
kind: Pod
metadata:
  name: test-redis
  labels:
    app: test-redis #ここの値を用いてServiceと連携する
spec:
  containers:
  - name: test-redis-ctr
    image: redis:5.0.3
    env:
    - name: REDIS_PASSWORD # Pod内に挿入する環境変数の名前
      valueFrom:
        secretKeyRef: 
          name: redis-password # 先ほど作成したSecret名
          key: RedisPassword #  先ほど作成したKey名。Valueが環境変数内に代入される。
    command: ["redis-server"]
    args: ["--requirepass $(REDIS_PASSWORD)"]
    ports:
    - containerPort: 6379

yaml内のspec以下にcontainers項目があって、これらが生成するコンテナの詳細に設定する部分になります。-nameはコンテナの名前です。imageはそのPodを作成するに使用するDockerimageを記します。

また、envがあって、これがPod内の環境変数を作成する項目になります。env-name環境変数名です。 valueFromはその環境変数の値を引っ張ってくる設定ファイルがどの形式であるかというもので、Secretを使用する場合はsecretKeyRefとします。configMapKeyRefだとConfigMapというオブジェクトを使用するようになります。nameはSecretの名前を、keyはSecret内dataで記入したkeyであるRedisPasswordにします。

commandはPod生成時に実行されるコマンドで、redis-serverというコマンドを、argsで引数を渡しています。--requirepassは先に説明したとおりで、$(REDIS_PASSWORD)環境変数部分になります。ブランケットの括弧${}ではなくパレンティスの括弧$()なので注意してください。

kubectl apply -f test-pod.yamlで作成しましょう。

kubectl get podsで出来たPodを見られます。

NAME                              READY     STATUS      RESTARTS   AGE
test-redis                        1/1       Running     0          34m

なお、STATUSがErrImagePullといったものであった場合は、Redis:5.0.3のイメージが存在していないので作成できていないというエラーになります。minikubeでしたらdocker pull redis:5.0.3とかやってみてください。

Serviceを使ってみる(minikube)

次にServiceを使って先のPodを確認出来るようにします。

ファイル名test-svc.yaml

apiVersion: v1 # ここも固定。kubernetes側が対応したらVの値を増やす形に
kind: Service metadata:
  name: test-service # Serviceそのものの名前。アンスコが使えない等ルールあり
spec:
  type: NodePort  
  selector:
    app: test-redis 
  ports:
  - port: 6379 
    targetPort: 6379 

Serviceの説明は省きます。kubectl apply -f test-svc.yamlで作成し、kubectl get service (又は"svc")と打って確認しましょう。

NAME                    TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
test-service            NodePort       10.111.68.208   <none>        6379:30271/TCP   38m

CLUSTER-IPPORT(S)のコロン右側の値はそれぞれ異なると思います。

IPアドレスとポート番号を確認する

GKEと違って、minikubeではNodePortタイプ(LoadBalancerタイプも同様)のServiceにEXTERNAL-IPは振られないようです。代わりに、minikubeの下記のコマンドでIPアドレスを確認する必要があります。

minikube service [Service名] --url

kubernetes.io

今回はminikube service test-service --urlです。そうしますと、http://192.168.99.100:30271といった表示が出てくると思います。IPアドレス部分、Port番号部分はそれぞれ異なると思います。

C:\>minikube service test-service --url
http://192.168.99.100:30271

これが今回生成したServiceにアクセスするためのIPアドレスとPort番号になります。

redis-cliでアクセス

下記コマンドを打ちます。

redis-cli -h [IPアドレス] -p [Port番号]

root@kuzunohasan:# redis-cli -h 192.168.99.100 -p 30271
192.168.99.100:30271> get 1
(error) NOAUTH Authentication required.
192.168.99.100:30271> auth 1212
OK
192.168.99.100:30271> get 1
(nil)

最初のget 1(error) NOAUTH Authentication required.と弾かれていますが、auth 1212とした後、get 1とした場合は(nil)とされています。無事成功しています。

綺麗にする

kubectl apply -f ***.yamlで設定したものはkubectl delete -f ***.yamlで削除可能です。

また、applyでもそうですが、ディレクトリを選択すれば、ディレクトリ内のyamlファイル全てを読みに行き、全てを削除してくれます。

C:\>kubectl delete -f test-secret.yaml
secret "redis-password" deleted

C:\>kubectl delete -f test-pod.yaml
pod "test-redis" deleted

C:\>kubectl delete -f test-svc.yaml
service "test-service" deleted

なんでRedisConfでパスワードの設定しないの?

今回のRedisのパスワードの設定はサーバー実行コマンドのオプションを使っていて、confを使った設定は行っていません。なぜならconf内で環境変数を呼び出して取得する方法がなかったからです…シェルスクリプトを使ったらよいかもしれませんね。

github.com

【Python】 値が文字列の中に含まれているか確認する。

こんばんは、葛の葉です。

さて、標題の件になりますが、ちょっと言葉だと説明しにくいかなと思います。例を挙げると

I Have a Dream.

Wikipediaより引用

ja.wikipedia.org

この中に特定の文字が入っていないかをBoolで返してもらう方法を書きます。

inを使う

例えばaという単語が入っているかを確認するには以下のようにします。

 'a' in 'I Have a Dream'

以下がコマンドラインPythonを実行したときの結果です。

>>> 'a' in 'I Have a Dream'
True
>>> 'b' in 'I Have a Dream'
False

Boolで返るのでassertも使えます。

>>> assert 'a' in 'I Have a Dream'
>>> assert 'b' in 'I Have a Dream'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

assert 'a' in 'I Have a Dream'は特に問題がないためTrueとなり、エラーなどは発生していません。一方、assert 'b' in 'I Have a Dream'はFalseとなるため、AssertionErrorとなっています。

listに入った複数の文字列が該当の文字列内に含まれているかの確認

例えば以下のように、IHaveaの文字全てが該当の文字列に含まれているかを確認したいとします。しかし、下記のようにリストをinとしてもTypeErrorが返されます。

>>> ['I', 'Have', 'a'] in 'I Have a Dream'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'in <string>' requires string as left operand, not list

リスト内包表記を使う

というわけでリスト内包表記を使いましょう。

False not in [i in 文字列 for i in 検索したい文字列の入ったリスト]

コマンドラインでは以下のようになります。

>>> False not in [i in 'I Have a Dream' for i in ['I', 'Have', 'a']]
True

分解するとFalse not in [リスト][i in 'I Have a Dream' for i in ['I', 'Have', 'a']]になると思います。

False not in [リスト]は最初にお話した'a' in 'I Have a Dream'の応用で、[list]の中にFalseが無ければTrueとなります。すなわち、リスト内が全てTrueだとTrueを返し、一つでもFalseがあればFalseを返します。ヤヤコシイネ。

>>> False not in [True,True,True,True]
True
>>> False not in [True,True,True,False]
False
>>> False not in [False,False,False,False]
False

さて、[i in 'I Have a Dream' for i in ['I', 'Have', 'a']]ですが、こちらは下部と同じ結果になります。

return_list = []
for i in ['I', 'Have', 'a']:
    result = i in 'I Have a Dream'
    return_list.append(result)

return_lisrt
[True, True, True]

結果は[True, True, True]となります。

すなわち、for文を用いて、含まれているか確認したい文字列が入ったリストの要素を一つずつとりだし、それぞれinを用いてBool値を返してもらい、それをリストの中に格納しています。

リスト内包表記での結果はこちら。

[i in 'I Have a Dream' for i in ['I', 'Have', 'a']]
[True, True, True]

含まれていない文字があればFalseがリストに返されます。

>>> [i in 'I Have a Dream' for i in ['I', 'king', 'a']]
[True, False, True] # kingの文字は存在しない

上記のFalse not inと組み合わせると

>>> False not in [i in 'I Have a Dream' for i in ['I', 'Have', 'a']]
True
>>> False not in [i in 'I Have a Dream' for i in ['I', 'king', 'a']]
False

と、このようになるわけです。

assertではこうです。

>>> assert False not in [i in 'I Have a Dream' for i in ['I', 'Have', 'a']]
>>> assert False not in [i in 'I Have a Dream' for i in ['I', 'king', 'a']]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

関数はこちら

def bool_check_words_in_word(check_words: list, the_word: str):
    # check_wordsには確認したい文字が含まれているリストを入れてください。
    # the_wordにはチェックしたい文字列を入れてください。
    return False not in [check_word in the_word for check_word in check_words]
print(
    bool_check_words_in_word(
        check_words=['I', 'Have', 'a'],
        the_word='I Have a Dream')
)

True

【Docker】docker-compose "exec" について

こんばんは、葛の葉です。

さて、DockerComposeを使ってコンテナを立ち上げたり、あるいはDockerNetworkを構築したりすることがあると思います。DockerComposeで立ち上げたコンテナの中に侵入するためにdocker exec -it コンテナ名 /bin/bashと打つ方もいらっしゃるかと思いますが、docker-compose.ymlにコンテナ名を確認しに行ったり、docker ps -a等で動いている(あるいは死んでる)コンテナを見に行ったりしてコンテナ名を確認したりしているのかなと思慮します。

前置きが長くなってしまいましたが、今回は上記よりもう少し簡単にDockerComposeで起動したコンテナ内に侵入できるdocker-compose execについてお話します。

docker-composeの記載内容の例

version: "3"
services:
    py:
        build: .
        image: python:3.6.5
        container_name: test_python
        volumes:
          - "./program/:/var/program/"
        tty: true
        networks:
            test_net:
                ipv4_address: 172.20.0.2
    rdy:
        image: redis:4.0.10
        container_name: test_redis
        volumes:
          - "./data:/data"
        networks:
            test_net:
                ipv4_address: 172.20.0.3
networks:
    test_net:
        driver: bridge
        ipam:
            driver: default 
            config: 
            - subnet: 172.20.0.0/24 

上記のようなdocker-compose.ymlがあったとします。今回の方法ではサービスを用います。DockerComposeにおけるサービスとはコンテナにオプションを施したものの単位という認識でよいと思います。今回、docker-compose.ymlに記載されているサービスはpyrdyとになります。pyというサービスはpython:3.6.5のイメージを使ってコンテナを作成し、volumesというオプションを使ってホストとゲスト間のディレクトリを共有しています。

まず、docker-compose.ymlがあるディレクトリ上でdocker-compose up -dコマンドを実行し、2つのサービスをデタッチドモード起動しましょう。2つのサービスの起動が完了するまで待ちます。

上記のサービスに侵入するには以下のコマンドを使用します。

docker-compose exec (service名) /bin/bash

今回のケースだとdocker-compose exec py /bin/bashになります。

root@50947e77690d:/#

こんな感じで出力されたかと思います。50947e77690dの部分は人やタイミングによって変わるかな、と思います。

docker-compose exec (service名) (command)

例えばdocker-compose exec py pythonとすることで、Pythonコマンドラインを出力することできます。

Python 3.6.5 (default, Jun 27 2018, 08:15:56)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.

今回は起動したサービスの元となっているImageがpython:3.6.5であり、pythonがインストールされているイメージであるため、上記のコマンドラインが出現しています。

では、もう一つのサービスであるrdyのコンテナに入ってみましょう。こちらはRedis:4.0.1がインストールされているサービスなります。

> docker-compose exec rdy redis-cli set testkey hoge
OK
> docker-compose exec rdy redis-cli get testkey
"hoge"

docker-compose exec rdy redis-cli set testkey hogeを分解すると

docker-compose exec はDockerComposeのコマンド

rdydocker-compose.ymlで作成したサービスの名前

redis-cli set testkey hogeはサービスに実行してもらうコマンド部分になります。

余談ですが、redis-cliコマンドはRedis用のコマンドです。set (KEY) (VALUE)とすると、KEYに値をセットでき、get (KEY)とするとKEYにセットされた値を出力しています。

今回はtestkeyというKEYにhogeというVALUEを与えています。

/bin/bashもコマンドです

ご存知の方が多いと思いますが、/bin/bashbashというshellを呼び出すコマンドです。docker-compose exec (service名) /bin/bashとは、サービス内のbashを呼び出しているので、サービス内に侵入できているわけです。

【Python】byte型だけが入ったlistを全てstr型に変換したlistとして返す + アノテーションを少し

こんばんは、葛の葉です。

さて、私は最近、Redisを使っているのですが、Pythonを通して取得した場合、byte型で受け取ることになると思います。Listとして受け取った場合、List内全てがbyte型になってしまいます。今回はbyteが入ったリストを一括で変化するための関数を作りました。

環境

Python 3.6.5

コード

def byte_to_str(byte_list: list) -> list:
    return [i.decode('utf8') for i in byte_list]

少し変わった書き方が見えると思います。引数(byte_list: list) -> list: list-> listアノテーションと言って、注釈の意味になります。(byte_list: list)の個所のアノテーションは「引数に入れるのはリストで入れてください」と注釈しています。また、 -> listの個所は返り値の型がlistであることを注釈しています。ただ、あくまで注釈なので拘束力はありません。

もう一つ、返り値の[i.decode('utf8') for i in byte_list]の書き方はリスト内包表記となっております。

実行

test_list = [b'python',b'ruby', b'node', b'golang']
byte_to_str(test_list)
['python', 'ruby', 'node', 'golang']

さもありなん

アノテーションあれこれ

文字列を入れることもできるので説明しやすくていいですね。

def byte_to_str(byte_list: "byteだけが入ったlistを入れてね") -> "listで返します。":
    return [i.decode('utf8') for i in byte_list]

また、アノテーションには拘束力はないので、引数に文字列を入れることが出来てしまいます。以下は引数を文字列で渡しました。結果、strオブジェクトのAttributeErrorになっています。引数として読み込まれたtest変数が関数内で.decode('utf8')を呼び出そうとした際に起きたErrorです。str.decode('utf8')インスタンスが無いのでAttributeErrorとなります。

test = 'insert str'
byte_to_str(test)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in byte_to_str
  File "<stdin>", line 2, in <listcomp>
AttributeError: 'str' object has no attribute 'decode'

その他、アノテーションについて、詳しくはPEPの以下のリンクをチェック

www.python.org