ブログ

割とコンピュータよりの情報をお届けします。

2019年

SQLiteのデータベースファイルへの同時アクセス2

先の記事の例がSQLiteが直接 DATETIMEを記録する型を持たずTEXTやNUMERIC(数値型でパースを試みてダメならTEXTで記録する型)に直すということについて考察が抜けている(あえてINTEGERに直していた)

どうも以下のようにしたらうまく保存できるようです.
SELECTの時にdatetime()に戻す方法もあるが,DateTimeの桁がおちてしまう(秒未満).

さらに細かくいうと,yyyy-MM-dd HH:mm:ss.fffは本当はfffではDateTimeの桁を落としてしまうのでfffffffらしい(.NETのDateTimeは100 ns単位).(JavaScriptなら1 ms単位)

書き込み

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Data.SQLite;

namespace SQLite2
{
    public partial class Form1 : Form
    {
        private System.Data.SQLite.SQLiteConnection cn;

        public Form1()
        {
            InitializeComponent();

            SQLiteConnectionStringBuilder sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = "test.db" };
            this.cn = new SQLiteConnection(sqlConnectionSb.ToString());
            this.cn.Open();

            var cmd = new SQLiteCommand(cn);
            cmd.CommandText = "CREATE TABLE IF NOT EXISTS test(" +
                    "time DATETIME NOT NULL PRIMARY KEY," +
                    "value REAL)";
            cmd.ExecuteNonQuery();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            DateTime datetime_ = DateTime.Now;
            Double value = datetime_.Ticks;
            string datetime = datetime_.ToString("yyyy-MM-dd HH:mm:ss.fff");
            var cmd = new SQLiteCommand(cn);
            cmd.CommandText = "INSERT INTO test(time, value) "
                               + "VALUES("
                               + $"'{datetime}', {value})";
            cmd.ExecuteNonQuery();
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            cn.Close();
        }
    }
}

読み込み

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Data.SQLite;

namespace SQLView2
{
    public partial class Form1 : Form
    {
        private System.Data.SQLite.SQLiteConnection cn;
        private DataSet ds;
        private System.Data.SQLite.SQLiteDataAdapter da;

        public Form1()
        {
            InitializeComponent();

            SQLiteConnectionStringBuilder sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = "../../../SQLite2/bin/Debug/test.db" };
            this.cn = new SQLiteConnection(sqlConnectionSb.ToString());
            this.cn.Open();

            ds = new DataSet();
            da = new SQLiteDataAdapter();
            var cmd = new SQLiteCommand(cn);
            cmd.CommandText = "SELECT * FROM test ORDER BY time asc";
            da.SelectCommand = cmd;

            da.Fill(ds, "test");
            this.dataGridView1.DataSource = ds.Tables["test"];

            chart1.Series[0].XValueMember = "time";// チャートへのバインド
            chart1.Series[0].YValueMembers = "value";
            this.chart1.DataSource = ds.Tables["test"];
            this.chart1.DataBind();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (ds.Tables["test"].Rows.Count > 0)
            {
                DateTime last = (DateTime)ds.Tables["test"].Rows[ds.Tables["test"].Rows.Count - 1][0];
                var cmd = new SQLiteCommand(cn);
                cmd.CommandText = "SELECT * FROM test WHERE time > "
                    + $"'{last.ToString("yyyy-MM-dd HH:mm:ss.fff")}'"
                    + " ORDER BY time asc";
                da.SelectCommand = cmd;
            }

            this.da.Fill(ds, "test");
            this.chart1.DataBind();
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            this.cn.Close();
        }
    }
}

≫ 続きを読む

2019/03/15 コンピュータ   TakeMe

SQLiteのデータベースファイルへの同時アクセス

SQLiteでは複数のプロセスからの同時アクセスはできないというページをよく見るが,一応参照は問題ないということらしい.

SQLiteを使ってデータを書き込んでみた.そのデータを参照するプロセスは複数でもよいらしい.
実際にSQLiteでデータベースを使うアプリを作ってみるとそのようだ.
実際には,Windowsの場合にはあまりお勧めしないが,一応同時アクセスを可決しようとしている実装になっているらしい.
ただし,あらゆる環境で確実な動作を保証していなそうな記述がみられる.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using System.Data.SQLite;

namespace SQLiteSample
{
    public partial class Form1 : Form
    {
        private System.Data.SQLite.SQLiteConnection cn;

        public Form1()
        {
            InitializeComponent();

            SQLiteConnectionStringBuilder sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = "test.db" };
            this.cn = new SQLiteConnection(sqlConnectionSb.ToString());
            this.cn.Open();

            var cmd = new SQLiteCommand(cn);
            cmd.CommandText = "CREATE TABLE IF NOT EXISTS test(" +
                    "datetime INTEGER NOT NULL PRIMARY KEY," +
                    "value REAL)";
            cmd.ExecuteNonQuery();
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            this.cn.Close();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Int64 datetime = DateTime.Now.Ticks;
            Double value = datetime;
            var cmd = new SQLiteCommand(cn);
            cmd.CommandText = "INSERT INTO test(datetime, value) "
                               + "VALUES("
                               + $"{datetime}, {value})";
            cmd.ExecuteNonQuery();
        }
    }
}

クライアント側

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Data.SQLite;

namespace SQLView
{
    public partial class Form1 : Form
    {
        private System.Data.SQLite.SQLiteConnection cn;
        private DataSet ds;
        private System.Data.SQLite.SQLiteDataAdapter da;

        public Form1()
        {
            InitializeComponent();

            SQLiteConnectionStringBuilder sqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = "../../../SQLiteSample/bin/Debug/test.db" };
            this.cn = new SQLiteConnection(sqlConnectionSb.ToString());
            this.cn.Open();

            ds = new DataSet();
            da = new SQLiteDataAdapter();
            var cmd = new SQLiteCommand(cn);
            cmd.CommandText = "SELECT * FROM test ORDER BY datetime asc";
            da.SelectCommand = cmd;

            da.Fill(ds, "test");
            this.dataGridView1.DataSource = ds.Tables["test"];
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            this.cn.Close();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (ds.Tables["test"].Rows.Count > 0)
            {
                Int64 last = (Int64)ds.Tables["test"].Rows[ds.Tables["test"].Rows.Count - 1][0];
                var cmd = new SQLiteCommand(cn);
                cmd.CommandText = "SELECT * FROM test WHERE datetime > "
                    + $"{last}" 
                    + " ORDER BY datetime asc";
                da.SelectCommand = cmd;
            }

            this.da.Fill(ds, "test");
        }
    }
}

≫ 続きを読む

2019/03/10 コンピュータ   TakeMe

Chart Controlsにデータバインディングしてみる例

.NET FrameworkのChart Controlsにデータバインディングしてみることにした.

なかなか難しかった.
と思ったら,データの列だけであれば,次のようにすればよいらしい.
DataTableを使用すればいちいちクラスを作らなくてもデータの列を保管できる.
そして,これをそのままバインドすればよい.

using System;
using System.Data;
using System.Windows.Forms;

namespace chartTEST
{
    public partial class Form1 : Form
    {
        private DataTable table = new DataTable("Table");
        private Timer timer = new Timer();
        private Int64 count = 0;

        public Form1()
        {
            InitializeComponent();

            

            // カラム名の追加
            table.Columns.Add("DateTime", Type.GetType("System.DateTime"));
            table.Columns.Add("Value", Type.GetType("System.Double"));

            for (int i = 0; i < 1; i++)
            {
                DataRow row = table.NewRow();
                row[0] = DateTime.Now;
                row[1] = 0.001;
                table.Rows.Add(row);
            }

            chart1.Series[0].XValueMember = "DateTime";
            chart1.Series[0].YValueMembers = "Value";
            this.chart1.DataSource = table;
            this.chart1.DataBind();

            timer.Interval = 1000;
            timer.Tick += timer1_Tick;
            timer.Start();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            count++;
            DataRow row = table.NewRow();
            row[0] = DateTime.Now;
            row[1] = 1.0 * Math.Sin(count * 0.1) + 0.001;
            table.Rows.Add(row);
            this.chart1.DataBind();
        }
    }
}

≫ 続きを読む

2019/03/04 コンピュータ   TakeMe

Pythonのmultiprocessingの例2

Pythonではmultithreading よりもmultiprocessingの方がCPUのコアを有効に使用できるが,前の例だとCtrl+Cで停止したときにプロセスが残る.
WinPython 3.7では少し改善しているので対策コードが追加できる.

メインのプロセスから,terminate()を実行するととりあえず子プロセスは消える.
実際には残るらしい.
その場合にはProcessのコンストラクタにdaemon=Falseを指定する
最初に考えたコードは次のようなもの... しかし,実際にはうまくいっていない感がある

import numpy as np
from multiprocessing import Process
import multiprocessing as mp

def calc(queue, i_range, arg1, arg2, arg3):
    print('Start: {}\r\n'.format(mp.current_process().name))
    a = arg1[i_range] + arg2[i_range];
    b = arg2[i_range] + arg3[i_range];
    c = arg3[i_range] + arg1[i_range];
    queue.put([a, b, c])

if __name__ == '__main__':
    

    x = np.linspace(0, 10000, 10000);
    y = np.linspace(10000, 0, 10000);
    z = np.linspace(0, 10000, 10000) - 5000;

    mp_size = 4
    ps = [];
    queue = mp.Queue()
    results = dict()
    for i in range(mp_size):
        ps.append(mp.Process(target=calc, args=(queue, range(x.shape[0]*i//mp_size, x.shape[0]*(i+1)//mp_size), x, y, z), name="{}".format(i+1), daemon=False))
        
    for p in ps:
        p.start()

    try:
        for i in range(mp_size):
            results[i] = queue.get();
        
    except KeyboardInterrupt:
        for p in ps:
            p.terminate()

    a = 0;
    b = 0;
    c = 0;
    for i in range(mp_size):
        a += np.asscalar(np.sum(results[i][0]));
        b += np.asscalar(np.sum(results[i][1]));
        c += np.asscalar(np.sum(results[i][2]));
    
    print('{},{},{}'.format(a, b, c))

無理やり止めるコードを追加しなおしたのが以下のコードである.
無理やり止めているのでidleでデバッグしていると例外などが発生したって確認できなくなる.ただしエンドユーザには関係なくなる.

import numpy as np
from multiprocessing import Process
import multiprocessing as mp
import signal
import sys

ps = [];

def calc(queue, i_range, arg1, arg2, arg3):
    print('Start: {}\r\n'.format(mp.current_process().name))
    a = arg1[i_range] + arg2[i_range];
    b = arg2[i_range] + arg3[i_range];
    c = arg3[i_range] + arg1[i_range];
    queue.put([a, b, c])

def handler(signum, frame):
    global ps;
    for p in ps:
        if p.is_alive():
            try:
                p.terminate()
            except AssertionError:
                pass
            print('terminate {}'.format(p.pid));
    sys.exit(0);

if __name__ == '__main__':
    
    signal.signal(signal.SIGINT, handler);
    
    x = np.linspace(0, 10000, 10000);
    y = np.linspace(10000, 0, 10000);
    z = np.linspace(0, 10000, 10000) - 5000;

    mp_size = 4
    
    queue = mp.Queue()
    results = dict()
    for i in range(mp_size):
        ps.append(mp.Process(target=calc, args=(queue, range(x.shape[0]*i//mp_size, x.shape[0]*(i+1)//mp_size), x, y, z), name="{}".format(i+1), daemon=False))
       
    for p in ps:
        p.start()
        
    for i in range(len(ps)):
        results[i] = queue.get();
        
    a = 0;
    b = 0;
    c = 0;
    for i in range(mp_size):
        a += np.asscalar(np.sum(results[i][0]));
        b += np.asscalar(np.sum(results[i][1]));
        c += np.asscalar(np.sum(results[i][2]));
    
    print('{},{},{}'.format(a, b, c))
    

実はこれでも対策は不十分でどうにもならないことがある.子プロセスの準備中などに止められると異常終了して意図通りに止まらないことがあるのだ.(is_alive()のせいかも)

≫ 続きを読む

2019/03/02 コンピュータ   TakeMe
タグ:Python

Pythonのmultiprocessingの例

Pythonではmultithreading よりもmultiprocessingの方がCPUのコアを有効に使用できる.

Pythonではマルチスレッドのプログラムが簡単.
よほどのことがない限り,異常は発生してなかったが,
GILのためという理由で,なかなか性能が発揮できない場面が大かった.

おそらくこの例が,マルチプロセスに直した場合になっている.

(これを作ったときには,nameに文字列以外を挿入して大失敗した.ちゃんと文字列を入れましょう)

import numpy as np
from multiprocessing import Process
import multiprocessing as mp

def calc(queue, i_range, arg1, arg2, arg3):
    print('Start: {}\r\n'.format(mp.current_process().name))
    a = arg1[i_range] + arg2[i_range];
    b = arg2[i_range] + arg3[i_range];
    c = arg3[i_range] + arg1[i_range];
    queue.put([a, b, c])

if __name__ == '__main__':
    x = np.linspace(0, 10000, 10000);
    y = np.linspace(10000, 0, 10000);
    z = np.linspace(0, 10000, 10000) - 5000;

    mp_size = 4
    ps = [];
    queue = mp.Queue()
    results = dict()
    for i in range(mp_size):
        ps.append(mp.Process(target=calc, args=(queue, range(x.shape[0]*i//mp_size, x.shape[0]*(i+1)//mp_size), x, y, z), name="{}".format(i+1)))
        
    for p in ps:
        p.start()
    
    for i in range(mp_size):
        results[i] = queue.get();

    
    a = 0;
    b = 0;
    c = 0;
    for i in range(mp_size):
        a += np.asscalar(np.sum(results[i][0]));
        b += np.asscalar(np.sum(results[i][1]));
        c += np.asscalar(np.sum(results[i][2]));
    
    
    print('{},{},{}'.format(a, b, c))

なんの問題もなく終了するときは良いが,強制終了がかかると子プロセスが残る問題が生じる.この件は次の記事で扱う.

≫ 続きを読む

2019/03/01 コンピュータ   TakeMe
タグ:Python

WCFのバッファサイズ制限

WCFを使用するアプリケーションを開発していたところ,大容量のデータを転送したところで,失敗した.

デフォルト バッファサイズが決まっている.

WCFを使用するアプリケーションを開発していたところ,大容量のデータを転送したところで,失敗した.が,これまでまともに設定ファイルなどいじったことがないため焦ってしまった.

デフォルト バッファサイズが決まっているので,maxBufferSizeとmaxReceivedMessageSizeを設定する必要がある.

参考「WCFで大きなデータを送受信するさいに注意する設定値(クオータ)のメモ

これら2つの数値は,2つあるので別々だと思っていたが,異なる値を設定すると失敗した.
WCFの導入までは何の苦労もなくできてしまうので,いったん躓くとつらい.

名前付きパイプを使っていたので今回はこれでよいのだが,HTTPなどを使用する場合には,安易にバッファサイズを大きくすることは攻撃に弱くなったり,意図しない再送・ハイレートの転送でシステムがダウンする恐れが高まるということで,バッファを超えたら分けて送付というやり方の方が本当は良いのかもしれない.

≫ 続きを読む

2019/02/28 コンピュータ   TakeMe

WindowsのMemory Mapped Fileはx86とx64のデータの橋渡しにも使える場合が多い

.NETのMemory Mapped Fileを扱うプログラムを書いていたら,32bitも64bitも混ざっていたがどうもやり取りはできるらしい.

Windowsでは共有メモリとしてMemory Mapped Fileを使うと複数のプロセスから同じメモリ領域を参照しているような動作が可能になる.
.NET Framework 4.0からはサポートするクラスが用意されているので比較的簡単に利用できる.
ただし,.NETではターゲットがAny CPUだと32bit用のつもりのものが64bit環境で動いたり,逆も起こる.
このときどうなるのか気になっていたが,概ね問題なく動作する.

概ねというのは一つ一つの変数のアドレスを指定したバイト単位の参照の場合には問題ないということ.おそらく一度に参照しようとした場合には考慮すべきことが増えるのではないかと思うので後で調べる.

例えば,以下のようなコード

// サーバ側
using System;
using System.Windows.Forms;
using System.IO.MemoryMappedFiles;

namespace MemoryMappedFile_TEST
{
    public partial class ServerForm : Form
    {
        private MemoryMappedFile mem = null;
        private MemoryMappedViewAccessor accessor = null;
        private Timer timer = new Timer();

        private Int64 counter = 0;

        public ServerForm()
        {
            InitializeComponent();

            this.FormClosed += ServerForm_FormClosed;

            mem = MemoryMappedFile.CreateNew("test_memory", 64);
            accessor = mem.CreateViewAccessor();

            this.label1.Text = counter.ToString();

            timer.Interval = 1000;
            timer.Tick += timer_Tick;
            timer.Start();
        }

        private void ServerForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            timer.Stop();
            accessor.Dispose();
        }

        private void timer_Tick(object sender, EventArgs e)
        {
            // 書き込み
            accessor.Write((Int64)0, (Int64)counter++);

            this.label1.Text = counter.ToString();
        }
    }
}
// クライアント側
using System;
using System.Windows.Forms;
using System.IO.MemoryMappedFiles;

namespace Client
{
    public partial class UserForm : Form
    {
        private MemoryMappedFile mem = null;
        private MemoryMappedViewAccessor accessor = null;
        private Timer timer = new Timer();

        private Int64 value = 0;

        public UserForm()
        {
            InitializeComponent();

            this.FormClosed += UserForm_FormClosed;

            mem = MemoryMappedFile.OpenExisting("test_memory");
            accessor = mem.CreateViewAccessor();

            this.label1.Text = value.ToString();

            timer.Interval = 1000;
            timer.Tick += timer_Tick;
            timer.Start();
        }

        private void UserForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            timer.Stop();
            accessor.Dispose();
        }

        private void timer_Tick(object sender, EventArgs e)
        {
            // 書き込み
            value = accessor.ReadInt64((Int64)0);

            this.label1.Text = value.ToString();
        }
    }
}

≫ 続きを読む

2019/02/18 コンピュータ   TakeMe

WPFのメモリリーク対策

C#ではメモリリークはほとんど心配ないと思っていたが,WPFアプリケーションを作っていると,メモリリークが頻繁に起きることが分かって恐ろしくなった.

まず,参考のページ「俺が遭遇したWPFイメージコントロールのメモリーリークと回避法(?)の1つ」を参考にすると,Imageコントロールはよくリークを起こす.参考のページのサンプルコードの最後の方にあるように,明示的にSourceにnullを入れてやらないと割と残るようだ.

リーク以外に,キャッシュという仕組みもありわかりにくい.xamlにイメージのファイル名を直接書いていると一度使ったイメージはキャッシュされ次の使用が速くなる(らしい).

このほか,UserControlもうまく設計していない場合,リークを起こす.Imageを使う場合やTimerを使う場合にはIDisposalインタフェースを実装して明示的にDisposeを呼び出すことを強く推奨する.TimerなんかはいったんStartしたままコントロールを破棄してしまうと(Windowを閉じるなど),一見コントロールが使えなくなくなっても残ってしまう.しかも,消えるタイミングが不定で恐ろしいバグになる(たいていはtickも残る).

Visual Studio 2017 ProfessionalやCommunityなら診断ツールを用いてヒープを表示させると,参照しているオブジェクト種類の一覧と参照されているオブジェクトの一覧が取れるのでデバッグの助けになる.
ポイントはImageが残っていないか?Start()を呼んだままのTimerが残っていないか?かな

≫ 続きを読む

2019/02/13 日記   TakeMe
タグ:WPF

C#からPythonの関数を呼び出し2

C#からPythonの関数を呼び出し」の最後の方で,「デバッグの時は云々」の部分をもう少し詳しく書く.

まずはコードを示してみる.

using System;
using System.Collections.Generic;
using Python.Runtime;

namespace TEST
{
    class Program
    {
        // TEST.exe C:\python-3.7.0-embed-amd64\
        // numpy入りのpython embeddableを指定してください.
        static void Main(string[] args)
        {
            var path = Environment.GetEnvironmentVariable("PATH");
            path = path + args[0] + ";";
            Environment.SetEnvironmentVariable("PATH", path);

            using (Py.GIL())
            {
                dynamic sys = Py.Import("sys");
                sys.path.append(args[0]);

                dynamic np = Py.Import("numpy");
                dynamic a = np.array(new List<double> { 1, 2, 3 });
                Console.WriteLine(a.dtype);
                dynamic b = np.array(new List<int> { 6, 5, 4 }, dtype: np.int32);
                Console.WriteLine(b.dtype);
                Console.WriteLine(a * b);
                Console.ReadKey();
            }
        }
    }
}

まずは,環境変数PathにPython Embeddableのインストールパスを設定する.
さらに,sys.path.appendでPythonモジュールのパスも指定する.
このようにすると,実行時の引数にPython embeddableのインストールパスを指定すると実行ができる.
 

≫ 続きを読む

2019/02/11 コンピュータ   TakeMe
タグ:Python

C#からPythonの関数を呼び出し

C言語からPythonの関数呼び出し4」から発展を考えていたら,C#から呼び出しの記事を書いていないことに気が付いたので備忘録も含めて記載する.
なお,参考は「Pythonから.NETを呼び出す方法とついでにその逆も(https://qiita.com/y-tsutsu/items/0c4561d6478be32e6f8e)」なのだ.

使用するのはPython for .NETである.これ自体は前にも扱っている.このときには,Pythonから.NETを呼び出すというのをやっていた.
今回はその逆である.

git clone https://github.com/pythonnet/pythonnet.git

でgithubからpythonnetをダウンロードする.
ダウンロードされたフォルダの中にpythonnet.slnファイルがあるはずなのでこれを開く(Visual Studio 2017).

その後,ビルドの構成はReleaseWinPY3でx64(実際には実行環境に合わせる)でビルドする.するとbinフォルダにPython.Runtime.dllが出来上がる.

次に,.NETのプロジェクトを作る.
この時,アッセンブリの参照に先のPython.Runtime.dllを加えてやる(これが大事).参考のページではNugetパッケージをとってきているが,Python 3.7もPython for .NETもGitHubの方の更新が速いのでこのNugetパッケージは更新が全く追い付いていない.

続いて,「Pythonから.NETを呼び出す方法とついでにその逆も」からサンプルコードを頂く.
例えば以下のようなコードになる.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Python.Runtime;

namespace TEST
{
    class Program
    {
        static void Main(string[] args)
        {
            using (Py.GIL())
            {
                dynamic np = Py.Import("numpy");
                dynamic a = np.array(new List<double> { 1.0, 2.1, 3.2 });
                Console.WriteLine(a.dtype);
                dynamic b = np.array(new List<float> { 4, 5, 6 }, dtype: np.int32);
                Console.WriteLine(b.dtype);
                Console.WriteLine(a * b);
                Console.ReadKey();
            }
        }
    }
}

実行時には,NumpyをインストールしたPython Embeddableのフォルダの場所にexeファイルとdllファイル突っ込めば実行できる.(Visual Studioのデバッグの際にはプロジェクトのプロパティで「デバッグ」の設定で,同等の構成になるように作業フォルダなどを調整する)

≫ 続きを読む

2019/02/10 コンピュータ   TakeMe
タグ:Python