複数ページにわたる情報の取得。スクレイピング。

## はじめに

どうも。スクレイピング歴11日の初心者です。
本日学習したスクレイピングでは複雑な表記やif文､for文の集大成感があり、
上手く纏めて理解できると､スキルアップに繋がるのではと思いqiitaに書き残します。

### 環境

windows11
jupyterlab


### 本記事の目的

- キーワード検索した結果の中で、自動でそのページ内の情報を取得する事。
- 更に検索結果が複数のページにわたる場合でも対応できるようにすること。
例えばこのような場合。
![2022-07-07_21h10_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/e77ef92e-f8a0-ea54-2fa3-e5c8bc8abcb0.png)


この記事を通して

- 機械学習やAIアプリの開発､自動化のデータ取得の効率化の為にスクレイピングの技術を取得する。
- コードを書いてみて､内包表記やif文も多く見られたので復習も兼ねる。
このような狙いを達成できればと思います。

### バックナンバー
- [初学者が勉強するSelenium。①](https://qiita.com/The_Boys/items/6d53992ff9323c3c2597)
- [初学者が勉強するSelenium。②](https://qiita.com/The_Boys/items/7b0fa0a75748f65b1edf)
- [seleniumはCopy_full_xpathで少し楽になる](https://qiita.com/The_Boys/items/4c0fa1bcac066de2c643)
- [初学者が勉強するBeautiful Soup①](https://qiita.com/The_Boys/items/da2eb6895ca400730fb1)
- [初学者が勉強するBeautiful Soup②](https://qiita.com/The_Boys/items/43d8837fce179cca237f)
- [Beautiful Soupで画像を取得](https://qiita.com/The_Boys/items/6d7dffc982760e8d8066)
- [BeautifulSoupでURLを一括取得](https://qiita.com/The_Boys/items/14d42da70d26fad6dde2)
- [複数ページにわたる情報の取得。スクレイピング。](https://qiita.com/The_Boys/items/0862cb8a72100519aee9)
- [複数に渡るページでの画像収集スクレイピング](https://qiita.com/The_Boys/items/9486026c20973bc74787)
#### 教材

https://www.youtube.com/watch?v=ku1XLvh49Vw&t=379s

https://qiita.com/The_Boys/items/6d7dffc982760e8d8066

https://qiita.com/The_Boys/items/da2eb6895ca400730fb1

基本的には上の動画を参考にして自分流に書き起こしました。

## 学習

まず初めに総まとめコードを添付します。最初にコードを読解し少しでもこれから行う処理のイメージをして頂ければと。


```python
from bs4 import BeautifulSoup
import requests
import bs4
import time
import pandas as pd

keyword = 'Python'

url = 'https://kino-code.work/?s={}'.format(keyword)

r = requests.get(url)
time.sleep(3)

soup = BeautifulSoup(r.text, 'html.parser')
page_na = soup.find(class_ = 'pagination')
page_num = page_na.find_all(class_ = 'page-numbers')

pages = []
for i in page_num:
    pages.append(i.text)

urls = []

if not pages:
    urls = ['https://kino-code.com/?s={}'.format(keyword)]
else:
    last_page = int(pages[-2])

    for i in range(1, last_page + 1):
        url = 'https://kino-code.work/?s={}'.format(keyword) + '&paged={}'.format(i)
        urls.append(url)

links = []
titles = []
snippets = []

for i in range(len(urls)):

    r = requests.get(urls[i])
    time.sleep(3)
    soup = BeautifulSoup(r.text, 'html.parser')
    # find_allの引数に、HTMLタグが持つCSSのクラスで検索をかける。
    get_list_info = soup.find_all('a', class_ =  'entry-card-wrap')

    for n in range(len(get_list_info)):
        # リンクを取得
        get_list_link = get_list_info[n].attrs['href']
        links.append(get_list_link)

        # タイトルを取得
        get_list_title = get_list_info[n].attrs['title']
        titles.append(get_list_title)

        # 説明文を取得
        get_list_snippet = get_list_info[n].find(class_ = 'entry-card-snippet').text
        snippets.append(get_list_snippet)
        

result = {
    'title': titles,
    'link': links,
    'snippet': snippets
}

df = pd.DataFrame(result)
df
```


それでは次のセクションで基本的な流れや要点をまとめて行きます。

### フロー
　



<details><summary>①　モジュールのインポートとサイトの取得</summary><div>

モジュールのインポートと目的画像のあるページデータの取得

```python
from bs4 import BeautifulSoup     # HTMLの構造の解析と抽出
import requests                   # HTTP接続
import pandas as pd               # pandasのインポート
import time                       # timeのインポート
```

この部分については特に語る事はありません。必要なスクレイピングのモジュールをインストールします。



```python
keyword = 'Python'                                     # 検索したい言葉

url = 'https://kino-code.work/?s={}'.format(keyword)   # 今回スクレイピングを行うサイト

r = requests.get(url)                                  # 指定したurlをリクエストで取得
time.sleep(3)                                          # 3秒間待機
```


<font color="red">今回は変数 url にて検索されたkeyword(Python)にて表示された全ページに対してスクレイピングを行います。</font>

- url
format構文を使用しておりますね。書き下すと
`https://kino-code.work/?s=Python`　このようになります。keywordが{}内に格納されます。
つまりurlの最後の部分には、検索されたkeywordが格納されるという仕組みです。例を挙げますと
- https://kino-code.work/?s=Python
- https://kino-code.work/?s=Go
- https://kino-code.work/?s=Javascript
等。

各々のサイトにも同様な法則がある場合も多いので是非探ってみてください。

</div></details>

<details><summary>②　検索結果が複数ページに渡る際の対処</summary><div>

先ほど取得したurlにはPythonの検索結果が複数あります。目的はその複数ページ全てから情報を取得する事です。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/861a8daf-306b-2381-04a5-e85586ffce45.png)


まず最初に、1,2,3ページ目のurlを比較してみましょう。

![2022-07-08_14h47_a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/4f5a1daa-bac2-5cb0-1e7f-6a8eb71f0e1f.png)

上記画像のように、<font color="red">ページ数とurlが呼応</font>している事が確認できます。
更に検証で調べてみると､ページ数は<font color="red">class_ = 'page-numbers'</font>に属している事が確認できます。

検証での調べた結果
![2022-07-08_14h53_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/109d455c-d473-65a0-53ad-974c02b6c308.png)
そうしますと下記のコードはイメージできますでしょうか。

```python

soup = BeautifulSoup(r.text, 'html.parser')            #html全体の情報を取得

page_num = page_na.find_all(class_ = 'page-numbers')   #その全体からclass_ = 'page-numbers'を取得。

``` 

一旦まとめも兼ねてpage_numを確認してみましょう。

```python
from bs4 import BeautifulSoup
import requests
import bs4
import time
import pandas as pd

keyword = 'Python'

url = 'https://kino-code.work/?s={}'.format(keyword)

r = requests.get(url)
time.sleep(3)

soup = BeautifulSoup(r.text, 'html.parser')
page_na = soup.find(class_ = 'pagination')
page_num = page_na.find_all(class_ = 'page-numbers')

print(page_num)
print(page_num[0].text)
print(page_num[1].text)
print(page_num[2].text)
```

実行結果化は以下の通りです。
![2022-07-08_15h06_34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/0d2ba43a-e978-7be3-9f8e-64258245cf85.png)

`print(page_num)`ではhtmlをそのまま抜き出す為、`.text`メソッドを用いてテキストのみ抽出します。そして`find_all`はリストを返すので各々はリストで表示させてあげる必要があります。


</div></details>


<details><summary>③　各ページのurlを取得</summary><div>
Pythonの場合は３ページでしたが、検索するkeywordの結果によりページ数は異なります。
そのページ数を取得しましょう。この際に`page_num`を用います。

```python
pages = []                     #空のリストを作成
for i in page_num:             #page_numの回数文要素をforで回す。
    pages.append(i.text)       #空のリストに要素をテキスト化してappendで追加。

#結果
#['1', '2', '3', '']
```
3ページまである事が確認できました。　空白の''は「次へ」のボタンのことです。
同じような手段で別のKeywordでも試してはいかがでしょうか。


今度はそのページに呼応するurlを取得します。1Pのurl､2Pのurl等、検索結果のページの数だけurlを取得します。<font color="red">ここはポイントの一つ</font>です。(私的に)


```python
urls = []     #空のリストを作成します。

last_page = int(pages[-2])             #最後のページ番号を変数に格納します。

   for i in range(1, last_page + 1):   # for文を用いてURLの検索結果の作成
        url = 'https://kino-code.work/?s={}'.format(keyword) + '&paged={}'.format(i)
        urls.append(url)               #リストurlに作成したurlを格納。
```

- last_page = int(pages[-2]) について
pagesの要素は['1', '2', '3', '']です。ここから<font color="red">最後から二番目[-2]</font>と指定すれば最後のページを取得する事が出来ます。取得時に<font color="red">int型</font>に直しておきましょう。

-  for i in range(1, last_page + 1)について
ページ数分のurlが必要なのでその分range関数で設定を行います。第一引数は[1]です。ページは1から始まりますので。最後のページは<font color="red">リストの性質上</font>プラス１にしないと最後のページの一つ手前で終わってしまいます。最後まで取得するには＋1してください。
これでページ数に呼応したurlを作成することができます。

- .format(keyword) + '&paged={}'.format(i) について

![2022-07-08_14h53_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/ed4f1434-d555-eb9a-78dc-d172b245d14a.png)

このhtmlの構造を再現しましょう。keywordを{}の中に入れて、ページ数の番号(1,2,3)を変数 i で対応しています。

#結果
['https://kino-code.work/?s=Python&paged=1',
 'https://kino-code.work/?s=Python&paged=2',
 'https://kino-code.work/?s=Python&paged=3']


</div></details>



<details><summary>④　例外処理</summary><div>

複数にページが渡る場合はこれで作動します。しかし<font color="red">ページ数が1ページしかないjavascript</font>の場合はいかがでしょうか？

![2022-07-08_18h06_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/3ec6fa55-b5db-4e34-c34b-7d8a05621870.png)

検索結果が複数ない場合はページングが無い為にエラーになってしまいます。

![2022-07-08_18h11_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/2c06ec79-7fdc-50f2-6125-77e19d847ea2.png)

この場合はif文により条件分岐をして処理を分ける<font color="red">例外処理</font>を行いましょう。

pagesの中身が空かどうかを判定。

```python
if not pages:  #リストpagesの中身が空かどうかを判定。
    urls = ['https://kino-code.com/?s={}'.format(keyword)]   #空ならばこの処理を行う。
else:  # pagesの中身が格納されている場合はelseに移行。
```

このように書き換えも可能です。こちらの方が分かりやすいでしょうか。

```python
if pages ==[]:  #リストpagesの中身が空かどうかを判定。
    urls = ['https://kino-code.com/?s={}'.format(keyword)] 
else:  # pagesの中身が格納されている場合はelseに移行。
```


上記を踏まえ一旦ここまで確認してみましょう。


```python
from bs4 import BeautifulSoup
import requests
import bs4
import time
import pandas as pd

keyword = 'Python'

url = 'https://kino-code.work/?s={}'.format(keyword)

r = requests.get(url)                                           #URLを取得できたかを確認。
time.sleep(3)

soup = BeautifulSoup(r.text, 'html.parser')                     #htmlを表示。
page_na = soup.find(class_ = 'pagination')
page_num = page_na.find_all(class_ = 'page-numbers')            #1ページ2ページ3ページ。

pages = []                                                     # pagesには1,2,3。
for i in page_num:
    pages.append(i.text)                                      #空のリストに要素をテキスト化して詰め込むのはappendを用いる。

urls = []

                                                              #ここがかなり不安。
if not pages:     #データが何もない場合                                            #[ある条件を満たさない場合] に行う処理を書くときに使います。　　　　　　　　　　　　　　　　　　不明
    urls = ['https://kino-code.com/?s={}'.format(keyword)]    #python で検索された検索結果。 を満たさない場合？　　　　　　　　　　　　　　　　　　　　　　　不明
else:
    last_page = int(pages[-2])                     

    for i in range(1, last_page + 1):                                                       # これはどこからどこまでというのが分からない。
        url = 'https://kino-code.work/?s={}'.format(keyword) + '&paged={}'.format(i)        #最後の数字で python ページが変化する。
        urls.append(url)                                                                    #これで全てのページの url (1,2,3) を取得することができた。
```

</div></details>



<details><summary>⑤　各urlの情報を取得</summary><div>
前セクションまでにkeyword= "Python"で検索された結果の1,2,3の全ページのurlを取得する事が出来ました。　
次はその各々のページに記載されている､タイトルとリンクと説明文を一括で取得しましょう。

![2022-07-09_13h13_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/867b0802-e6db-cde0-8457-ed1b29d79289.png)

まずは各々のデータを格納するリストを作成します。同じパターンですね。

```python
links = []
titles = []
snippets = []

for i in range(len(urls)):
   r = requests.get(urls[i])   #rangeの回数分urlが取得される。
```
同じようにリストから一つづつ行うという処理です。しかし私自身<font color="red">range(len(urls)):</font>の解釈に苦戦したので一旦区切り説明させて頂きます。

- rangeの引数はint型のみ。urlsはリストなのでlenを用いてint型に変換する必要がある。
- 何故rangeを使用するのか？リストの個数だけfor文を回したいから。rangeの回数(i回)だけurlの要素が取り出されforで回る。rangeの i とurlsの i がリンクされている。


- for i in urls ではだめな理由。
シンプルにこれでいいのでは？と思いますよね？いつも通り。試しに
下記のコードを実装して結果をご確認ください。

```python
for i in urls:
    print(i)
```
![2022-07-09_15h57_42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/1e8a6b9e-91b8-33da-8c8a-8ae0e238a174.png)

ご覧の通り<font color="red">,</font>で区切られておらず、それぞれのurlが繋がっています。
これではエラーになるのも納得ですよね。なので<font color="red">for i in range(len(urls))</font>を用います。




```python
for i in range(len(urls)):

    r = requests.get(urls[i])                      #rangeの回数分urlが取得される。
    time.sleep(3)                                  # 3秒待つ。
    soup = BeautifulSoup(r.text, 'html.parser')    #読み込んだhtmlの解析
    get_list_info = soup.find_all('a', class_ =  'entry-card-wrap') #解析したhtmlから欲しい情報を取得。
```
そこから欲しい情報がhtmlのどこに格納されているのか見極めて取得します。

![2022-07-09_16h25_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2719278/9d96f5d4-ee42-7459-aaa0-c7cc91ff3947.png)

カーソルで合わせている部分class="entry-card-wrap a-wrap border-element cf"。
これは""で囲まれている1つのクラスではなくて、<font color="red">[entry-card-wrap][a-wrap][border-element][cf]</font> というクラスが含まれているという事です。
今回は'entry-card-wrap' から抽出します。

一旦確認も挟みます。

```python
a = soup.find_all('a', class_ =  'entry-card-wrap')
a[0]
```

クラス、｢entry-card-wrap｣　からaタグを全て取得します。
その抽出されたパートから、
- リンク、href="https://kino-code.work/python-scraping/"
- タイトル、"Python超入門コース(テスト)"
- 説明、Python超入門コース#01 Pythonのコース紹介 Python超入門コース#02 Pythonとは？ Python超入門コース#03 環境構築 for Mac Python超入門コース#04 プログラムの基本構造...

がそれぞれ格納されていることをご確認下さい。
そしてその回数分forで回し、情報を各々取得します。

これらをまとめると下記のコードになります。


```python
links = []
titles = []
snippets = []

for i in range(len(urls)):

    r = requests.get(urls[i])
    time.sleep(3)
    soup = BeautifulSoup(r.text, 'html.parser')
    # find_allの引数に、HTMLタグが持つCSSのクラスで検索をかける。
    get_list_info = soup.find_all('a', class_ =  'entry-card-wrap')

    for n in range(len(get_list_info)):#取得した get_list_info の回数分。
        get_list_link = get_list_info[n].attrs['href']  #hrefを含むものを取得
        links.append(get_list_link)                     #それをリストにappend

        
        get_list_title = get_list_info[n].attrs['title'] # タイトルを取得。
        titles.append(get_list_title)                    # リストに追加。

        # 説明文を取得
        get_list_snippet = get_list_info[n].find(class_ = 'entry-card-snippet').text
        snippets.append(get_list_snippet)

#説明文はclass_ = 'entry-card-snippet'に格納されているので、テキスト化して抽出。リストに追加。
```

これで全ての要素をリストに追加する事が出来ました。それではこの3つのリストを
辞書化してdfに変換しましょう。


```python
result = {
    'title': titles,      # key名に"title", value要素にtitles
    'link': links,        # key名に"title", value要素にtitles
    'snippet': snippets   # key名に"title", value要素にtitles
}

df = pd.DataFrame(result)
df
```
ここは難しくないはずです。　辞書化の基本通りで、
keyとvalueを指定しdfに変換してください。ここまででやっと当初の目的は達成されました。
</div></details>






## 終わりに

今回はかなりタフな内容でした。しかし、forの内包表記やif文、例外処理や辞書化等様々な基本動作を用いたので復習にはもってこいでした。

不安要素としては他のサイトから｢検証｣で要素を正しく持ってこれるかです。是非手ごろな練習サイトで同じような処理を試してみたいものですね。
今回urlと情報を取得したので次回は画像をまとめて取得したいと思います。
[次の記事も是非ご覧ください。](https://qiita.com/The_Boys/private/9486026c20973bc74787)

質問やご意見いつでもお待ちしております。