ラベル SQLite の投稿を表示しています。 すべての投稿を表示
ラベル SQLite の投稿を表示しています。 すべての投稿を表示

2023-12-29

PDF をデータベースに格納 ~ PySide6

PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux/X11, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。

本ブログの過去記事 [1] で、PDF ファイルを SQLite のデータベースに格納する PySide6 のサンプルを紹介しました。その後 PostgreSQL でも同じことをしようと試行錯誤していたところ、バイナリをデータベースに格納するのに Base64 のモジュールより、PySide6 の QByteArray [2] クラスを利用した方がシンプルになりそうだったので、書き直すことにしました。

また SQLite と PostgreSQL それぞれに対応したサンプルを、なるべく共通な部分を流用することで一緒に紹介することにしました。

今回のテーマ
  • 選択した PDF ファイルを読み込んでデータベースへ格納します。そして、データベースから読み込んだ内容を(一旦保存して) QPdfView クラスのウィジェット上に表示する、という GUI サンプルを紹介します。
  • データベースは最初 SQLite で確認したあと、PostgreSQL でも同じことができるサンプルを作成します。そのため、SQLite と PostgreSQL で共通に使える部分が多くなるように意識してサンプルを作成します。

なお、PDF ファイルのサンプルは、東京国立博物館ニュースの PDF ファイルを利用させていただきました [3]

下記の OS 環境で動作確認をしています。

Fedora Workstation 39 x86_64
Python 3.11.7
PySide6 6.6.1
(libpq-15.3-1.fc39.x86_64)
AlmaLinux 9.3 (192.168.0.34) x86_64
PostgreSQL (server) 15.5

【注意】

本稿で紹介するサンプルは、シンプルで判りやすく、かつサイズ(行数)を抑えることを優先したため、想定しうるエラー処理をほとんどしていません。

データベースに SQLite を利用したサンプル

以下にサンプル qt_db_sqlite_pdf.py を示しました(SQLitePDF クラス)。

SQLite 版サンプルの実行に必要なファイル
  • qt_db_sqlite_pdf.py(メイン)
  • qt_db_common_pdf.py
qt_db_sqlite_pdf.py

SQLite と PostgreSQL で変更無しに利用できるクエリ処理部分を qt_db_common_pdf.py に分離しました。

qt_db_common_pdf.py

サンプルの説明

初期化

サンプルを実行すると、データベースに接続するインスタンスを生成します。このとき指定した名前のデータベース・ファイル test.sqlite が存在してければ作成されます。

SQLitePDF クラスの冒頭
class SQLitePDF(QMainWindow):
    app_title = 'SQLite & PDF test'

    def __init__(self):
        super().__init__()
        self.con = self.get_connection()
        self.init_table()
           :
           :

データベースに接続するインスタンスの生成は、SQLite と PostgreSQL では異なるので、あとあとのことを考えて get_connection メソッドで処理するようにしました。

get_connection メソッド(SQLite 用)
    @staticmethod
    def get_connection() -> QSqlDatabase:
        con = QSqlDatabase.addDatabase('QSQLITE')
        dbname = 'testdb.sqlite'
        con.setDatabaseName(dbname)
        return con

その後 init_table メソッドで、使用するデータベースのテーブルの初期化をします。

init_table メソッド
    def init_table(self):
        if self.con.open():
            self.create_table()
            self.con.close()

テーブルの初期化処理は create_table メソッドで処理しています。

create_table メソッド(SQLite 用)
    @staticmethod
    def create_table():
        query = QSqlQuery()
        sql = """
            CREATE TABLE IF NOT EXISTS pdfrepo (
                name_file TEXT UNIQUE,
                content NONE
            );
        """
        if not query.exec(sql):
            print(query.lastError())

なお、get_connectioncreate_table は、敢えてスタティック・メソッドとして SQLitePDF クラス内に残しています。

その後、プログラムは GUI を生成します。

PDF ファイルを読み込んでみる

実行したサンプルのメニューから File » Open をクリックします。

qt_db_sqlite_pdf.py の実行例 (1) - メニューから File » Open

データベースに格納する PDF ファイルを選択します。

qt_db_sqlite_pdf.py の実行例 (2) - PDF ファイルの選択

バイナリーデータをデータベースへ格納

ファイルをバイナリモードで読み込んで、パスを除いたファイル名と読み込んだ内容を、データベースに格納します。

show_dialog メソッド
    def show_dialog(self):
        dialog = QFileDialog()
        dialog.setWindowTitle('PDF file selection')
        dialog.setNameFilters(['PDF files (*.pdf)'])
        if dialog.exec():
            filename = dialog.selectedFiles()[0]
            basename = os.path.basename(filename)
            f = open(filename, 'rb')
            with f:
                content = f.read()

            if self.con.open():
                insert_filename_content(basename, content)
                self.con.close()
                self.update_filelist()
                self.combo.setCurrentText(basename)

PySide6 でバイナリ・データをそのままクエリに渡してもデータベースに格納できなかったのですが、QByteArray 型にすると格納できました。QByteArray 型にするということは、読み出すときにも QByteArray 型を扱えるようにする必要があるので、「PySide6 あるいは Qt6 のライブラリを使わなければならない」という縛りができてしまいますが、ここではそれを良しとしました。

このクエリを処理する insert_filename_content 関数は PostgreSQL でも利用します。

insert_filename_content 関数
def insert_filename_content(filename: str, content: bytes):
    sql = 'INSERT INTO pdfrepo VALUES(?, ?);'
    query = QSqlQuery()
    query.prepare(sql)
    query.bindValue(0, filename)
    query.bindValue(1, QByteArray(content))
    if not query.exec():
        print(query.lastError())

データベースに書き込んだ内容を読み込み直して QPdfView のインスタンスに表示されます。

qt_db_sqlite_pdf.py の実行例 (3) - 読み込んだ PDF ファイルの表示

データベースにあるファイルリストの取得

QPdfView のインスタンス view は、QMainWindow を継承した SQLitePDF クラスに setCentralWidget メソッドで配置されています。

init_ui メソッドの一部
    def init_ui(self):
           :
           :
        combo.currentTextChanged.connect(self.on_current_text_changed)
        toolbar.addWidget(combo)

        view = QPdfView(self)
        view.setPageMode(QPdfView.PageMode.MultiPage)
        view.setZoomMode(QPdfView.ZoomMode.FitToWidth)
        self.setCentralWidget(view)

QToolBar にある QComboBox に表示するファイルリストは下記の関数のクエリで取得しています。

このクエリを処理する get_list_file 関数は PostgreSQL でも利用します。

get_list_file 関数
def get_list_file(list_file: list):
    query = QSqlQuery()
    sql = 'SELECT name_file FROM pdfrepo;'
    flag = query.exec(sql)
    while query.next():
        list_file.append(query.value(0))
    if not flag:
        print(query.lastError())

QComboBox のインスタンスに表示されている文字が変更されるとシグナルが発生して、インスタンス・メソッド on_current_text_changed メソッドが実行されます。

on_current_text_changed メソッド
    def on_current_text_changed(self):
        filename = self.combo.currentText()
        if len(filename) == 0:
            return

        if self.con.open():
            content = get_content_from_filename(filename)
            self.con.close()

            if content is not None:
                filepath = os.path.join(tempfile.gettempdir(), filename)
                with open(filepath, 'wb') as f:
                    f.write(content)
                document = QPdfDocument(self)
                document.load(filepath)
                view: QWidget | QPdfView = self.centralWidget()
                view.setDocument(document)

このメソッドで、データベースから QComboBox のインスタンスに表示された文字列 = ファイル名に対応するファイルの中身(バイナリ・データ)をデータベースから読み込みます(get_content_from_filename 関数)。

データベースから取り出して元に戻したバイナリにファイル名をつけてテンポラリの領域に保存して、それを QPdfDocument のインスタンスで読み込んで、QPdfView のインスタンス view に渡して表示しています。

【追記】テンポラリ領域に保存しない方法 (2024-01-04)

データベースから取り出したバイナリをファイルに保存せずに QPdfDocument のインスタンスに読み込ませる方が効率的です。あとになって動作確認ができたので、追記します。

on_current_text_changed メソッド(改)
    def on_current_text_changed(self):
        filename = self.combo.currentText()
        if len(filename) == 0:
            return

        if self.con.open():
            content = get_content_from_filename(filename)
            self.con.close()

            if content is not None:
                byte_array = QByteArray(content)
                buffer = QBuffer(byte_array)
                buffer.open(QIODevice.OpenModeFlag.ReadOnly)
                document = QPdfDocument(self)
                document.load(buffer)
                view: QWidget | QPdfView = self.centralWidget()
                view.setDocument(document)

QByteArray 型を QBuffer のインスタンスに渡す関係で、get_content_from_filename 関数でデータベースから取り出した際に bytes 型に戻したのに、再び QByteArray 型にすることになりました。最初にサンプルを作った流れを踏襲したので無駄な処理になってしまっています。🙇🏻

バイナリーデータをデータベースから取り出す

クエリで取り出した QByteArray 型のオブジェクトから data メソッドで元のバイナリ取り出して返しています。

この get_content_from_filename 関数は PostgreSQL でも利用します。

get_content_from_filename 関数
def get_content_from_filename(filename: str) -> bytes:
    byte_array = None
    content = None
    query = QSqlQuery()
    sql = """
        SELECT content FROM pdfrepo
        WHERE name_file = '%s';
    """ % filename
    flag = query.exec(sql)
    if query.next():
        byte_array = query.value(0)
    if not flag:
        print(query.lastError())
    if byte_array is not None:
        content = byte_array.data()
    return content

複数の PDF ファイルの読み込み

複数の PDF ファイルをデータベースに読み込むと、QComboBox のインスタンスにファイル名が列挙されます。どれかを選択すれば、その PDF ファイルが表示されます。

qt_db_sqlite_pdf.py の実行例 (4) - 読み込んだ複数の PDF ファイルから選択

データベースに PostgreSQL を利用したサンプル

以下にサンプル qt_db_postgres_pdf.py を示しました(PostgresPDF クラス)。

PostgreSQL 版サンプルの実行に必要なファイル
  • qt_db_sqlite_pdf.py(SQLite 版のスクリプト)
  • qt_db_common_pdf.py(SQLite 版のスクリプト)
  • qt_db_postgres_pdf.py(メイン)
  • qt_db_postgres_pdf.py(データベース接続用ダイアログ)
qt_db_postgres_pdf.py

PostgreSQL データベースへ接続するのに必要な情報を入力するダイアログ、DBInfoDlg クラスを用意しました (qt_db_postgres_dialog.py)。サンプルの起動の度に入力するのは面倒ですが、安易にパスワードを露わにして処理することに躊躇しました。😅

qt_db_postgres_dialog.py

サンプルの説明

さて、メインの PostgresPDF クラスですが、SQLite を利用した前述の SQLitePDF クラスを継承して、PostgresSQL 用にオーバーライドしています。

SQLitePDF を継承した PostgresPDF クラス(PostgreSQL 用)
class PostgresPDF(SQLitePDF):
    app_title = 'PostgreSQL & PDF test'

    def __init__(self):
        super().__init__()

オーバーライドしているのは、ウィンドウに表示するタイトル文字列と、スタティック・メソッドにした下記の二つのメソッドです。

なお、テーブルを作成する SQL は、SQLite と PostgreSQL で共通にできるのかもしれませんが、ここでは別々にしました。

create_table メソッド(PostgreSQL 用)
    @staticmethod
    def create_table():
        query = QSqlQuery()
        sql = """
            CREATE TABLE IF NOT EXISTS pdfrepo (
                name_file character varying(255) UNIQUE,
                content bytea
            );
        """
        if not query.exec(sql):
            print(query.lastError())

データベースとの接続を処理する get_connection メソッドでは、DBInfoDlg クラスのダイアログを表示して、接続に必要な情報を取得しています。

get_connection メソッド(PostgreSQL 用)
    @staticmethod
    def get_connection() -> QSqlDatabase:
        con = QSqlDatabase.addDatabase('QPSQL')
        dict_info = dict()
        dlg = DBInfoDlg(dict_info)
        if dlg.exec():
            con.setHostName(dict_info['host'])
            con.setDatabaseName(dict_info['database'])
            con.setUserName(dict_info['user'])
            con.setPassword(dict_info['password'])
        return con

サンプル qt_db_postgres_pdf.py を実行すると、最初にデータベースへ接続するための情報を入力するダイアログが表示されます。必要事項を正しく入力して OK ボタンをクリックします。入力情報が間違っていてもなにか警告が出るようにはなっていませんのでご注意ください。🙇🏻

qt_db_postgres_pdf.py の実行例 (1) - データベースへの接続ダイアログ

あとの操作は、SQLite 版 (qt_db_sqlite_pdf.py) と同じです。

qt_db_postgres_pdf.py の実行例 (2) - 読み込んだ PDF ファイルの表示例

まとめ

データベースを利用する規模や状況にもよるのでしょうが、定型のレポートなど、あまり大きくないサイズのファイルをデータベースにバイナリで格納するはアリだと思っているので、PDF を例にしてサンプルを作ってみました。

また、なにかデータベースを利用した GUI アプリを作るのに、まず試しに SQLite で動作確認をしてから PostgreSQL などのデータベース・サーバーに移行する、ということがよくあります。移行の際、あまり大きな変更をしなくとも PostgreSQL へ移行するのに効果的な方法はないものかと模索している一環として、クラスを継承するというアプローチを取ってみました。

参考サイト

  1. bitWalk's: SQLite と PDF ファイル ~ PySide6 [2023-12-20]
  2. QByteArray - Qt for Python
  3. 東京国立博物館 - 調査・研究・貸与 出版・刊行物 東京国立博物館ニュース

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2023-12-20

SQLite と PDF ファイル ~ PySide6

SQLite は、パブリックドメインの軽量なリレーショナルデータベース管理システム (RDBMS) です。他の多くのデータベース管理システムとは対照的に、サーバとしてではなくアプリケーションに組み込んで利用するデータベースです。 一般的な RDBMS と違い、API は単純にライブラリを呼び出すだけであり、データの保存に単一のファイルを使用することが特徴です。

Wikipedia より引用、編集
今回のテーマ

選択した PDF ファイルを読み込んで SQLite のデータベースに格納します。そして、データベースから読み込んだ内容を(一旦保存して) QPdfView クラスのウィジェットで開く、という GUI サンプルを紹介します。

  • 本ブログの過去記事 [1] を PySide2 から PySide6 に変更して、PySide6 / Qt6 の機能をなるべく利用できるような内容に見直しました。

下記の OS 環境で動作確認をしています。

Fedora Workstation 39 x86_64
Python 3.11.6
PySide6 6.6.1

少し長いサンプルになってしまいましたが、以下にサンプル qt_sqlite_pdf.py を示しました。

qt_sqlite_pdf.py

サンプルの説明

初期化

サンプルを実行すると、データベースに接続するインスタンスを生成します。このとき指定した名前のデータベースファイル test.sqlite が存在してければ作成されます。

class Example(QMainWindow):
    def __init__(self):
        super().__init__()
        self.dbname = 'test.sqlite'
        self.con = QSqlDatabase.addDatabase('QSQLITE')
        self.con.setDatabaseName(self.dbname)
        self.init_table()
           :
           :

その後、使用するデータベースのテーブルの初期化をします。

    def init_table(self):
        if self.con.open():
            create_table()
            self.con.close()

テーブルの初期化処理は下記のように単純なものです。

def create_table():
    query = QSqlQuery()
    sql = """
        CREATE TABLE IF NOT EXISTS file (
            name_file TEXT UNIQUE,
            content NONE
        );
    """
    if not query.exec(sql):
        print(query.lastError())

その後、プログラムは GUI を生成します。

PDF ファイルを読み込んでみる

実行したサンプルのメニューから File » Open をクリックします。

qt_sqlite_pdf.py の実行例 (1)

データベースに格納する PDF ファイルを選択します。

qt_sqlite_pdf.py の実行例 (2)

ファイルをバイナリモードで読み込んで、パスを除いたファイル名と読み込んだ内容を、データベースに格納します。

    def show_dialog(self):
        dialog = QFileDialog()
        dialog.setWindowTitle('PDF file selection')
        dialog.setNameFilters(['PDF files (*.pdf)'])
        if dialog.exec():
            filename = dialog.selectedFiles()[0]
            basename = os.path.basename(filename)
            f = open(filename, 'rb')
            with f:
                content = base64.b64encode(f.read())
                content_str: str = content.decode()

            if self.con.open():
                insert_filename_content(basename, content_str)
                self.con.close()
                self.update_filelist()
                self.combo.setCurrentText(basename)

PySide2 を利用した過去記事 [1] では、sqlite3 モジュールを利用して、なにも考えずにバイナリをデータベースに格納できたのですが、PySide6 では QSqlQuery のインスタンスで同じようにしたところ、バイナリを書き込むことができませんでした。

PySide6 ではクエリのタイプにバイナリを指定できるのですが、これでは解決できなかったので、やむなく Python の標準ライブラリで利用できる Base64 でエンコードした byte 型のデータ列を文字列にデコードして格納しています。

def insert_filename_content(filename: str, content_str: str):
    sql = 'INSERT INTO file VALUES(?, ?);'
    query = QSqlQuery()
    query.prepare(sql)
    query.bindValue(0, filename)
    query.bindValue(1, content_str)
    """
    note: This does not work!
    query.bindValue(
        1, content,
        type=QSql.ParamTypeFlag.In | QSql.ParamTypeFlag.Binary
    )
    """
    if not query.exec():
        print(query.lastError())

データベースに書き込んだ内容を読み込み直して QPdfView のインスタンスに表示されます。

qt_sqlite_pdf.py の実行例 (3)

QPdfView のインスタンス view は、QMainWindow を継承した Example クラスに setCentralWidget メソッドで配置されています。

    def init_ui(self):
           :
           :
        self.combo.currentTextChanged.connect(self.on_current_text_changed)
        toolbar.addWidget(self.combo)

        view = QPdfView(self)
        view.setPageMode(QPdfView.PageMode.MultiPage)
        view.setZoomMode(QPdfView.ZoomMode.FitToWidth)
        self.setCentralWidget(view)

QComboBox のインスタンスに表示されている文字が変更されるとシグナルが発生して、インスタンス・メソッド on_current_text_changed メソッドが実行されます。

    def on_current_text_changed(self):
        filename = self.combo.currentText()
        if len(filename) == 0:
            return

        if self.con.open():
            content = get_content_from_filename(filename)
            self.con.close()

            if content is not None:
                filepath = os.path.join(tempfile.gettempdir(), filename)
                with open(filepath, 'wb') as f:
                    f.write(content)
                document = QPdfDocument(self)
                document.load(filepath)
                view: QWidget | QPdfView = self.centralWidget()
                view.setDocument(document)

このメソッドで、データベースから QComboBox のインスタンスに表示された文字列 = ファイル名に対応するファイルの中身をデータベースから読み込みます。

クエリの処理は下記のようなっています。ここで、取り出した文字列を byte 型にエンコードして、Base64 のデコードをして元のバイナリに戻しています(こうやって説明すると、なんだか面倒くさいことをしていると思います、今後の改善課題です)。

def get_content_from_filename(filename: str) -> bytes:
    content = None
    query = QSqlQuery()
    sql = """
        SELECT content FROM file
        WHERE name_file = "%s";
    """ % filename
    query.exec(sql)
    if query.next():
        content_str = query.value(0)
        content = base64.b64decode(content_str.encode())
    return content

データベースから取り出して元に戻したバイナリにファイル名をつけてテンポラリの領域に保存して、それを QPdfDocument のインスタンスで読み込んで、QPdfView のインスタンス view に渡して表示しています。

複数の PDF ファイルの読み込み

複数の PDF ファイルをデータベースに読み込むと、QComboBox のインスタンスにファイル名が列挙されます。どれかを選択すれば、その PDF ファイルが表示されます。

qt_sqlite_pdf.py の実行例 (4)

既知の問題

PySide6 / Qt6 の PDF を扱うモジュールをまだ使い慣れていないためか、標準出力に下記のような警告が出ます。特定の PDF ファイルで警告が出るのですが、それが PDF ファイルの問題なのか特定できていません。対応方法が判ったら追記します。

qt.pdf.links: skipping link with invalid page number
qt.pdf.links: skipping link with invalid page number
qt.pdf.links: skipping link with invalid page number
    :
    :
    :

参考サイト

  1. bitWalk's: 【備忘録】SQLite のデータベースでバイナリデータを扱う [2021-02-18]
  2. QPdfDocument - Qt for Python
  3. QPdfView - Qt for Python

 

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2021-02-18

【備忘録】SQLite のデータベースでバイナリデータを扱う

SQLite (/ˌɛsˌkjuːˌɛlˈaɪt/, /ˈsiːkwəˌlaɪt/) は、パブリックドメインの軽量なリレーショナルデータベース管理システム (RDBMS) です。他の多くのデータベース管理システムとは対照的に、サーバとしてではなくアプリケーションに組み込んで利用するデータベースです。 一般的な RDBMS と違い、API は単純にライブラリを呼び出すだけであり、データの保存に単一のファイルを使用することが特徴でです。

Wikipedia より引用、編集

SQLite のデータベースでバイナリーデータを扱うケースを、Qt for Python (Pyside2) の GUI で、PDF ファイルを選んで読み込んだ内容をデータベースに格納し、それを外部プログラムで開く、というサンプルを作って確認しましたので、備忘録にしました。

本ブログ記事では下記の OS 環境で動作確認をしています。

Fedora 33 (Workstation Edition) x86_64

以下に、サンプル sqlite_binary_test.py を示しました。

sqlite_binary_test.py

サンプルを実行して、メニューから File(F) » Open をクリックします。

sqlite_binary_test.py の実行例 (1)

SQLite のデータベースファイル self.dbname (= 'test.sqlite') が存在しなければ、カレントディレクトリに作成します。

    def initDB(self):
        init_query = [
            'CREATE TABLE file (name_file TEXT UNIQUE, content NONE)',
        ]

        con = sqlite3.connect(self.dbname)
        cur = con.cursor()

        for query in init_query:
            cur.execute(query)

        con.commit()
        con.close()

データベースに書き込む PDF ファイルを選択します。

sqlite_binary_test.py の実行例 (2)

ファイルをバイナリモードで読み込んでパスを除いたファイル名と、読み込んだバイナリをデータベースに書き込みます。

    def showDialog(self, combo: QComboBox):
        dialog = QFileDialog()
        dialog.setNameFilters(['PDF files (*.pdf)'])
        if dialog.exec_():
            fname = dialog.selectedFiles()[0]
            name_file = os.path.basename(fname)
            f = open(fname, 'rb')
            with f:
                content = f.read()

                con = sqlite3.connect(self.dbname)
                cur = con.cursor()
                cur.execute("INSERT INTO file VALUES(?, ?);", [name_file, content])
                con.commit()
                con.close()

                self.get_filelist(combo)

コンボボックスに読み込んだファイルが追加されるので、 Open ボタンをクリックします。

sqlite_binary_test.py の実行例 (3)

コンボボックスに表示されているファイル名と一致するレコードのバイナリの内容を読み込んで、一時保存領域にファイル名で保存して、デフォルトのアプリケーションでファイルを開きます。

    def on_click_open(self, combo: QComboBox):
        name_file = combo.currentText()
        if len(name_file) == 0:
            return

        out_file = os.path.join(tempfile.gettempdir(), name_file)

        con = sqlite3.connect(self.dbname)
        cur = con.cursor()
        cur.execute("SELECT content FROM file WHERE name_file = ?;", [name_file])
        out = cur.fetchall()
        con.close()

        with open(out_file, 'wb') as f:
            f.write(out[0][0])

        if platform.system() == 'Linux':
            subprocess.Popen(['xdg-open', out_file])
        elif platform.system() == 'Darwin':
            subprocess.Popen(['open', out_file])
        else:
            os.startfile(out_file)

GNOME のドキュメントビューア Evince が起動し、PDF の内容が表示されます。

Evince による PDF ファイルの表示

動作確認をする目的で作成したサンプルであるため、無駄な重複が残っていますしエラー時の例外処理などもしていません。なお、今回扱った PDF ファイルは圧縮されているので、Python 側で更に圧縮する処理は加えていませんが、扱うバイナリファイルの内容により検討する必要があります。

参考サイト

  1. SQLite Home Page
  2. SQLiteで利用可能なデータ型 | SQLite入門

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村

2019-08-17

機械学習と株価

R の quantmod (Quantitative Financial Modelling Framework) パッケージは、クオンツ取引 (qualitative trading) [1] において、統計的手法に基づいた取引モデルの構築、検証および運用の支援ができるように提供されています [2] [3]。このパッケージの getSymbols() 関数を使うと、例えば Yahoo!ファイナンス のサイトから指定した証券コードの株価データを取得することができます。

株価予測に機械学習を応用

今年初めにこのパッケージを使ってみて、株価を大量に取得できることを知って大いに驚きました。これは機械学習の応用分野に最適だと考え、予測モデルを作ることに取り組むことにしました [4] [5]。手始めに以下のようなテーマを掲げました。

当日の株関連情報を用いて、翌日(次回)のソフトバンク株式会社 (9434.T) の始値を予測せよ。

ソフトバンクグループ (SBG) の国内通信子会社、ソフトバンク株式会社 (9434.T) に注目した理由は、昨年 12 月 19 日に東京証券取引所第1部に上場したからです [6]。これでデータの取得開始日を 2019-12-19 に設定することができました。当日株価を取得してデータベースに追加、データを学習させて次回の始値を予測するという R スクリプトを作成し、毎晩そのスクリプトを実行して予測パフォーマンスを評価していました。

私は平日のほとんどが出張でホテル暮らしです。会社から支給されたノート PC の他に、プライベートで使用する Linux をインストールしたノート PC も携行し、仕事から戻って毎晩、株価データを更新、翌日の始値の予測値を算出していました。しばらく続けると当然データが増えてくるので、それだけ毎日の学習データが増えます。計算に時間がかかるようになりましたが、その分、予測トレンドがもっともらしくなってきました。

スクリプトのアプリ化

そろそろ手作業でやっている部分を自動化したいと思うようになり、R の Shiny パッケージを使って Web のアプリ化に取りかかり始めたのが先月のことです。今週一週間、夏休みにすることができたので、じっくり手を入れてひとまず使えそうな UI に仕上げる事ができました(下図)。

 stock-explorer 0.2 の実行例 

簡単な売買シミュレーション

予測パフォーマンスを眺めているだけでは飽きますので、簡単な投資シミュレーションの機能を付けてみました。予測モデルの精度が高いことはもちろん望ましいことですが、それだけでは投資ができないので、次のテーマに進むことにしたわけです。

とりあえず想定上 100 万円を元手に 2019 年 2 月 1 日に対象銘柄の株を 100 株単位で買えるだけ買って、そこから売買のシミュレーションをしています。証券会社に支払う手数料や税金はまだ考慮していません。

現在の売買基準は、予測した次の取引の始値が購入価格より高ければ売却し、本日の始値より安ければ(買えるだけ)購入するという保守的でシンプルなものです。投機的攻めの売買基準ではないので取引頻度は低いのですが、幸い、シミュレーションでは対象としている銘柄はどれも利益を出しています。ただ、手数料などを考慮に入れれば、利益は小遣い程度にしかならないでしょう。いまのところ、実現可能な投資資金をターゲットにして、アルゴリズムに工夫を加えて利益を高めるためのシミュレーションを重ねます。単一銘柄への投資方法がある程度固まったら、(同一の資金で)複数銘柄を扱うシミュレーションができるようにしたいです。

環境の整備

この夏休みに向けて、家庭内 LAN につなげて常時稼動させるサーバーを導入しようとあれこれ品定めをしていたのですが、ぴったりな PC が Amazon.jp にあったので 8 月に入って早々に買ってしまいました [7] [8]。8 月になったら CentOS 8 の RC (Release Cendidate) 版が公開されるだろうと踏んでいて、この新しいサーバーにインストールしようと思っていました。しかし予測が外れてしまったので、現行の CentOS 7 をインストールして使い始めています。

このサーバーの用途のひとつに、本件の株価データ更新分および翌日の始値の予測値の自動算出があります。crontab コマンドで毎日午後 8 時(後に午後 7 時に変更)に更新用のプログラムを起動するように設定して、この一週間(と言っても 4 日しかありませんでしたが)問題なく動作することを確認しました [9]

今後

現在、このアプリ (stock-explore) はバージョン 0.2 です。まだまだ先の話ですが、誰でも使えるように汎用化を進めて、正式リリース (1.0) の目処が立って、需要がありそうだと見込めれば github に公開したいなあと思っています。

参考サイト

  1. 【WSJで学ぶ経済英語】第280回 クオンツ投資 - WSJ
  2. quantmod: Quantitative Financial Modelling Framework
  3. CRAN - Package quantmod
  4. bitWalk's: Rで株価を扱う 〜 quantmod パッケージ [2019-01-16]
  5. bitWalk's: Rで株価を扱う 〜 始値を予測してみた [2019-02-03]
  6. ソフトバンク上場、終値1282円 公開価格を15%下回る:日本経済新聞 [2018-12-19]
  7. bitWalk's: Skynew m2s (1) [2019-08-03]
  8. bitWalk's: Skynew m2s (2) [2019-08-04]
  9. bitWalk's: crontab を使う [2019-08-15]

一番左は、R のパッケージ Shiny について書かれた邦書の入門書が少ない中で書かれた意欲的な著作です。しかし、実際に買って読んでみると Shiny について体系的にまとめるにはページ数が足りなかったという印象を持ちます。もし英文を読むのが苦でなければ、残り三冊の中から何冊かを副読本として読むことをオススメします。

ブログランキング・にほんブログ村へにほんブログ村

2016-11-20

Python と SQLite3

Python では、バージョン 2.5 から標準ライブラリに含まれる sqlite3 モジュールで SQLite のデータベースを利用できるようになっています [1]。Python から SQL 文を実行することで、プログラム的には非常に柔軟にデータベースを操作することができるようになります。

以前、本ブログで sqlite3 のコマンドを紹介しましたが [2]、今回は Python から SQLite のデータベースを操作する例を紹介します。

使用環境は以下のとおりです。

  • OS: Fedora 25 beta (x86_64)
  • Python: python-2.7.12-7.fc25.x86_64

ここで紹介する例は、DBOnlineSQLite入門 › データの追加と削除 › データの更新 [3] で使われている例をベースにして、Python から操作したかのように焼き直しています。あらかじめご了承ください。

データベースの作成とデータの追加

データベースの作成とデータを追加する例を示しました。

# -*- coding: utf-8 -*-
import os
import sqlite3

db_name = "test01.db"
db_file = os.path.join(os.environ['HOME'], "tmp", db_name)
conn = sqlite3.connect(db_file)
cur = conn.cursor()

sql = "create table staff(id integer, name text, unit text, flag text);"
cur.execute(sql)

sql = "insert into staff(id, name, unit, flag) values (?, ?, ?, ?);"
users = [
    (1, 'Satoh', 'Sales', 'Yes'),
    (2, 'Mori', 'Office', 'No'),
    (3, 'Yamada', 'Office', 'Yes'),
    (4, 'Asada', 'Tech', 'Yes'),
    (5, 'Noguchi', 'Sales', 'No')
]
cur.executemany(sql, users)
conn.commit()

sql = "select * from staff;"
for row in cur.execute(sql):
    print(row)

conn.close()

実行例を示します。

(1, u'Satoh', u'Sales', u'Yes')
(2, u'Mori', u'Office', u'No')
(3, u'Yamada', u'Office', u'Yes')
(4, u'Asada', u'Tech', u'Yes')
(5, u'Noguchi', u'Sales', u'No')

データの更新

これは特定のレコードのデータを更新する例です。レコードを決めて更新するのではつまらないので、name フィールドが 'Yamada' であるレコードを探し、'Yamaoka' に変更するようにしました。しかし、合致するレコードは(あるとしても)一つしかないという前提でコーディングしています。

# -*- coding: utf-8 -*-
import os
import sqlite3

db_name = "test01.db"
db_file = os.path.join(os.environ['HOME'], "tmp", db_name)
conn = sqlite3.connect(db_file)
cur = conn.cursor()

user = ('Yamada',)
cur.execute('select * from staff where name = ?', user)
result = cur.fetchone()

if isinstance(result, tuple):
    sql = "update staff set name = ? where id = ?;"
    user_swap = ['Yamaoka', result[0]]
    cur.execute(sql, user_swap)
    conn.commit()

    sql = "select * from staff;"
    for row in cur.execute(sql):
        print(row)
else:
    print result

conn.close()

実行例を示します。

(1, u'Satoh', u'Sales', u'Yes')
(2, u'Mori', u'Office', u'No')
(3, u'Yamaoka', u'Office', u'Yes')
(4, u'Asada', u'Tech', u'Yes')
(5, u'Noguchi', u'Sales', u'No')

Python の sqlite3 モジュールの使い方については、参考サイト [4] に判りやすくまとめられていましたので、参考にさせていただきました。

参考サイト

  1. 11.13. sqlite3 — SQLite データベースに対する DB-API 2.0 インタフェース — Python 2.7.x ドキュメント
  2. bitWalk's: sqlite3 のコマンド [2016-10-02]
  3. データの更新(UPDATE文) - SQLite入門
  4. Pythonでsqlite - Qiita [2016-05-12]

ブログランキング・にほんブログ村へ
にほんブログ村

2016-11-03

【備忘録】sqlite3 で attempt to write a readonly database エラー

Linux で sqlite3 を使っていたらすぐに判ったことだったと思うのですが、Windows 上での操作で出たエラーでしたので、もしやデータベースが壊れたかと焦ってしまいました。

Windows 2008 サーバ上で稼働している、とあるシステムが使っている SQLite のデータベースを一部更新するため、コマンドプロンプト上で sqlite3 を起動して、SQL の update 文を実行した時に下記のエラーが出たのです。

SQL error: attempt to write a readonly database.

Administrator アカウントでインストールしたシステムが使っている SQLite のデータベースを、管理者権限を持つ別のアカウントで操作しようとしたこと、要するに permission が無かったことがエラー発生の原因です。しかしそのことを認識するのに時間がかかってしまいました。

今回は permission をあれこれ変更することせずに Administrator でログインし直すことであっさり問題解消でした。しかし、Administrator アカウントであれこれデータベースを操作するのは安全とは言えません、なんとかしないと…(これは別な問題です)。

SQLite は、データベースがファイルとして存在するので簡単にデータベース環境を構築して利用することができますが、サーバ・クライアントモデルのデータベースに比べると、更新処理の競合の対処については弱い処理系であると言われています。そのため稼働しているシステムのデータベースを手作業で一部更新するときは、いつも怖々と SQL 文を実行しています。しかし、ジャーナリングファイルシステムが備わっているようですから、SQL 文をひとつひとつ手作業で実行するぐらいであれば、競合が原因でデータベースが壊れることを恐れる必要はあまりないのかもしれません [1][2]

参考サイト

  1. SQLite - SQLiteの同時書き込みを解決する現実的な方法は?(5099)|teratail
  2. パフォーマンスを追求するためのSQLite設定 | ITハンドブック

 

ブログランキング・にほんブログ村へ
にほんブログ村

2016-10-02

sqlite3 のコマンド

SQLite は、パブリックドメインの軽量な関係データベース管理システム (RDBMS) です。他の多くの RDBMS がクライアント・サーバシステムで使用されるデータベースエンジンであるのに対し、SQLite はそうはなく、アプリケーションに組み込んで利用されます。Wikipedia の英語サイトの SQLite にある Notable users を見ると、意外と多くのアプリケーション、システムで SQLite が利用されていることに驚かされます。

そんな SQLite を扱う必要が出てきました。Java か Python から SQLite のデータベースへアクセスすることを想定していますが、なにかとデータベースの中身を確認したりする必要が出てくると考えられますので、SQLite というデータベースの特徴がどんなものかをまとめる前に、まずは SQLite に付属している、スタンドアロンのコマンドライン・プログラム sqlite3 で利用できるコマンドを確認しました。

動作環境は次の通りです。

  • OS: Fedora 24 (x86_64)
  • SQLite: sqlite-3.13.0-1.fc24.x86_64

端末エミュレータを起動して sqlite3 と入力して SQLite のコマンドライン・プログラムを実行します。.help を入力すれば用法(のヒント)が表示されるとあるので、その通りにやってみました。

$ sqlite3
SQLite version 3.13.0 2016-05-18 10:57:30
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .help
.auth ON|OFF           Show authorizer callbacks
.backup ?DB? FILE      Backup DB (default "main") to FILE
.bail on|off           Stop after hitting an error.  Default OFF
.binary on|off         Turn binary output on or off.  Default OFF
.changes on|off        Show number of rows changed by SQL
.clone NEWDB           Clone data into NEWDB from the existing database
.databases             List names and files of attached databases
.dbinfo ?DB?           Show status information about the database
...
...

コマンドの一覧がズラッと出てきますが、このままではなんだか使いづらいので、翻訳も兼ねて以下にまとめてみました。ただし、機能がよく判らなかったり確認できないものは無理して翻訳をせずにそのままにしてあります。ただし、後になって確認ができたものは、翻訳を加える可能性がありますので予めご了承ください。

sqlite3 のコマンド
Command Description
.auth ON|OFF Show authorizer callbacks
.backup ?DB? FILE データベース DB(デフォルトは "main")を FILE へバックアップ。
.bail on|off Stop after hitting an error. Default OFF
.binary on|off Turn binary output on or off. Default OFF
.changes on|off Show number of rows changed by SQL
.clone NEWDB Clone data into NEWDB from the existing database
.databases アタッチしているデータベースのテーブル名とファイル名を表示。
.dbinfo ?DB? データベースの現在の情報を表示。
.dump ?TABLE? ... SQL 形式のテキストフォーマットでデータベースをダンプ。TABLE が指定されていれば、TABLE にマッチングするテーブルのみ。
.echo on|off コマンドエコーを ON あるいは OFF へ切り替え。
.eqp on|off|full Enable or disable automatic EXPLAIN QUERY PLAN
.exit 本プログラムを終了。
.explain ?on|off|auto? Turn EXPLAIN output mode on or off or to automatic
.fullschema ?--indent? テーブルのスキーマと sqlite stat table の内容を表示。--indent を追加することで表示を整形。
.headers on|off ヘッダー表示の有無を切り替え。
.help ヘルプを表示。
.import FILE TABLE ファイル FILE のデータをテーブル TABLE にインポート。
.indexes ?TABLE? インデックスの一覧を表示。TABLE が指定されていれば、TABLE にマッチングするテーブルのインデックスのみ。
.limit ?LIMIT? ?VAL? Display or change the value of an SQLITE_LIMIT
.load FILE ?ENTRY? Load an extension library
.log FILE|off Turn logging on or off. FILE can be stderr/stdout
.mode MODE ?TABLE? 出力モードを以下のいずれかの MODE に設定。TABLE が指定されていれば、TABLE にマッチングするテーブルのみ。
ascii Columns/rows delimited by 0x1F and 0x1E
csv Comma-separated values
column Left-aligned columns. (See .width)
html HTML <table> code
insert SQL insert statements for TABLE
line One value per line
list Values delimited by .separator strings
tabs Tab-separated values
tcl TCL list elements
.nullvalue STRING NULL の代わりに STRING を使う。
.once FILENAME Output for the next SQL command only to FILENAME
.open ?FILENAME? 現在のデータベースを閉じて FILENAME を開く。FILENAME の指定が無い場合は、現在のデータベースを閉じるのみ。
.output ?FILENAME? 出力先を FILENAME あるいは標準出力 stdout に設定。
.print STRING... 文字リテラル STRING... を出力。
.prompt MAIN CONTINUE 標準のプロンプトを MAIN CONTINUE へ変更。
.quit 本プログラムを終了。.exit と同じ。
.read FILENAME FILENAME の SQL を実行。
.restore ?DB? FILE バックアップした FILE から DB(デフォルトは "main")へデータベースの内容をレストア(復元)。
.save FILE Write in-memory database into FILE
.scanstats on|off Turn sqlite3_stmt_scanstatus() metrics on or off
.schema ?PATTERN? テーブルのスキーマ(CREATE 文)を表示。PATTERN が指定されていればマッチングした内容のみ。--indent を追加することで表示を整形。
.separator COL ?ROW? 出力モードと .import コマンドにおいて、カラムの区切り文字を COL に、オプションで行(レコード)の区切り文字を ROW へ変更
.shell CMD ARGS... システムシェルのコマンド CMD ARGS... を実行。
.show 様々な設定の現在の値を表示。
.stats ?on|off? Show stats or turn stats on or off
.system CMD ARGS... システムシェルのコマンド CMD ARGS... を実行。.shell コマンドと同じ。
.tables ?TABLE? テーブル名の一覧を表示。TABLE が指定されていれば、TABLE にマッチングするテーブルのみ。
.timeout MS Try opening locked tables for MS milliseconds
.timer on|off Turn SQL timer on or off
.trace FILE|off Output each SQL statement as it is run
.vfsinfo ?AUX? Information about the top-level VFS
.vfslist List all available VFSes
.vfsname ?AUX? Print the name of the VFS stack
.width NUM1 NUM2 ... データを表示する時のカラム毎の幅を設定。負数の場合は右寄せ。

参考サイト

  1. SQLite Home Page
  2. Command Line Shell For SQLite

 

ブログランキング・にほんブログ村へ
にほんブログ村