Zennのタグ情報をスクレイピング【備忘録】

# 概要
QiitaとZennってどんな特徴があるの？
気になったけれどZennの情報はまとまっていなかったのでスクレイピングで調べてみました。
その時の困りごと等々忘れない様、備忘録代わりに書いてみました。
初投稿なので拙い部分多々あるかと思いますが、よろしくお願いいたします。

# 目次
[1.タグ情報のランキング](#1タグ情報のランキング)
[2.実行環境](#2実行環境)
[3.利用規約確認](#3利用規約確認)
[4.サイトURLスクレイピング](#4サイトurlスクレイピング)
[5.タグ情報スクレイピング](#5タグ情報スクレイピング)
[6.最後に](#6最後に)
# 1.タグ情報のランキング
2021年のタグ情報をまとめてみました。

個人的には特徴があるなと思ったのは、下記キーワード別順位比較を見ると、
Zenn、Qiitaともに上位のタグもある中、
TypeScript、Flutter、Go、Next.js、GitHubなどはZennのみで上位（10位以内）でした。
逆に初心者、Rails、Ruby等のタグはQiitaのみで上位（10以内)でそれぞれのサイトの特徴の様なものが見られました。

### Zenn、Qiita順位比較
|順位|Zenn     |Qiita    |
|---|---       |---     |  
|1  |JavaScript|Python  | 
|2  |AWS       |JavaScript|
|3  |TypeScript|初心者     |
|4  |Python    |Rails     |
|5  |React     |AWS       |
|6  |Docker    |Ruby      |
|7  |Flutter   |Docker    |
|8  |Go        |PHP       |
|9  |Next.js   |React     |
|10 |GitHub    |Swift     |
|11 |iOS       |Laravel   |
|12 |PHP       |Java      |
|13 |Laravel   |Vue.js    |
|14 |Node.js   |TypeScript|
|15 |Linux     |Linux     |
|16 |初心者     |HTML      |
|17 |Rails     |Unity     |
|18 |Ruby      |Git       |
|19 |Vue.js    |C#        |
|20 |Git       |CSS       |

### キーワード別順位比較
|キーワード|Zenn順位|Qiita順位|差分|
|---       |---    |---      |--- |
|JavaScript|1      |2        |+1   | 
|AWS       |2      |5        |+3   |
|TypeScript|3      |14       |+11  |
|Python    |4      |1        |-3  |
|React     |5      |9        |+4   |
|Docker    |6      |7        |+1   |
|Flutter   |7      |31       |+24  |
|Go        |8      |36       |+28  |
|Next.js   |9      |51       |+42  |
|GitHub    |10     |23       |+13  |
|iOS       |11     |22       |+11  |
|PHP       |12     |8        |-4  |
|Laravel   |13     |11       |-2  |
|Node.js   |14     |25       |+11  |
|Linux     |15     |15       |±0   |
|初心者　　|16     |3        |-13  | 
|Rails     |17    |4     　　|-13 |
|Ruby      |18    |6        |-12  |
|Vue.js    |19    |13       |-6   |
|Git       |20    |18       |-2   |

### Zenn、Qiita順位比較（件数付き）
|順位|Zenn     |Zenn件数|Qiita    |Qiita件数|
|---|---       |---   |---       |---     |  
|1  |JavaScript|1261  |Python    |12127   | 
|2  |AWS       |1248  |JavaScript|7003    |
|3  |TypeScript|1226  |初心者     |6694   |
|4  |Python    |1193  |Rails     |6599    |
|5  |React     |1050  |AWS       |6501    |
|6  |Docker    |664   |Ruby      |5543    |
|7  |Flutter   |641   |Docker    |3620    |
|8  |Go        |541   |PHP       |3283    |
|9  |Next.js   |538   |React     |3088    |
|10 |GitHub    |492   |Swift     |2833    |
|11 |iOS       |456   |Laravel   |2647    |
|12 |PHP       |452   |Java      |2558    |
|13 |Laravel   |450   |Vue.js    |2318    |
|14 |Node.js   |440   |TypeScript|2293    |
|15 |Linux     |422   |Linux     |2124    |
|16 |初心者     |416   |HTML      |2121   |
|17 |Rails     |398   |Unity     |2093    |
|18 |Ruby      |391   |Git       |2062    |
|19 |Vue.js    |370   |C#        |2000    |
|20 |Git       |361   |CSS       |1925    |

# 2.実行環境
今回、Google Colaboratoryで実施しました。

# 3.利用規約確認
まずはスクレイピングしても良いかどうか確認しました。
[Zenn利用規約](https://zenn.dev/terms)
[Zenn/robot.text](https://zenn.dev/robots.txt)

スクレイピングできるかどうかは利用規約の変更等の可能性もありますので、ご自身で確認頂き自己責任でお願いいたします。

↓基本的なスクレイピングの流れは動画にはなりますがこちらを参考にしました。

https://www.youtube.com/watch?v=fzpYaYnQ3Sc&list=PL4Y-mUWLK2t1LehwHVwAqxXTXw5xd-Yq8


# 4.サイトURLスクレイピング
まずはZennの新着一覧記事から基本情報をスクレイピングで取得していきます。
[Zenn新着記事-１ページ目-](https://zenn.dev/articles?page={1})
このページから、「記事のタイトル」、「著者」、「投稿時間」、「記事URL」を取得していきます。
```python
from bs4 import BeautifulSoup
import requests

# 変数urlに、Zenn新着ページのURLを入れる
url = 'https://zenn.dev/articles?page={1}'

# urlへのアクセス結果を、変数rに格納
r = requests.get(url)

# 取得結果を解析してsoupに格納
soup = BeautifulSoup(r.text)

# ArticleList_itemContainer__xlBMcクラスを持ったdivタグをすべて取得して、変数contentsに格納
contents = soup.find_all('div', class_='ArticleList_itemContainer__xlBMc')
```
contents の中身を確認すると大量のdivタブ情報が取得できるのがわかるかと思います。
```
contents
```
これで必要な情報に関連するdivタブ情報の一覧が取得できたので、
ここから必要な情報を取得していきます。
```python
d_list=[]

for content in contents:
    #URLlink
    link = 'https://zenn.dev' + content.a.get('href')
    #タイトル
    title= content.find('h2').text
    #著者
    author = content.find('div', class_='ArticleList_userName__GWXDx').text
    #時間
    time  = content.time.get('datetime')
    # 変数dに、これまで取得した項目を格納する
    d = {
        'title': title,
        'author': author,
        'link': link,
            'time': time
    }
    d_list.append(d)
```

こちらのページは
https://zenn.dev/articles?page={ページ番号}
という形で、ページ番号を増やしていけば全ての記事情報を得ることができます。
ページが正常に取得できる状態の間、ページ番号を増やしていきます。
ページの正常取得はstatus_codeで確認します。
200であれば正常にページが開けています。
そうでなければループが閉じる様にします。
```python
if r.status_code == 200:
    ・・・
    ・・・
    ・・・
else:
    break
```

ここまでをまとめると下記の様になります。
```python
from bs4 import BeautifulSoup
import requests
import pandas as pd
from time import sleep

# 変数urlに、Zenn新着ページのURLを入れる
url = 'https://zenn.dev/articles?page={}'

# 変数d_listに空のリストを作成する
d_list = []
#ページ番号i
i=0
# アクセスするためのURLをtarget_urlに格納する 
while True:
    i +=1
    target_url = url.format(i)
    # target_urlへのアクセス結果を、変数rに格納
    r = requests.get(target_url)

    if r.status_code == 200:
        print('d_listの大きさ：', len(d_list))
        # print()してtarget_urlを確認する
        print(target_url)

        # 取得結果を解析してsoupに格納
        soup = BeautifulSoup(r.text)
        # ArticleList_itemContainer__xlBMcクラスを持ったdivタグをすべて取得して、変数contentsに格納
        contents = soup.find_all('div', class_='ArticleList_itemContainer__xlBMc')
        #1秒ウェイトを入れる
        sleep(1)

        for content in contents:
            #URLlink
            link = 'https://zenn.dev' + content.a.get('href')
            #タイトル
            title= content.find('h2').text
            #著者
            author = content.find('div', class_='ArticleList_userName__GWXDx').text
            #時間
            time  = content.time.get('datetime')
            # 変数dに、これまで取得した項目を格納する
            d = {
                'title': title,
                'author': author,
                'link': link,
                 'time': time
            }
            d_list.append(d)
    else:
        break 


# 変数d_listを使って、データフレームを作成する
df = pd.DataFrame(d_list)

# to_csv()を使って、データフレームをCSV出力する
df.to_csv('ファイルの保存場所/ファイル名.csv', index=None, encoding='utf-8-sig')
```

# 5.タグ情報スクレイピング

先ほど取得した,個別のページのurlを用いてタグ情報を取得していきます。

### Google Colaboratoryセッション切れ対策
Google Colaboratoryで実行した場合限定ですが、
セッションが
1.何も操作しなかった場合90分
2.無条件で１２時間
で切れる仕様になっています。

1の対策はこちらのサイトに対処しました。

https://qiita.com/enmaru/items/2770df602dd7778d4ce6


2の対策はどうしようもないので、（有料登録すれば２４時間まで伸ばせる様ですが、根本的には同じ）
時間制限を超えない様に工夫するしかないです。

ファイルの長さを確認します。
```python
import pandas as pd

#csvからページ名を取得
df = pd.read_csv('ファイル保存場所/ファイル名.csv')

print(len(df))
```
```
28000
```
スリープ1秒の長さを考慮し、１処理当たり２秒かかるとすると

28000行　× 2秒　÷ 3600時間/秒　= **約15時間**

となり、ギリギリ１２時間を超えてしまいます。

なので、ファイルを分割し、それぞれ処理することで処理を１２時間以内におさめます。
今回は２分割しますが、かかる時間に合わせて分割します。

分割方法はこちらを参考にしました。

https://arakan-pgm-ai.hatenablog.com/entry/2020/05/27/000000

https://python.ms/division/#_3%E3%81%A4%E3%81%AE%E3%82%84%E3%82%8A%E6%96%B9

### ファイル分割
```python
import pandas as pd

#csvからページ名を取得
df = pd.read_csv('/ファイル保存場所/ファイル名.csv')
##行の中央を計算
row = len(df) //2
#前半後半に分けて格納
df1 = df[:row]
df2 = df[row:]

# to_csv()を使って、データフレームをCSV出力する
df1.to_csv('/ファイル保存場所/ファイル名_1.csv', index=None, encoding='utf-8-sig')
df2.to_csv('/ファイル保存場所/ファイル名_2.csv', index=None, encoding='utf-8-sig')
```
csvファイルからタグ情報を取得します。
処理するファイルがいくつかあるので、関数を作ります。

また、今回ファイルを分割したので処理を2回行うので関数として下記の様に作成します。
### タグ情報読出
```python
def get_tag(num):
    from bs4 import BeautifulSoup
    import requests
    import pandas as pd
    from time import sleep

    #fileNoを定義
    read_file = '/ファイル保存場所/ファイル名_{}.csv'
    target_read_file=read_file.format(num)

    # 変数d_listに空のリストを作成する
    d_list = []
    #csvからページ名を取得
    df = pd.read_csv(target_read_file)

    #処理数nを定義
    n=0

    for title, author,link,time in zip(df.title, df.author, df.link, df.time):
        #処理数をカウント
        n += 1
        #エラー処理
        try:#3、予期せぬエラー対策〜except Exception
            # アクセスするためのURLをtarget_urlに格納する 
            if link is None:#１、リンク情報がない
                print("{}ページURL情報がありません。".format(n))
                continue
            target_url = link
            # target_urlへのアクセス結果を、変数rに格納
            r = requests.get(target_url)
            if r.status_code != 200:#２、リンク先が消えている
                print("{}ページアクセスエラー".format(n))
                continue
            # 取得結果を解析してsoupに格納
            soup = BeautifulSoup(r.text)
            #100処理毎に表示
            if n % 100 == 0:
                print("現在処理数は{}です。".format(n))

            #1秒ウェイトを入れる
            sleep(1)

            #変数tag情報リセット
            for x in range(1,11):
                exec("tag_{}={}".format(x, None))

            #タグ情報のクラス名
            tags = soup.find_all('div', class_='View_spTopicName__Jo5TO')

            i = 1

            for tag in tags:
                tag = tag.text
                exec("tag_{}={}".format(i, "tag"))
                i += 1
            # 変数dに、これまで取得した11項目を格納する
            d = {
            'title':title,
            'author':author,
            'link':link,
            'time':time,
            'tag_1': tag_1,
            'tag_2': tag_2,
            'tag_3': tag_3,
            'tag_4': tag_4,
            'tag_5': tag_5,
            'tag_6': tag_6,
            'tag_7': tag_7,
            'tag_8': tag_8,
            'tag_9': tag_9,
            'tag_10': tag_10         
            }
            d_list.append(d)
        except Exception as e:
            print(type(e))
            print("予期せぬエラーが{}で発生しスキップしました。".format(n))
            continue

    # 変数d_listを使って、データフレームを作成する
    df = pd.DataFrame(d_list)
    #fileNoを定義
    write_file='/ファイル保存場所/ファイル名_{}.csv'
    target_write_file=write_file.format(num)

    # to_csv()を使って、データフレームをCSV出力する
    df.to_csv(target_write_file, index=None, encoding='utf-8-sig')
```
処理の際、エラーがいくつか発生したので、エラー対策をします。
１、リンク情報がない
```python
if link is None:
```
２、リンク先が消えている
```python
 if r.status_code != 200:
```
３、予期せぬエラー対策
```python
try:
    ・・・
    ・・・
    ・・・
except Exception
```
３、のエラー対策はこちらを参考にしました。

https://note.com/data_crazy/n/n2397bbb4d8b3#u5YsF

タグ情報はいくつあるかわからないので、こちらを参考に最大１０個格納できる様にしました。
こちらのサイトを参考にしました。

https://qiita.com/ytsumura/items/917de811c023ee9f3709
```python
for tag in tags:
    tag = tag.text
    exec("tag_{}={}".format(i, "tag"))
    i += 1
```

### ファイルの結合

分割したファイルそれぞれに処理を行います。
```python
#1回目
get_tag(1)
```
```python
#2回目
get_tag(2)
```

分割されたcsvファイルを結合します。
```python
import pandas as pd

t1 = pd.read_csv('/ファイル保存場所/ファイル名_1.csv')
t2 = pd.read_csv('/ファイル保存場所/ファイル名_2.csv')
t = pd.concat([t1, t2], ignore_index=True)

t.to_csv('/ファイル保存場所/ファイル名.csv', index=None, encoding='utf-8-sig')
```
# 6.最後に
こういった感じで、Zennのスクレイピングはできると思います。
はじめてのプログラミングでエラーが多発して非常に苦労しましたがなんとかできました。
もっとこうすればいいなどありましたら、アドバイス頂けると嬉しいです。