こうかの雑記

こうかの雑記

昔の懐かしいこと、ubuntuのこと、その他いろいろ

pythonでmarkdownファイルに目次を付けたり外したりする

 markdown記法を「はてな」のブログ記事の作成、メモやマニュアルの作成に利用しています。ブログ記事だけであれば問題はないのですが、その他のビューワで見る資料、マニュアルづくりでは残念なことがあります。それは目次の作成です。  markdown記法そのものには目次についての規則が無いようです。

 「はてなブログ」の記事作成では'[:contents]'と書いておくだけで、その箇所に目次が挿入されて表示されます。また、Markdownエディタのtyporaでは'[toc]'と書くだけで、同様に目次が表示されます。

 私はブラウザのFirefoxにアドオンのMarkdown Viewer、Markdown Viewer Webextを追加してブラウドで見ていますが、'[toc]'や'[:contents]'は目次に変換されず、そのまま表示されてしまいます。

 ネット上で探すとマイクロソフトVisual Studio Code拡張機能を使うと簡単にできるという記事を沢山見つけました。しかしVScodeをインストールする気は今のところありません。ubuntuには標準でpython3があるのだから、これを使ってなんとかしようと思いました。拙い技術力しか持ち合わせていませんが作ってみましたので紹介します。

■対象環境

  • ubuntu18.04
     他の環境でも大丈夫だとは思いますが、行の終端は'\n'でやっています。多分Windowsは'\r\n'に変更する必要があるんだろうなと思います。
  • python3.6
  • tkinter  ubuntutkinterがインストールされてない場合は次のコマンドでインストールできます。
$ sudo apt-get -y install python3-tk

■仕様

目次の挿入機能

 オリジナルmdファイルには、目次を表示したい位置に次の行を入れておきます。

[toc]

 出力されるmdファイルには、"[toc]"のある行がカットされて代わりに次のような行が追加されます。

<h2>目次</h2>

  [文法](#anchor0002) <br>
   [コメントの表記](#anchor0003) <br>
  [書き出し](#anchor0004) <br>

 目次部分は以上のように、本文中の見出し部分へのリンクの行になります。  また本文中の見出しの前には次のように"<a"タグが挿入されます。

<a id='anchor0002'></a>
## 文法

 文法説明

<a id='anchor0003'></a>
### コメントの表記

 コメント説明

<a id='anchor0004'></a>
## 書き出し

 書き出し説明

 "a2"タグが要所要所に挿入されます。

 この例の表示は次の様になります。

f:id:koukaforest:20200125160100p:plain

目次の除去機能

 資料に改変を加えると目次の項目も変わるかも知れません。既にある目次関係を取り去ってもう一度目次を作り変えたほうが良いと思うことでしょう。そのために既存の目次関係の行を取り除いて、目次を付ける前の状態に戻します。

 "<h2>目次</h2>"の文字列を"[toc]"に置き換えます。
 次に"#anchor"含む行、すなわち目次を除去します。
 "<a id='anchor'>"の文字列がある行を全て除去します。

■実行

 コマンドラインからパラメータで対象ファイルを指定する方法は採リませんでした。スクリプトを指定する起動させるとファイル選択の画面が出るようにしています。ここはtkinterを使っています。

 ファイルの一覧が表示されるので、そこから対象ファイルを選びます。スクリプトが保存されているフォルダーが表示されますが、フォルダー移動して目的のファイルを探して選択します。

 目次を付けるのか、目次を除去するのかは選択したmdファイルを調べて自動判断されて処理されます。  古いファイルは拡張子に'bak'が追加されて保存されます。

■注意

 目次の除去の際には"<h2>目次</h2>"、"#anchor"、"<a id='anchor'>"の文字列をその他の目的で含む場合、それら文字列が消えてしまいますので注意下さい。

 例えば、この説明記事のmdファイルの様な場合には説明のためにこれらの文字列を含ませていますが、その行が消えてしまい説明にならなくなると思います。

 ご利用は自己責任ということでお願いします。

pythonスクリプト

#!/usr/bin/env python3
"""
mdtoc.py
    markdownファイルに目次を追加する、または除去する。
author  s.k
create date 2020/01/22
"""
import tkinter
import tkinter.filedialog
import tkinter.messagebox
import os

block = False   # md記法で"```"で囲まれているかどうか
seq   = 0       #見出し項目のカウント
"""
    与えられた文字列から見出しレベルと見出し、参照名、行番号を取り出す。
    見出しでなければ (-1, '') を返す
    尚、'```'で挟まれている場合は無視する。
        見出しレベル:#の数
            見出し
            参照名 anckor+連番
            行番号 元ファイル上で何行目かを示す。
"""
def get_header(text):
    global block, seq
    if text[0:3] == '```':
        if block:
            block = False
        else:
            block = True
    if text[0] != '#' :
        return [-1, '', 0]
    if block == False:
        headdic={'#':1, '##':2, '###':3, '####':4, '#####':5, '######':6}
        s      = text.split(' ',1)
        lvl    = headdic[s[0]]
        header = s[1].strip()
        seq = seq + 1
        seqs    = 'anchor' + str(seq).zfill(4)
    else:
        lvl = -1
        header =''
        seqs=''
    return [lvl, header, seqs]

"""
    目次項目を取り出す。
"""
def get_toclist(infile):
    try:
        inf = open(infile, 'r')
    except Exception as err:
        print(infile, 'オープンに失敗しました。')
        return False
    lst=[]
    c = 0
    for l in inf:
        header = get_header(l)
        c = c +1
        header.append(c)
        if header[0] > 0:
            lst.append(header)
    inf.close()
    return lst
"""
    指定行番号( [toc] )の一つ前のレベルを取り出す。
"""
def getstartlevel(lst, line):
    level = -1
    for l in lst:
        if l[3] < line:
            level = l[0]
    return level

def get_anchor(lst, line):
    for l in lst:
        if l[3] == line:
            return l
    return []
"""
    見出しの字下げ空白を作る。受け取るnはレベル
"""
def makindent(n):
    indent=''
    c=0
    while c < n:
        indent = indent + ' '
        c = c + 1
    return indent
"""
    目次部分を出力する。
"""
def put_toc(outf, lst, startlevel):
    outf.write('<h2>目次</h2>\n\n')
    for l in lst:
        if l[0] > startlevel:
            indent = makindent(l[0])
            outl = (indent + '[' + l[1] + '](#' + l[2] + ')  \n')
            try:
                outf.write(outl)
            except Exception as err:
                print('目次の書き込みで書き込み失敗しました。')
                return False

"""
指定ファイルに目次を作る
"""
def mak_toc(infile):
    lst = get_toclist(infile)   # 目次項目のリストを得る
    try:
        inf = open(infile, 'r')
    except Exception as err:
        print(infile, 'オープンに失敗しました。')
        return False
    outfile  = infile + 'work'
    try:
        outf = open(outfile, 'w')
    except Exception as err:
        print(outfile, 'オープンに失敗しました。')
        return False
    startlevel = 1
    lcnt = 0
    for l in inf:
        lcnt = lcnt + 1
        t = l.find('[toc]', 0)  # '[toc]'があるか調べる
        if t != -1:
            # あればその行はコピせずに目次を埋め込む
            startlevel = getstartlevel(lst, lcnt ) 
            put_toc(outf, lst, startlevel)
        else:
            try:
                anchor = get_anchor(lst, lcnt)
                if anchor != []:
                    ancstr = "<a id='" + anchor[2] + "'></a>\n"
                
                    outf.write(ancstr)
                outf.write(l)
            except Exception as err:
                print('目次作成で書き込み失敗しました。')
                return False
    inf.close()
    outf.close()
    sw = 0
    bakfile = infile + 'bak'
    try:
        os.rename(infile, bakfile)
    except Exception as err:
        print('元ファイルrename失敗しました。', infile)
        sw = 1
    try:
        os.rename(outfile, infile)
    except Exception as err:
        print('仮ファイルrename失敗しました。', outfile)
        sw = sw +1
    if sw == 0:
        print('目次付け終りました。')
        return True
    return False

"""
指定ファイルから目次に関する行を取り除く
"""    
def rm_toc(infile):

    try:
        inf = open(infile, 'r')
    except Exception as err:
        print(infile, 'オープンに失敗しました。')
        return False 
    outfile= infile + 'wrk'
    try:
        outf = open(outfile, 'w')
    except Exception as err:
        print(outfile, 'オープンに失敗しました。')
        return False 
    for l in inf:
        p = l.find('<h2>目次</h2>', 0)
        if p != -1:
            try:
                outf.write('[toc]')
            except Exception as err:
                print('目次取り除きで書き込み失敗しました。')
                return False
            # 目次部分カット
            continue

        p = l.find("](#anchor",0)   # 目次部分?
        if p == -1:
            p = l.find("<a id='anchor", 0) # 見出しのアンカー?
            if p == -1:
                # いずれでもないので出力
                try:
                    outf.write(l )
                except Exception as err:
                    print('目次取り除きで書き込み失敗しました。')
                    return False
            else:
                pass    # 目次またはアンカーなので無視
    inf.close()
    outf.close()
    sw = 0
    bakfile = infile + 'bak'
    try:
        os.rename(infile, bakfile)
    except Exception as err:
        print('元ファイルrename失敗しました。', infile)
        sw = 1
    try:
        os.rename(outfile, infile)
    except Exception as err:
        print('仮ファイルrename失敗しました。', outfile)
        sw = sw +1
    if sw == 0:
        print('目次除去終りました。')
        return True
    return False

"""
    指定ファイルに'[toc]'と目次のいずれがあるかを確認する。
    結果は'[toc]'があれば'mk'を、目次があれば'rm'を返す。
"""
def checkread(infile):
    try:
        inf = open(infile, 'r')
    except IOError as err:
        print(infile, '確認オープンに失敗しました。')
        return 'error' 
    toc = False
    mokuji = False
    for l in inf:
        p = l.find('[toc]',0)
        if p != -1:
            toc = True
        p = l.find('<h2>目次</h2>')
        if p != -1:
            mokuji = True
    inf.close()
    if toc:
        return 'mak'
    if mokuji:
        return 'rm'
    return 'error'

def main_rtn():
    root = tkinter.Tk()
    root.withdraw()     # 小さなウインドウが出るのを止める
    tkinter.messagebox.showinfo('mdtoc.py','対象の.mdファイルを選択して下さい。')
    fname = tkinter.filedialog.askopenfilename(title='mdtoc.py')
    root.destroy()
    # fname = "/home/sk/デスクトップ/pythonmemo.md"
    syori = checkread(fname)

    if syori == 'mak':
        print('目次を作成')
        mak_toc(fname)
    else:
        print('目次を除去')
        rm_toc(fname)

if __name__ == '__main__':
    main_rtn()

 何か問題があれば、教えて下さい。  メールはこちら https://fm.sekkaku.net/mail/1201407490/

e-TAXで確定申告はこんな感じでできます

 今年の確定申告をe-TAXで送信するつもりをしています。 昨年はどうしたかというと、e-TAXで申告書を作りましたが、実際には確定申告会場まで出向いて、そこで入力、送信しました。その際にIDとパスワードを登録しました。 なので今年からは自分で出来るのです。

 e-TAXでの確定申告に関心をお持ち方で、躊躇しておられる方、多いのではないでしょうか? そんな方のために少し紹介させて頂きます。

 私は公的年金のみの所得で給与所得も雑収入も何もない状態なので、一番単純な例だと思います。その一番単純な例でどんな感じか紹介します。 多分、その他所得が有る場合も、さほど違い無いようにも思いますが、参考になるかと思います。 前回は退職した年で、給与所得と年金があるパターンを経験しましたが、もう忘れました。

 そうではなくて操作が不安という方は、確定申告会場では自分の手でPC入力に挑戦してみて下さい。一度体験してみると良いと思います。(多分、並ぶ時間が短くて済むと思いますし、分からない時には助けて貰えます。)

入力方法の選択

 給与・年金の方(給与・年金専用)左記以外の所得のある方(全ての所得対応)左のボタン選択がお分かりにならない方の3種類から選択しますが、今回は年金のみなので最初の給与・年金の方(給与・年金専用)にある作成開始ボタンをクリックします。

申告書の作成をはじめる前に

 事前に用意して机上に置いておくとスムーズに進められます。 申告会場に行く場合にも必要ですね。

  など

  • 所得控除に関する書類
    • 医療費の領収書
    • 生命保険料控除証明書
    • 地震保険料控除証明書
    • 寄附した団体などから交付を受けた寄附金の受領証
    • 社会保険料納付証明書(申告用) 市役所発行

  など

  • その他の準備
    • マイナンバーの番号 マイナンバーカードでの申告でなくても記載に必要です。
    • 配偶者がいる場合は配偶者のマイナンバーの番号、生年月日も必要。

提出方法の選択等

  • 作成する確定申告書の提出方法

     次のいずれかを選択します。後で変更出来ます。

    • e-Taxにより税務署へ提出
    • 確定申告書を印刷して税務署へ提出
  • 申告される方の生年月日

   申告書等への表示や控除額の計算に使われます。

所得の種類選択

 前年分の所得の種類について、いずれかを選択します。

所得の種類  説明 
給与のみ ※ 「給与所得の源泉徴収票」をお持ちの方、※ サラリーマンの方や、パート又はアルバイトによる所得のある方 など
年金のみ ※ 「公的年金等の源泉徴収票」をお持ちの方、※ 保険会社から送付された個人年金に係る「年金支払証明書」等をお持ちの方 など
給与と年金の両方 ※ 上記の両方に該当する方

 今回は年金のみを選択します。

 次に年金の種類の選択になりますが、公的年金のみ個人年金(公的年金等以外の年金)のみ公的年金と個人年金の両方の3種類あるうち公的年金のみを選びます。

 多分、どれを選んでも入力出来る欄が違うだけの同じ画面になるのだろうと思います。
 今回は公的年金の行のみが可能になっています。
 入力は源泉徴収票のどの部分を見るか図で示してくれているので迷うことはないです。

f:id:koukaforest:20200119202635p:plain

 基金などの企業年金がある場合はここで忘れないで入力します。

所得控除の入力

 ここは人によって異なると思います。私の場合は社会保険料控除生命保険料控除地震保険料控除配偶者(特別)控除がありました。
 配偶者がおられる場合は配偶者(特別)控除を忘れないにしましょう。

f:id:koukaforest:20200119202831p:plain

 この辺は会社に提出する年末調整の資料を書いていたよりも簡単です。

税額控除等の入力

 ここも人によって異なると思いますが、あまり関係はないと思います。災害減免を受ける場合はここで入力します。その場合は「り災証明書」等が必要になります。

計算結果の確認

 ここまでの操作で還付される金額が表示されます。申告書の第一表イメージを画面で確認できますので、前年の申告書と比較して項目のモレがないか確認できます。間違いが見つかれば修正出来ますので安心です。

住民税等に関する事項の入力

 16歳未満の扶養親族の有無、別居の控除対象配偶者・控除対象扶養親族の有無の2項目で有無を選択します。

 この後、還付金の受取方法(金融機関、口座の指定)、住所氏名等、納税地情報の確認等をします。
 配偶者と合わせてマイナンバーの入力をします。

おしまい

 後は申告書の送信・印刷になると思います。まだ必要書類が一つ足りないので完成してないのですよ。

 尚、送信の際は申告書控えを印刷して、入力の元となった資料と合わせて5年間保管するようにしましょう。 印刷したものを送る場合は、添付して送ることになると思います。

 さて、e-TAX、使ってみる気になりましたでしょうか?

 マイナンバーカードを使う場合は、カードリーダを用意しなくてはなりませんのでID登録してパスワードを使うのが良いと思います。ただ、この場合は税務署に出向いて登録しなくてはなりません。 私は昨年の確定申告会場で行いました。

 と書いたところでやまもさんの記事「SONY製ICカードリーダライタ RC-S330 を e-Tax で利用する」のコメントでパスワード方式が暫定だとありました。いずれマイナンバーカードのみになる方向らしいですね。ということはマイナンバーカードに対応したスマートフォンにしろということなのかな?  2019年1月からおおむね3年間の暫定期間らしいです。 その時はlinuxでは対応できないと思うので印刷して提出することになるのかな。

ブログのスクレイピングの悩ましさ

 はてなブログの読者一覧でスクレイピングして読者のブログを訪問するようにしていますが、今朝のスクレイピングでエラーになるブログがありました。

 スクリプトにエラー処理を書き加えて問題のブログを特定し、ブラウザで訪問してみると、どうもデザインを一新されたようです。 そのデザインでの構成が今までにないパターンなので上手く項目を取り出すことが出来なかったというのが原因です。

 そもそもスクレイピングは相手側に断り無く想定外の処理をしているわけですから、何時デザインが変わるか知れたものではありません。 なのでいつ使えなくなってもおかしくない訳です。 でも、今朝はその一件のために全てのブログデータを出力出来なかったのは残念過ぎました。

 今までより効率よく読者のブログを巡回できるようになっていたので簡単に諦め辛いものが有ります。今回はエラーになるブログを無視する形で対応しました。

 新しいパターンに遭遇する度に対応すればよいのですが、その新しいブログのhtml構造を確認するだけでも結構負担になりますし、あまりに条件をいじくりまわしていると既に上手く行っているブログの条件にも悪影響が出る恐れもあります。悩ましいところです。

 正直言って、html文の階層構造をわかりやすく直す作業が面倒すぎなんです……。