turtlechanのブログ

無知の私がLinuxをいじりながら書いていくブログ

【Linux】一般ユーザーで NTFS をマウントできるようにする

ラズパイ用。Sambaで使用するための設定メモ。

はじめに

通常は一般ユーザーでマウントする場合は、'/etc/fstab' に設定を記述しますが、NTFS だとパーミッションがどうだので怒られる。

ちなみに exFAT とは違ってパーミッションはSUIDに設定されている(はず)。

$ ls -l /bin/ntfs-3g
-rwsr-xr-x 1 root root 112456  3月 15  2019 /bin/ntfs-3g

設定

多分以下のことをすれば一般ユーザーでマウントできるようになったはず。
※確認するのが面倒なので確かめてはいないです。

ユーザーを disk グループに所属させる

以下のコマンド。

$ sudo gpasswd -a ユーザー名 disk

適応するために一旦ログアウト。

$ exit

ログインして以下のコマンドで確認。

$ groups
turtlepi adm disk dialout cdrom sudo audio video plugdev games users input netdev gpio i2c spi

'disk' という文字があればオッケー。'turtlepi' は私のユーザー名です。

マウント先のパーミッションを変更

マウント先は '/mnt/hdd/naspi' と想定しています。
各自読み替えて下さい。

$ sudo chmod 777 /mnt/hdd/naspi

/etc/fstab にマウント情報を記入

Samba を想定しているので、再起動ごとにデバイスファイルが変わると困るので UUID で指定します。

以下のコマンドで、マウントしたいデバイスのUUIDを確認。

$ sudo blkid
/dev/mmcblk0p1: LABEL="boot" UUID="16D2-035F" TYPE="vfat" PARTUUID="eff4f6a1-01"
/dev/mmcblk0p2: LABEL="rootfs" UUID="d065e631-6b9d-48c0-a8fe-e663b42828e0" TYPE="ext4" PARTUUID="eff4f6a1-02"
/dev/sda1: UUID="4DB262664BB9FD7B" TYPE="ntfs" PTTYPE="dos"
/dev/mmcblk0: PTUUID="eff4f6a1" PTTYPE="dos"

赤くしたやつが今回マウントしたいデバイス(外付けHDD)。

テキストエディタ等で /etc/fstab を編集。

$ sudo vi /etc/fstab

私の場合は、以下を記入した。

UUID=4DB262664BB9FD7B    /mnt/hdd/naspi    ntfs-3g   user,async,noauto,exec,gid=65534,rw,uid=65534,umask=000    0    0

上書きしたらオッケー。

マウントしてみる

マウントできるか確認してみます。
コマンドは以下。'$ mount マウント先のディレクトリ' です。

$ mount /mnt/hdd/naspi

エラーが出なければ正常にマウントされているはずです。
df コマンドで確かめてみてもいいです。

余談

crontab で起動時にマウント

fstab に auto を指定すると、OS起動時にそのデバイスが接続されていないと起動しなくなります。接続してあげればちゃんと起動します。でも、少々不便です。

一般ユーザーでマウントできるように設定しているなら crontab で起動時にマウントするようにしておくのが私的にオススメ。

 

fstab のオプションを noauto にしておく。

そして普段使う一般ユーザーで

$ crontab -e

して、以下を記述。

@reboot mount マウント先のディレクトリ

これで起動時に一般ユーザーでマウントされ、デバイスが接続されていなかった場合でもOSは起動します。

exFAT を一般ユーザーでマウント

exFAT も fstab に記述しただけでは、一般ユーザーでマウントできません。
exfat-fuse はデフォでSUIDになっていないのが原因。

以下のコマンドでexfat-fuseを探します。

$ which mount.exfat
/sbin/mount.exfat
$ ls -l /sbin/mount.exfat
lrwxrwxrwx 1 root root 16  1月 20  2017 /sbin/mount.exfat -> mount.exfat-fuse
$ ls -l /sbin/mount.exfat-fuse 
-rwxr-xr-x 1 root root 46724  1月 20  2017 /sbin/mount.exfat-fuse

/sbin/mount.exfat-fuse が実体です。
以下のコマンドでSUIDにします。

$ sudo chmod 4755 /sbin/mount.exfat-fuse

以下のコマンドで確認。

ls -l /sbin/mount.exfat-fuse 
-rwsr-xr-x 1 root root 46724  1月 20  2017 /sbin/mount.exfat-fuse

-rwsr-xr-x になっていればオッケーです。

おわりに

何かの参考になれば幸いです。

 

Alpha Chart の株価更新に無尽蔵の日足データを使いたい

Alpha Chart の試用期間が終わるとデータ更新が「ダウンロード済みデータで更新」しかなくなります。
株価データ倉庫 の日足データが使えるが、更新が週一(日曜)なため毎日更新できない。
そこで、毎日更新している 無尽蔵 のデータを使えればいいなという話。

 

先に今回作ったやつ載せておきます。
ファイル: turtlechan_apcconv_v1.zip
解凍して '$ python apcconv.py' で起動。無尽蔵 の zipファイル を指定すればいいです。

起動画面

※ Python2 でも 3 でも動くはず。

はじめに

Alpha Chart で読み込める 株価データ倉庫 のtxtファイルを見てみたいと思います。
2020/01/06 のデータを参考にしています。

株価データ倉庫

ファイル名は 'y200106.txt' となっている。'y%y%m%d.txt' のようです。

中身はtsv形式のようです。タブで要素が区切られているやつですね。
一行目が、'20200106' なので '%Y%m%d'
二行目以降は

銘柄コード\t銘柄名\t始値\t高値\t安値\t終値\t出来高\r\n

 

以上の形式が Alpha Chart で読み込めるので、無尽蔵のデータをこのように整形すれば読み込める。
ついでに、無尽蔵 のcsvファイルも見ておきます。

 

無尽蔵

ファイル名は 'T200106.csv' なので 'T%y%m%d.csv'。

中身はcsv形式。カンマで要素が区切られているやつ。
一行目から以下の形式。

日付,銘柄コード,取引所別の数字,銘柄コード 銘柄名,始値,高値,安値,終値,出来高,取引所名\r\n

 

これを整形すれば良いのです。ちょろいですね。
無尽蔵の方には先物とか含まれていないけど、ないものはしかたないので無視。

今回作ったやつ

apcconv.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import zipfile
# Python のバージョンによって tk の分岐
if sys.version_info.major == 2:
    import Tkinter as tk
    import tkFileDialog as tkfd
elif sys.version_info.major == 3:
    import tkinter as tk
    import tkinter.filedialog as tkfd


class Converter(tk.Frame):

    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.tk.call('wm', 'iconphoto', master._w, tk.PhotoImage(file='favicon.gif'))
        master.title('Turtlechan apcConv(仮)')
        master.geometry('+50+50')
        self.pack()
        self.create_widget()

    def create_widget(self):
        # ラベル
        self.lbl = tk.Label(self, text='入力:')
        self.strv_status = tk.StringVar()
        self.lbl_status = tk.Label(self, fg='#888', bd=1, relief=tk.SUNKEN, anchor=tk.E, textvariable=self.strv_status)
        # テキストボックス
        self.strv = tk.StringVar()
        self.ent = tk.Entry(self, width=60, textvariable=self.strv)
        self.strv.set(os.path.abspath(os.path.dirname(__file__)))
        # ボタン
        self.btn_fd = tk.Button(self, text='ファイル選択', command=self.push_fd)
        self.btn_conv = tk.Button(self, text='Convert', command=self.push_conv)
        # 配置
        self.lbl.grid(row=0, column=0)
        self.ent.grid(row=0, column=1)
        self.btn_fd.grid(row=0, column=2)
        self.btn_conv.grid(row=100, column=1)
        self.lbl_status.grid(row=102, column=0, columnspan=3, sticky=tk.W + tk.E + tk.N + tk.S)

    def push_fd(self):
        # ファイル選択ウィンドウ
        self.fp = tkfd.askopenfilename(filetypes=[('ZIPアーカイブ', '*.zip')])
        if len(self.fp) == 0:
            return
        else:
            self.strv.set(self.fp)

    def push_conv(self):
        # zipアーカイブの読み込み
        with zipfile.ZipFile(self.strv.get(), 'r') as zf:
            # Python のバージョンによって分岐
            data = [row.decode('shift-jis').split(',') if sys.version_info.major == 3 else row.split(',') for row in zf.open(zf.namelist()[0])]
        data = ['{0}\t{1}\t{2[0]}\t{2[1]}\t{2[2]}\t{2[3]}\t{3}'.format(row[1], row[3][4:].replace(' ', ''), row[4:8], float(row[8]) / 10000000 if int(row[1]) < 1300 else float(row[8]) / 1000) for row in data if row[8] != '0']
        # 書き出し
        with open(os.path.join(os.path.dirname(self.strv.get()), os.path.basename(self.strv.get()).split('.')[0].replace('T', 'y') + '.txt'), 'w') as f:
            f.write(os.path.basename(self.strv.get()).split('.')[0].replace('T', '20') + '\n')
            f.write('\r\n'.join(data))
        self.strv_status.set(self.strv.get() + '\t変換完了')


def main():
    root = tk.Tk()
    converter = Converter(root)
    converter.mainloop()


if __name__ == '__main__':
    main()

おわりに

Python3 で作ったファイルは Alpha Chart に読み込ませるとき銘柄名が文字化けします。
更新自体に問題はなさそうです。Python3 の文字列の扱い分からないんだよなぁ。。。

 

何かの参考になれば幸いです。

株価データをダウンロードする Pythonスクリプト 書いたよ!

今年の GW ももう終わりですね。
時間があったので、株価データをダウンロードする Pythonスクリプト 書いてみました。
私はまともに GUI を作ったことがなかったので、苦戦しましたがとりあえず出来たのでここで紹介させて下さい。

※ Python2 で書いています。

はじめに

Python2 で書いているので Python3 で動かすには一部書き直す必要があります。
Linux でしか動作確認していません。そしてデバッグしていないので不具合がある可能性が高いです。

依存モジュール(サードパーティ)は以下。

requests は pip でも使ってインストールして下さい。
Tkinter は 'apt-get install tk-dev' とかでインストール出来たかと思います。

作ったやつ

ファイル: turtlechan_price_dl_v1.zip

もし興味のある人がいればダウンロードして使ってみて下さい。
zip で圧縮しているので解凍して使ってください。
解凍先に移動して以下のコマンドで起動できるはずです。

$ python downloader.py

とりあえず GUI は以下。

起動時

ウィンドウ01

チェックボックスにチェックを入れて「Download」を押すと確認ダイアログが表示される。
OK でダウンロードが開始される。

ウィンドウ02

ダウンロード中は赤線引いたところが更新されていくので動いているのが確認できると思う。

ウィンドウ03

保存先は指定できません、downloader.py と同じディレクトリに mujinzouディレクトリ等を作成して、その中に保存する感じです。
すでにダウンロードファイルが存在していた場合にはダウンロード処理はパスします。

営業日の判定は workday_txt ディレクトリ内にあるテキストファイルを読み込んで判定しています。

wokday_txt に置いておきます。
turtlechan_price_dl_v1.zip には含めてあるのでわざわざダウンロードする必要はないです。

ソースコード

誰もダウンロードして中を覗いてくれないと寂しいので、ソースコードを書いておきます。

workday_txt

workday_txt ディレクトリ内のファイルは営業日が書かれているだけ。
以下に例を書いておく、ファイル名は '西暦.txt' である必要がある。

2000.txt
2000-01-04
2000-01-05
2000-01-06
2000-01-07
2000-01-11
~ 省略 ~
2000-12-26
2000-12-27
2000-12-28
2000-12-29

Python スクリプト

dataservice.py は、各サイトのダウンロードURLとかを生成してもらうためのもの。
ちなみに株価データ倉庫の zip ファイル名の先頭文字が 'd' のやつには対応できてない。

dataservice.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import os
import datetime
import zipfile


class Mujinzou(object):
    NAME = 'mujinzou'
    DOMAIN = 'http://mujinzou.com'
    EXTEND = 'zip'
    ARCHIVE = 'T{0:%y%m%d}.{1}'
    PATH = '/k_data/{0:%Y}/{0:%y}_{0:%m}/{1}'
    DIR = os.path.join(os.path.dirname(__file__), NAME)

    def __init__(self, **kwargs):
        if 'date' in kwargs:
            self.date = kwargs['date']
            extend = self.__class__.EXTEND if self.date.year > 2014 else 'lzh'  # 2014年までlzh形式
            self.archive = self.__class__.ARCHIVE.format(self.date, extend)
        elif 'name' in kwargs:
            fn = kwargs['name'].split('.')
            self.date = datetime.datetime.strptime(fn[0][1:], '%y%m%d').date()
            extend = fn[-1]
            self.archive = kwargs['name']
        self.path = self.__class__.PATH.format(self.date, self.archive)
        self.domain = self.__class__.DOMAIN if self.date.year > 2018 else 'http://souba-data.com'  # 2019年からドメインが変更

    @property
    def url(self):
        return self.domain + self.path

    def exists(self):
        return os.path.isfile(os.path.join(self.__class__.DIR, self.archive))

    def load(self):
        with zipfile.ZipFile(os.path.join(self.__class__.DIR, self.archive), 'r') as zf:
            data = [row.split(',') for row in zf.open(zf.namelist()[0])]
        return {row[1]: tuple(row[4:9]) for row in data}


class Stock_databox(object):
    NAME = 'stock-databox'
    DOMAIN = 'http://stock-databox.net'
    EXTEND = 'zip'
    ARCHIVE = 'y{0:%y%m%d}.{1}'
    PATH = '/{1}'
    DIR = os.path.join(os.path.dirname(__file__), NAME)

    def __init__(self, **kwargs):
        if 'date' in kwargs:
            self.date = kwargs['date']
            self.archive = self.__class__.ARCHIVE.format(self.date, self.__class__.EXTEND)
        elif 'name' in kwargs:
            fn = kwargs['name'].split('.')
            self.date = datetime.datetime.strptime(fn[0][1:], '%y%m%d').date()
            self.archive = kwargs['name']
        self.path = self.__class__.PATH.format(self.date, self.archive)

    @property
    def url(self):
        return self.__class__.DOMAIN + self.path

    def exists(self):
        return os.path.isfile(os.path.join(self.__class__.DIR, self.archive))

    def load(self):
        with zipfile.ZipFile(os.path.join(self.__class__.DIR, self.archive), 'r') as zf:
            data = [row.strip().split('\t') for row in zf.open(zf.namelist()[0])]
        return {row[0]: (row[2], row[3], row[4], row[5], '{0:.0f}'.format(float(row[6])*100)) for row in data[1:]}

downloader.py は、営業日の確認処理担当の Workdayクラス、GUI生成とダウンロード処理担当の Downloaderクラス。

downloader.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import os
from time import sleep
import datetime
import threading
import requests
import Tkinter as tk
import tkMessageBox as tkmsg
from dataservice import Mujinzou, Stock_databox


SAVE_DIR = os.path.dirname(__file__)  # 保存先のディレクトリ
WAIT_TIME = 1  # サーバーへの負荷を考慮
USER_AGENT = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'}  # User-Agent 多分なんでもいい


class Downloader(tk.Frame):

    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.tk.call('wm', 'iconphoto', master._w, tk.PhotoImage(file='favicon.gif'))
        master.title('Turtlechan Price Downloader(仮)')
        master.geometry('200x200+50+50')
        master.minsize(width=200, height=200)
        master.maxsize(width=200, height=200)
        self.pack()
        self.create_widget()
        self.ssn = requests.Session()
        self.ssn.headers.update(USER_AGENT)

    def create_widget(self):
        # ラベル
        self.lbl_start = tk.Label(self, text='開始日')
        self.lbl_end = tk.Label(self, text='終了日')
        self.strv_status = tk.StringVar()
        self.lbl_status = tk.Label(self, text='取得先を選択してください。', fg='#888', bd=1, relief=tk.SUNKEN, anchor=tk.E, textvariable=self.strv_status)
        # テキストボックス
        self.ent_start = tk.Entry(self)
        self.ent_end = tk.Entry(self)
        self.ent_start.insert(tk.END, str(datetime.date.today() - datetime.timedelta(days=20)))
        self.ent_end.insert(tk.END, str(datetime.date.today()))
        # チェックボックス
        self.frame_service = tk.LabelFrame(self, text='取得先')
        self.boolv_muji = tk.IntVar()
        self.boolv_stkdb = tk.IntVar()
        self.boolv_muji.set(0)
        self.boolv_stkdb.set(0)
        self.chk_muji = tk.Checkbutton(self.frame_service, text='無尽蔵', variable=self.boolv_muji)
        self.chk_stkdb = tk.Checkbutton(self.frame_service, text='株価データ倉庫', variable=self.boolv_stkdb)
        self.chk_muji.grid(row=0, column=0)
        self.chk_stkdb.grid(row=0, column=1)
        # ボタン
        self.btn_quit = tk.Button(self, text='Quit', command=self.quit)
        self.btn_dl = tk.Button(self, text='Download', command=self.push_dl)
        # 配置
        self.lbl_start.grid(row=0, column=0)
        self.ent_start.grid(row=0, column=1)
        self.ent_end.grid(row=1, column=1)
        self.lbl_end.grid(row=1, column=0)
        self.frame_service.grid(row=2, column=0, pady=10, columnspan=2)
        self.btn_dl.grid(row=100, column=1, sticky=tk.E)
        self.btn_quit.grid(row=101, column=1, sticky=tk.E)
        self.lbl_status.grid(row=102, column=0, columnspan=2, pady=5, sticky=tk.W + tk.E + tk.N + tk.S)

    def __dl(self, Dataservice_obj):
        if Dataservice_obj.exists():
            return 0
        if not os.path.isdir(Dataservice_obj.DIR):
            os.makedirs(Dataservice_obj.DIR)
        ds = Dataservice_obj
        res = self.ssn.get(ds.url)
        try:  # ダウンロード先が存在しなかった場合
            res.raise_for_status()
        except requests.exceptions.HTTPError:
            self.strv_status.set('{0} は存在しないみたい。'.format(ds.url))
            return 0
        self.strv_status.set('Downloading...\t{0} '.format(Dataservice_obj.archive))
        with open(os.path.join(ds.DIR, ds.archive), 'wb') as f:
            f.write(res.content)
        return 1

    def __dl_loop(self, start, end, muji, stkdb):
        ''' 並列処理のために作った関数、他にいい方法ないの? '''
        workday = Workday()
        wait = float(WAIT_TIME) / (muji + stkdb)
        for date in workday.xrange(start, end):
            tmp = 0
            tmp += self.__dl(Mujinzou(date=date)) if muji else 0
            tmp += self.__dl(Stock_databox(date=date)) if stkdb else 0
            sleep(wait * tmp)
        self.strv_status.set('ダウンロード完了。')

    def push_dl(self):
        start = datetime.datetime.strptime(self.ent_start.get(), '%Y-%m-%d').date()
        end = datetime.datetime.strptime(self.ent_end.get(), '%Y-%m-%d').date()
        muji = self.boolv_muji.get()
        stkdb = self.boolv_stkdb.get()
        if not muji and not stkdb:  # チェックボックスがからの場合何もしない
            return
        if not tkmsg.askokcancel('Download', '{0} 〜 {1}\nダウンロードを開始します。'.format(str(start), str(end))):  # 確認ダイアログ
            return
        # 時間の掛かる処理は並列処理しないとウィンドウが更新されない
        thread = threading.Thread(target=self.__dl_loop, args=(start, end, muji, stkdb))
        thread.start()


class Workday(object):
    TXT_DIR = os.path.join(os.path.dirname(__file__), 'workday_txt')

    def __init__(self):
        self.wd = set()
        for txtfile in os.listdir(self.__class__.TXT_DIR):
            with open(os.path.join(self.__class__.TXT_DIR, txtfile), 'rb') as f:
                self.wd.update(row.strip() for row in f.readlines())

    def work(self, Date_obj):
        return str(Date_obj) in self.wd

    def near(self, Date_obj, old=True):
        dt = Date_obj
        if not self.work(dt):
            while True:
                dt = dt - datetime.timedelta(days=1) if old else dt + datetime.timedelta(days=1)
                if self.work(dt):
                    break
        return dt

    def xrange(self, start, end):
        wd_sorted = sorted(self.wd)
        return (datetime.datetime.strptime(date, '%Y-%m-%d').date() for date in wd_sorted[wd_sorted.index(str(self.near(start, old=False))): wd_sorted.index(str(self.near(end, old=True))) + 1])


def main():
    root = tk.Tk()
    dlr = Downloader(root)
    dlr.mainloop()


if __name__ == '__main__':
    main()

おわりに

ソースコード見たけど、その書き方おかしい」とかあったら教えてください。
また、「使ってみたよ」とか「とりあえず読んだ」、「動かない」とか何でもいいんで教えてくださるとうれしいです。
Pythonプログラミング勉強中なので、誰かの干渉があると励みになります。よろしくお願いします。

何かの参考になれば幸いです。