ゆっくりに青空文庫を読んでもらう: コード改善編

[前回](https://qiita.com/Mechanetai/items/f3e1b58f8c5549e2f31b)はゆっくり(SofTalk)に辞書登録しつつ読み上げさせることに成功しました
今回は、勉強のために公式ドキュメントや関連する記事を読みつつ、前回のコードを改善していきます

# 図書カードからURL取得まで

### 改善したコード

```python:aozora_urlsplit.py
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin

def aozoraurl(base_url):
    page = requests.get(base_url)
    soup = BeautifulSoup(page.text, 'lxml')

    xhtml_relative_url = soup.select('table.download a[href*=".html"]')[0].get('href')
    zipdl_relative_url = soup.select('table.download a[href*=".zip"]')[0].get('href')

    xhtml_url = urljoin(base_url, xhtml_relative_url)
    zipdl_url = urljoin(base_url, zipdl_relative_url)

    return xhtml_url, zipdl_url

if __name__ == '__main__':
    print(aozoraurl("https://www.aozora.gr.jp/cards/000879/card85.html"))
```

### Beautiful Soup 4
> **Beautiful Soup はHTMLやXMLファイルからデータを取得するPythonのライブラリです。**
> [Beautiful Soup 4.2.0 Doc. 日本語訳](http://kondou.com/BS4/)より

`BeautifulSoup` コンストラクタにHTMLドキュメントを渡すことで、その構造を解析します

```python
from bs4 import BeautifulSoup
soup = BeautifulSoup(page.content, 'lxml')
```

`'lxml'` はパーサ(構文解析器)を指定しています
HTMLパーサはPython標準(Batteries included)の `html,parser` , 高速な `lxml` , 非常に寛大な `html5lib` から選択できます
公式ドキュメントでは `lxml` が推奨されているようです

> If you can, I recommend you install and use lxml for speed.
> [Beautiful Soup 4.9.0 documentation](https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.html?highlight=css%20selector#) より

### Requests
> **Requestsは、人が使いやすいように設計されていて、Pythonで書かれている Apache2 Licensed ベースのHTTPライブラリです。**
> [Requests: 人間のためのHTTP](https://requests-docs-ja.readthedocs.io/en/latest/) より

`requests.get()` にURLを渡すことで、`Response` オブジェクトが返されます
`Response` の中身を取得するのには、`.text` と `.content` が使えます

```python:公式ドキュメントより
import requests
r = request.get(URL)
r.content # バイナリデータを取得
r.text    # テキストデータを取得
```

……バイナリとテキストの違いがわからないのですが、ここ[^stack]によるとHTMLやXMLには`.text`、画像やPDFには`.content`だそうです

### 2種類のURL (XHTML / zip)取得

要素取得の方法は色々ありますが、CSSセレクターが一番汎用性があると思います。

> If you want to search for tags that match two or more CSS classes, you should use a CSS selector:

```python:公式ドキュメントより
css_soup.select("p.strikeout.body") 
# [<p class="body strikeout"></p>]
```

青空文庫図書カードのページからそれぞれのURLを取り出します
`soup.select` の返り値はlistであることに注意しましょう

```python
xhtml_url = soup.select('table.download a[href*=".html"]')[0].get('href')
zip_url   = soup.select('table.download a[href*=".zip"]')[0].get('href')
```

### urljoin

青空文庫図書カードのページから、それぞれのページへのリンクは相対リンクで記載されています
`urljoin` を用いて元のURLと合成し、絶対リンクに戻します

>**urllib.parse.urljoin(base, url, allow_fragments=True)**
**"基底 URL"(base)と別のURL(url)を組み合わせて、完全な URL ("絶対 URL") を構成します。**
[urllib.parse --- URL を解析して構成要素にする](https://docs.python.org/ja/3/library/urllib.parse.html) より

```python:公式ドキュメントより
>>> from urllib.parse import urljoin
>>> urljoin('http://www.cwi.nl/%7Eguido/Python.html', 'FAQ.html')
'http://www.cwi.nl/%7Eguido/FAQ.html'
```

### return a, b

複数の返り値(今回はURL2つ)を返したいときは、このように書くのが最も簡潔なようです

>Pythonでは、単純にカンマ区切りでreturnするだけで文字列だろうが数値だろうがまとめて返せる。
[Pythonの関数で複数の戻り値を返す方法 | note.nkmk.me](https://note.nkmk.me/python-function-return-multiple-values/) より

この場合返り値はタプルになります。

[^stack]: What is the difference between 'content' and 'text'
 https://stackoverflow.com/questions/17011357/what-is-the-difference-between-content-and-text

# XHTMLページからのルビ抽出まで

### 改善したコード

```python:aozora_rubylist.py
from bs4 import BeautifulSoup
import requests
import jaconv

def aozoraruby(xhtml_url):
    page = requests.get(xhtml_url)
    soup = BeautifulSoup(page.content, 'lxml')

    _ruby = soup.select("ruby")

    _rubylist = [i.get_text().replace("）", "").split("（") for i in _ruby]

    for index, item in enumerate(_rubylist):
        _rubylist[index][1] = jaconv.kata2hira(item[1])
    
    return _rubylist

if __name__ == '__main__':
    print(aozoraruby("https://www.aozora.gr.jp/cards/000119/files/624_14544.html"))
```

### ルビの抽出

青空文庫XHTML本文からrubyタグ以下の文字を抽出します
`.select("ruby")` が一番簡単だと思います

とりあえず取得した内容を確認します
`.get_text()` で文字列部分を抽出できますが、リストには適用できないのでfor文で回します

```python
_ruby = soup.select("ruby")
for i in _ruby:
    print(i.get_text())

# 重（かさ）
# 死骸（しがい）
# 麻利耶（マリヤ）
# ︙
```

`漢字（ルビ）` の形で出力されます

### ルビのリスト作成

取得したルビをリストにします。
単純な `["漢字（ルビ）", "漢字（ルビ）", ...]` のリストでは使いづらいので、`[["漢字", "よみがな"], ["漢字", "よみがな"], ...]`の二次元配列を作成します

for文でリストを作成する際、これまではfor文の前で空リストを作成してそこに要素を追加していました
しかし、**リスト内包表記**という方法を使えば一文で作成できるようです [^naihou1] [^naihou2]

```python
x = [i.get_text().replace("）", "").split("（") for i in _ruby]
# [['重', 'かさ'], ['死骸', 'しがい'], ['麻利耶', 'マリヤ'],...]
```

### カタカタをひらがなに

SofTalkに辞書登録するために、読み仮名がカタカナのものをひらがなにします
[こちらのQiita記事](https://qiita.com/yukinoi/items/42f8b5461dc1b62db7f9)によると、今回の用法ではjaconvが適しているようです

> jaconv (Japanese Converter) はひらがな・カタカナ・全角・半角の文字種変換を高速に行います。 Pythonのみで実装されているので、Cコンパイラが使えない環境でも利用できます。
[jaconv/README_JP.rst at master · ikegami-yukino/jaconv · GitHub](https://github.com/ikegami-yukino/jaconv/blob/master/README_JP.rst) より

```python
for index, item in enumerate(_rubylist):
    _rubylist[index][1] = jaconv.kata2hira(item[1])
# [['重', 'かさ'], ['死骸', 'しがい'], ['麻利耶', 'まりや'],...]
```



[^naihou1]: 内包表記を使ってリストの作成を簡潔に書く - Qiita
 https://qiita.com/tag1216/items/040e482f9844805bce7f

[^naihou2]: リスト内包表記の使い方 | note.nkmk.me
 https://note.nkmk.me/python-list-comprehension/