pathlibやBeautifulSoupでurlの一覧からMarkdown用のリンクを生成するバッチを作った

# pathlibやBeautifulSoupでurlの一覧からMarkdown用のリンクを生成するバッチを作った

## 目的

- ブログやQiita用にMarkdown記法を使っているけど、リンクにタイトル付けるのが面倒くさい
- urlだけでタイトル勝手に取ってくれないかな
    - JavaScriptのブックマークレットなら簡単だろうけど、単体でしか使えないからなあ
    - やりようによっては複数もいけそうですが、その点は有識者にお任せします
- urlだけ1行ずつ書いたファイルを読み込んでMarkdownを吐き出すようにしたい
- 確認とコピペが楽なようにExcel向けのcsvファイル(ShiftJISエンコード)で出力しよう
    - Excelファイルでも良かったんですが、他の環境とかOpenOfficeとかでも対応出来るようにcsvで
    - まあ、そのせいで[以前の記事](https://qiita.com/urahito_solution/items/c37a11ce48291d34d589)でまんまと引っかかったエンコード問題にまた苦戦したんですが[^1]

[^1]: 今回もお世話になりました： [Windows Python3でのUnicodeEncodeErrorの原因と回避方法 - Qiita](https://qiita.com/butada/items/33db39ced989c2ebf644)

## 使ったライブラリ

|ライブラリ名|だいたいの内容|
|:---|:---|
|csv|csvファイルの書き出しを楽にする|
|bs4(BeautifulSoup)|html(xml)のパース（内部的に扱えるデータにすること）に利用|
|requests|urlを渡してhtmlファイルをgetしてくる|
|re|正規表現の処理に使う|
|pathlib|パス周りの処理を扱う。osやos.pathの拡張のようなもの。openメソッドなども持っている|

## 解説だけ（ソースは最後の方にリンクを置きました）

### ワンライナーなメソッドまわり

```Python:method
# 指定エンコードのgetメソッド
def get_enc(mode):
    enc_dic = dict(r='utf-8', w='sjis', p='cp932')
    return enc_dic[mode]

# インラインのfor文リストで除外文字以外を繋ぐ
def remove_str(target, str_list):    
    return ''.join([c for c in target if c not in str_list])

# 指定エンコードでエラー文字以外を再取得する
def ignore_str(target, enc):    
    return target.encode(enc, 'ignore').decode(enc)
```

- get_enc
    - いちいちエンコード指定で入れるのが面倒だったのでdict(辞書型)で定義[^3]
- remove_str
    - target文字列(url)内の文字を```for~in```で取り出し、```if~not in~```で除外リストにない文字を```''.join(~)```でつなげる表現
    - ```text_file.readlines()```で読み込んだ行テキストには改行文字が入るそうなので、除外リストに```['\r', '\n']```を指定して削除
- ignore_str
    - ページタイトルに、ファイルに出力できない文字があった場合は無視して、とりあえず出力できる分だけ出力するために記述
    - encode時に'ignore'（エラーになる文字は無視)指定で何とかなるそう[^5]

[^3]: 参考: [dictの作成は「{}」より「dict()」のほうが簡潔に書ける - Qiita](https://qiita.com/tag1216/items/fd5379ab813111a5e364)

[^5]: Qiitaのどこかでこの書き方を見た気がするのですが、思い出せないので後日思い出せたら追記します

### read_url(urlからtitleを取得し書き出す)
#### 正規表現の処理

```Python:regex
    # 正規表現処理
    rep = r'^https?://[\w/:%#\$&\?\(\)~\.=\+\-]+$'
    isUrl = re.match(rep, url) != None
```

- urlのパターン文字列は色々参考にしました
    - [忘れっぽい人のための正規表現チートシート - Qiita](https://qiita.com/tossh/items/635aea9a529b9deb3038)
    - [正規表現パターン-URL](http://www.megasoft.co.jp/mifes/seiki/s310.html)
    - [正規表現の基本 - Qiita](https://qiita.com/sea_ship/items/7c8811b5cf37d700adc4)

#### ページの取得

```Python:get_url
    # ページの取得
    res = requests.get(url)
    soup = bs4.BeautifulSoup(res.text)
```

- 1行目でhtmlファイルを取得して
- 2行目で内部的にテキストデータとして扱えるようにする（パースする）

#### タイトルの取得から出力まで

```Python:get_title_and_output
    # タイトルの取得とMarkdown用フォーマット
    title = ignore_str(soup.title.string, get_enc('w'))
    markup = '[{}]({})'.format(title, url)

    # csvファイルへ書き出し
    out_writer.writerow([url, title, markup])
```

- ```soup.title.string```でタイトルの中身だけ取ってこれるので利用
    - 取得したタイトルは先に定義したignore_strとget_encで処理
- 変数markupへのフォーマットは、```'[{}]({})'.format(title, url)```だけで済む
    - つまり、title→urlの順に{}で割り当てた位置に代入される
- [以前の記事](https://qiita.com/urahito_solution/items/c37a11ce48291d34d589)より簡単にurl→title→markupの順で書き出し

### mainメソッド

#### pathlibまわり

```Python:main-1
    # 各ディレクトリの取得
    parent_dir = Path(__file__).parent
    org_dir = parent_dir / 'org'
    out_dir = parent_dir / 'out'

    # フォルダが無ければ作成（あってもエラーなし）
    org_dir.mkdir(exist_ok=True)
    out_dir.mkdir(exist_ok=True)

    # 出力ファイルの決定
    out_file = out_dir / 'result.csv'
```

- ```__file__```で.pyファイルのパスを取得し、Path(path).parentで親ディレクトリの取得
- pathlibは結構便利で、```/```演算子がオーバーライド（機能の上書き)されて、```os.path```などPythonで扱う```/```記号で繋げる感覚で使えます[^4]
- ```pathlib.mkdir(exist_ok=True)```の```exist_ok```は、mkdirしたときに既に存在していた場合、エラーとせずに(True)そのままに出来る指定

[^4]: [Python公式ドキュメント（日本語版）の11.1.1.2節参照](https://docs.python.jp/3/library/pathlib.html#operators)

#### 読み込みと出力

```Python:main-2
    # 入力、出力ファイル・ディレクトリの取得
    files = list(org_dir.glob('*.txt'))
    with out_file.open('w', encoding=get_enc('w'), newline='') as out_file_obj:
        # csvファイルのwriterを取得
        out_writer = csv.writer(out_file_obj, dialect="excel")

        # 入力ファイルでfor文を回す
        for file_path in files:
            with Path(file_path).open('r', encoding=get_enc('r')) as read_file_obj:
                # 全行取り込む
                url_list = read_file_obj.readlines()

                # urlの読み込み
                read_url(url_list, out_writer)
```

- 頭の```org_dir.glob('*.txt')```はちゃんと```list()```でリスト化しないとfor文で使えないので注意
- withブロック(```class.method() as instance```)は、C#でいう```using```ブロックに相当します
    - つまり、内部的に```try-finally```ブロックが自動で構成され、finallyブロック内で```instance.close()```が呼ばれるので、エラーが起きた際にも安心
- あとはコメントの通りですが、forブロックをまた外のメソッドに書き出そうかと考えています
    - [The Zen of Python(参考記事)](https://qiita.com/IshitaTakeshi/items/e4145921c8dbf7ba57ef)にも「ネストは浅い方が良い」とありますしね

## 出力結果（兼、参考にした記事等）
|url|title|Markdown link|
|:---|:---|:---|
|https://qiita.com/tag1216/items/29bf3b9c3c4b563ff0fe|Python3.4以降ならos.pathはさっさと捨ててpathlibを使うべき - Qiita|[Python3.4以降ならos.pathはさっさと捨ててpathlibを使うべき - Qiita](https://qiita.com/tag1216/items/29bf3b9c3c4b563ff0fe)|
|https://docs.python.jp/3/library/pathlib.html|11.1. pathlib  §  Python 3.6.4 \|[11.1. pathlib  §  Python 3.6.4 \](https://docs.python.jp/3/library/pathlib.html)|
|https://qiita.com/tossh/items/635aea9a529b9deb3038|忘れっぽい人のための正規表現チートシート - Qiita|[忘れっぽい人のための正規表現チートシート - Qiita](https://qiita.com/tossh/items/635aea9a529b9deb3038)|
|http://www.megasoft.co.jp/mifes/seiki/s310.html|￡¨[^2]|[￡¨](http://www.megasoft.co.jp/mifes/seiki/s310.html)|
|https://qiita.com/meznat/items/a1cc61edb1e340d0b1a2|Pathlibチートシート - Qiita|[Pathlibチートシート - Qiita](https://qiita.com/meznat/items/a1cc61edb1e340d0b1a2)|
|https://qiita.com/Azunyan1111/items/9b3d16428d2bcc7c9406|Python Webスクレイピング 実践入門 - Qiita|[Python Webスクレイピング 実践入門 - Qiita](https://qiita.com/Azunyan1111/items/9b3d16428d2bcc7c9406)|
|https://qiita.com/urahito_solution/items/c37a11ce48291d34d589|PythonでCドライブ内を圧迫しているファイル・フォルダを一覧化する - Qiita|[PythonでCドライブ内を圧迫しているファイル・フォルダを一覧化する - Qiita](https://qiita.com/urahito_solution/items/c37a11ce48291d34d589)|
|https://qiita.com/butada/items/33db39ced989c2ebf644|(Windows) Python3でのUnicodeEncodeErrorの原因と回避方法 - Qiita|[(Windows) Python3でのUnicodeEncodeErrorの原因と回避方法 - Qiita](https://qiita.com/butada/items/33db39ced989c2ebf644)|
|https://qiita.com/tag1216/items/fd5379ab813111a5e364|dictの作成は「{}」より「dict()」のほうが簡潔に書ける - Qiita|[dictの作成は「{}」より「dict()」のほうが簡潔に書ける - Qiita](https://qiita.com/tag1216/items/fd5379ab813111a5e364)|
|https://qiita.com/sea_ship/items/7c8811b5cf37d700adc4|正規表現の基本 - Qiita|[正規表現の基本 - Qiita](https://qiita.com/sea_ship/items/7c8811b5cf37d700adc4)|

[^2]: 正規表現のurlパターンのサンプルです。"￡¨"ってミスってるのは、タイトル取得時に何やら問題があったらしいです（そのうち調べて補足記事立てます）

### 参考書籍（再登場）

- [退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング](https://www.amazon.co.jp/dp/487311778X/ref=cm_sw_r_tw_dp_U_x_SimuAbXBQ3WKF)（オライリー）

## 例のごとくソース全体はこちら

- [GitHub - get_md_link.py(urahito)](https://github.com/urahito/python_proj/blob/master/web/get_md_link.py)

## 今後やりたいこと

- csvからMarkdownの表レイアウト勝手にしてくれるやつ作りたい（エディタでチマチマ置換するのが面倒くさい）