pythonで青空文庫にある夏目漱石の書籍をスクレイピングする


# はじめに
[TechCommit Advent Calendar 2021](https://qiita.com/advent-calendar/2021/tech-commit) 3日目を担当しますRyotaです！
よろしくお願いいたします。

pythonで機械学習を勉強するため、書籍「Pythonによるスクレイピング＆機械学習 開発テクニック 」に沿って学習していました。
自然言語処理の章で学習用のデータを収集するため、青空文庫から夏目漱石などの作家の作品を一括ダウンロードする必要がありました。
一括ダウンロードサイトとして「http://keison.sakura.ne.jp/」
からダウンロードするようにと書かれていましたが、現在はそのサイトは無い様子。

そこで、自分で青空文庫から夏目漱石の書籍をダウンロードして展開するスクリプトを作成しましたので、参考になればと思い記載させていただきます。

# 注意事項
青空文庫のUIが変わると、正常にスクレイピングができなくなります。

# 環境
~~~
    windows 10
    Python 3.10.1
    bs4 0.0.1
    beautifulsoup4 4.10.0
~~~

# 前提知識
- pythonの基本的知識
- html/CSSの基本的知識

# まずはコード全部

ちょっと冗長な部分もありますが大目に見てください

```python:get_aozora.py
import urllib.request as req
import urllib
import re
from bs4 import BeautifulSoup
import time
import os
import zipfile


#青空文庫 夏目漱石(person148)リストURL
url = "https://www.aozora.gr.jp/index_pages/person148.html#sakuhin_list_1"

card_nums =[]

save_folder = ".//作品//夏目漱石//"

if not os.path.isdir(save_folder):
    os.makedirs(save_folder)

def get_card_num():
    print(url)
    try:
        res = req.urlopen(url)
    except urllib.error.HTTPError as e:
        print('raise HTTPError')
        print(e.reason)
    except urllib.error.URLError as e:
        print('raise URLError')
        print(e.reason)
    else:
        print(res.status)

    soup = BeautifulSoup(res, "html.parser")

    first_ol_element = soup.find('ol')

    for element in first_ol_element.find_all('li'): 
        sakuhin  = element.text
        #print(sakuhin)
        title_name = re.search(r'(.*)(（)',sakuhin)
        #print(title_name.group(1))
        card_num =re.search(r'作品ID：([\d:]+)',sakuhin)
        #print(card_num.group(1))
        card_nums.append(card_num.group(1))

get_card_num()

file_names = []

for card_num in card_nums:
    print(card_num)
    card_url = "https://www.aozora.gr.jp/cards/000148/card" + str(card_num) + ".html"
    print(card_url)
    try:
        res = req.urlopen(card_url)
    except urllib.error.HTTPError as e:
        print('raise HTTPError')
        print(e.reason)
        continue
    except urllib.error.URLError as e:
        print('rase URLError')
        print(e.reason)
        continue
    else:
        print(res.status)

    soup = BeautifulSoup(res, "html.parser")

    download_table = soup.find('table', {'class':'download'})
    #print(download_table)
    download_path = download_table.select_one("tr:nth-of-type(2) td:nth-of-type(3)").get_text()
    #print(download_path)
    file_names.append(download_path)
    time.sleep(1)

save_folder = ".//作品//夏目漱石//"

download_folder = save_folder + "zip//"
if not os.path.isdir(download_folder):
    os.makedirs(download_folder)

for file_name in file_names:
    print(file_name)
    download_file_url = "https://www.aozora.gr.jp/cards/000148/files/" + file_name
    local = download_folder + file_name
    if not os.path.exists(local):
        print("{0}のZIPファイルをダウンロード".format(file_name))
        try:
            download_data = urllib.request.urlopen(download_file_url).read()
        except urllib.error.HTTPError as e:
            print('raise HTTPError')
            print(e.reason)
            continue
        except urllib.error.URLError as e:
            print('rase URLError')
            print(e.reason)
            continue
        else:
            print(res.status)
            res.close()

        with open(local, mode="wb") as f:
            f.write(download_data)

        time.sleep(1)
    else:
        print("{0}のファイルはダウンロード済みです。".format(file_name))


zip_files = os.listdir(download_folder)

unzip_folder = save_folder + "unzip//"
if not os.path.isdir(unzip_folder):
    os.makedirs(unzip_folder)

unzip_filenames = []

for zip_file in zip_files:
    if ".zip" in zip_file:
        print("{0}のファイルを解凍します。".format(zip_file))
        zip_file_path = download_folder + zip_file
        with zipfile.ZipFile(zip_file_path, 'r') as z:
            for info in z.infolist():
                info.filename = info.filename.encode('shift_jis').decode('utf-8')
                z.extract(info, path=unzip_folder)
                unzip_filenames.append(info.filename)

utf8_folder = save_folder + "utf8//"
if not os.path.isdir(utf8_folder):
    os.makedirs(utf8_folder)

for unzip_filename in unzip_filenames:
    if os.path.isfile(unzip_folder + unzip_filename):
        if not os.path.isfile(utf8_folder + unzip_filename):
            sr_file = open((unzip_folder + unzip_filename), 'r',encoding='shift_jis')
            dt_file = open((utf8_folder + unzip_filename), 'w',encoding='utf-8')
            for row in sr_file:
                dt_file.write(row)
            
            sr_file.close
            dt_file.close
            print("{0}ファイルの文字コードをUTF8に変換しました。".format(unzip_filename))
        else:
            print("{0}ファイルは既に文字コード変換されています。".format(unzip_filename))
    else:
        print("{0}ファイルが存在しません。".format(unzip_filename))

```

# 出力先
下記の場所に各ファイルが作成されます。
このコードを実行したディレクトリを基準としています。

- 作品
    - 夏目漱石
        - unzip (ダウンロード後、解凍したファイル（SJIS）)
        - utf8 (ダウンロード後、解凍し文字コードを変更したファイル（UTF8）)
        - zip (ダウンロードしたファイル)

# 青空文庫の構成

青空文庫の構造は下記のようになっています。

- 作家別の作品一覧（作品名と作品ID）
    - ex) 夏目漱石の作品一覧 https://www.aozora.gr.jp/index_pages/person148.html#sakuhin_list_1
    - 作品ID
- 作品のページ
    - https://www.aozora.gr.jp/cards/（作家ID）/card（作品ID）.html
    - ex) （作品：こころ）https://www.aozora.gr.jp/cards/000148/card773.html
    - 作品ダウンロードURL
- 作品ダウンロードURL
    - https://www.aozora.gr.jp/cards/000148/files/作品圧縮ファイル
    - ex) （作品：こころ）https://www.aozora.gr.jp/cards/000148/files/773_ruby_5968.zip

作品ファイルはzipで圧縮されています。
文字コードはSJISになっています。

# コードの構成

### 作品一覧ページから各作品ID（card_nums）を取得

```python
#青空文庫 夏目漱石(person148)リストURL
url = "https://www.aozora.gr.jp/index_pages/person148.html#sakuhin_list_1"

def get_card_num():
    print(url)
    try:
        res = req.urlopen(url)
    except urllib.error.HTTPError as e:
        print('raise HTTPError')
        print(e.reason)
    except urllib.error.URLError as e:
        print('raise URLError')
        print(e.reason)
    else:
        print(res.status)

    soup = BeautifulSoup(res, "html.parser")

    first_ol_element = soup.find('ol')

    for element in first_ol_element.find_all('li'): 
        sakuhin  = element.text
        #print(sakuhin)
        title_name = re.search(r'(.*)(（)',sakuhin)
        #print(title_name.group(1))
        card_num =re.search(r'作品ID：([\d:]+)',sakuhin)
        #print(card_num.group(1))
        card_nums.append(card_num.group(1))

get_card_num()

```

### 作品ID（card_nums）をもとに作品ダウンロードリンク（file_names）を取得
```python
for card_num in card_nums:
    print(card_num)
    card_url = "https://www.aozora.gr.jp/cards/000148/card" + str(card_num) + ".html"
    print(card_url)
    try:
        res = req.urlopen(card_url)
    except urllib.error.HTTPError as e:
        print('raise HTTPError')
        print(e.reason)
        continue
    except urllib.error.URLError as e:
        print('rase URLError')
        print(e.reason)
        continue
    else:
        print(res.status)

    soup = BeautifulSoup(res, "html.parser")

    download_table = soup.find('table', {'class':'download'})
    #print(download_table)
    download_path = download_table.select_one("tr:nth-of-type(2) td:nth-of-type(3)").get_text()
    #print(download_path)
    file_names.append(download_path)
    time.sleep(1)
```

### 作品ダウンロードリンク（file_names）から作品をダウンロード
```python
for file_name in file_names:
    print(file_name)
    download_file_url = "https://www.aozora.gr.jp/cards/000148/files/" + file_name
    local = download_folder + file_name
    if not os.path.exists(local):
        print("{0}のZIPファイルをダウンロード".format(file_name))
        try:
            download_data = urllib.request.urlopen(download_file_url).read()
        except urllib.error.HTTPError as e:
            print('raise HTTPError')
            print(e.reason)
            continue
        except urllib.error.URLError as e:
            print('rase URLError')
            print(e.reason)
            continue
        else:
            print(res.status)
            res.close()

        with open(local, mode="wb") as f:
            f.write(download_data)

        time.sleep(1)
    else:
        print("{0}のファイルはダウンロード済みです。".format(file_name))
```

### ダウンロードしたzipファイルを解凍
```python
for zip_file in zip_files:
    if ".zip" in zip_file:
        print("{0}のファイルを解凍します。".format(zip_file))
        zip_file_path = download_folder + zip_file
        with zipfile.ZipFile(zip_file_path, 'r') as z:
            for info in z.infolist():
                info.filename = info.filename.encode('shift_jis').decode('utf-8')
                z.extract(info, path=unzip_folder)
                unzip_filenames.append(info.filename)

```

### 解凍した作品ファイルの文字コードをUTF8に変換
```python
for unzip_filename in unzip_filenames:
    if os.path.isfile(unzip_folder + unzip_filename):
        if not os.path.isfile(utf8_folder + unzip_filename):
            sr_file = open((unzip_folder + unzip_filename), 'r',encoding='shift_jis')
            dt_file = open((utf8_folder + unzip_filename), 'w',encoding='utf-8')
            for row in sr_file:
                dt_file.write(row)
            
            sr_file.close
            dt_file.close
            print("{0}ファイルの文字コードをUTF8に変換しました。".format(unzip_filename))
        else:
            print("{0}ファイルは既に文字コード変換されています。".format(unzip_filename))
    else:
        print("{0}ファイルが存在しません。".format(unzip_filename))

```

最後まで見てくださりありがとうございました。