Python3系でwebスクレイピング

# 多分に参考にさせていただいた記事の数々
##### 基礎の理解 => [Pythonによるスクレイピング超絶入門](https://note.mu/daikawai/n/nc73889d6d835)
この有料noteは初学者の人なら超有用。自分はProgateでPython入門を一周して「ここからどうしたら動くものを作れるのか」という部分を解決したくて購入。恐らく初心者向けの本を購入するよりも手っ取り早く動くものを作れる。ローカルの環境構築無しの記事なので、初心者あるあるの環境構築で躓いて数日かかるといったこともない。
解説部分を読みながら写経することで、基本的なスクレイピングのコードが書けるようになる。
##### 環境構築
- ローカルにpython3系をインストール、バージョンの指定([参考URL](https://hajipro.com/python/mac-python))
- ローカルで動かすに当たり、エディタを導入(VScode)([参考URL](https://dev.classmethod.jp/tool/python-pyenv-vscode/))
- 各種モジュールのインストール(pip利用)([参考URL](https://www.python-izm.com/tips/pip/))
- seleniumのインストール
- Beautiful Soup4のインストール
- pandasのインストール
- requestsのインストール
- chrome web driverの使い方([参考URL](https://qiita.com/motoki1990/items/a59a09c5966ce52128be))

##### 自分への課題で対象にさせていただいたサイト => [ニュートピ](https://newstopics.jp)
日頃見てるので
##### 改ページ問題の解決 => [【Python】Webスクレイピングチュートリアル -「次へ」ボタンが存在するページをすべて取得する場合 -](https://review-of-my-life.blogspot.com/2017/10/pager-next-selenium.html)
基礎の理解で紹介したnoteを書いている[Daiさん(@never_be_a_pm)](https://twitter.com/never_be_a_pm?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E917288553381367809&ref_url=https%3A%2F%2Freview-of-my-life.blogspot.com%2F2017%2F10%2Fpager-next-selenium.html)が書いている。わかりやすい。自分はPhantomJSではなく、Chromedriverを利用した。

# 詰まった・調べた内容
##### `invalid character in identifier`
頻繁に`invalid character in identifier`が出た。コメントを書いた流れで日本語入力のままスペースなどを打ってしまっていた。エディタを導入してLintツールを入れると該当の箇所を注意してくれるためすごく便利。
##### `'NoneType' object has no attribute 'string'`
結構な時間`'NoneType' object has no attribute 'string'`でハマった。原因はaタグしか入れていないリストの要素からaタグを取り出そうとしていたこと。ネストされている要素の場合は問題ない。
##### headlessモードについて
webdriverの使い方を調べている際にheadlessモードなるものがあることを知った。ブラウザがいちいち立ち上がらないので便利。ただ、デバッグの際は処理の止まった部分が分かりづらい。動作確認のとれたものに対して設定するほうがいいかも。
##### 記事の取得を関数化している理由
単純にジャンルがタブで分かれているニュース一覧に対し、同じことを繰り返す方法は関数化がスッキリ書けると考えたから。引数に相対URLを指定したのでわかりやすい。もしかしたら相対URLを一旦変数に格納する方がいいのかもしれない。
##### `find`メソッドの使い方
これは対象とするサイトの構造次第で書き方が変わってくる。今回は`find`と`find_all`くらいしか使っていないが、詰まって検索した際は色々な抽出方法がされていた。
詰まった部分としては、`find_all`でリストに格納された要素を、そのまま`find`で抽出しようとして出たエラー。`tag.find("div", {"class": "comment"}).find("a").get("href")`この書き方で動くのは便利。
##### 次ページの取得、移動
参考にした記事とは大きく形を変える必要があった。まず`find_all`で「次へ」の属性をリストに格納、リストの要素が0個でなければ続行の形に。その後`find`メソッドでリンク先URLを取得。相対パス問題は下記。
##### 相対URLの変換
「次へ」のhrefが相対URLだったため、普通に取得してもエラーになってしまった。
`urljoin`を利用して解決。ページ推移用に結合用URLを保管する変数を最初に指定。([参考URL](
http://shuzo-kino.hateblo.jp/entry/2016/08/30/221758
))
##### スコープの問題
関数化、for文、if文の各所でスコープ範囲を間違えた。関数化するときにglobal変数を宣言する際、しなくてはいけないものとそうでないものの判断基準が謎だった。宣言しないと使えなかったものだけ宣言。
##### データフレーム内で重複した内容の削除
対象がニュースのため、同じ内容を取得しても意味がない。重複した内容は削除する形をとった。`inplace=True`のTrueにダブルクォートをつけてしまい、エラーになった。([参考URL](http://ailaby.com/duplicated/#id1_1))
##### CSV出力
既存ファイルを開いて追記するのではなく、別ファイルとして保存する形式をとった。そのため、`datetime`をimportしてファイル名をyymmddhhmmssの状態にした。行名は不要のため`index=False`に。ここでもFalseにダブルクォートをつけてしまい、エラーになった。
##### 最終ページの判定位置が早すぎた問題
当初は最終ページの判定if文の中で記事の取得をしていた。一見ちゃんと動いていたが、実は最終ページで記事の取得前にwhileを抜けていたことが判明。ネストを解除し、記事取得の動作後に最終ページの判定をした。

# 全体のコード
```python:
# coding: UTF-8
from selenium.webdriver import Chrome, ChromeOptions
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import pandas as pd
import requests
import datetime
import time

options = ChromeOptions()
options.add_argument("--headless")
browser = Chrome(executable_path="chromedriverのある場所のパス", options=options)

originalUrl = "https://newstopics.jp/"
columns = ["title", "url"]
df = pd.DataFrame(columns=columns)


def get_article(category):
    url = urljoin(originalUrl, category)
    browser.get(url)
    page = 1
    global df

    while True:
        html = requests.get(url)
        soup = BeautifulSoup(html.content, 'html.parser')
        nextpage = soup.find_all("a", {"rel": "next"})
        print("Starting to get posts... page: {}".format(page))

        div = soup.find("div", {"class": "content_list"})
        tags = div.find_all("div", {"class": "list_item"})

        for tag in tags:
            title = tag.find("a", {"class": "title"}).text
            url = tag.find("div", {"class": "comment"}).find("a").get("href")
            se = pd.Series([title, url], columns)
            df = df.append(se, columns)

        if len(nextpage) > 0:
            nextpage_url = soup.find("a", {"rel": "next"}).get("href")
            url = urljoin(originalUrl, nextpage_url)
            print("next url:{}".format(url))
            browser.get(url)
            page += 1
            browser.implicitly_wait(10)
            print("Moving to next page......")
            time.sleep(10)

        else:
            print("no pager exist anymore")
            break


get_article("categories/soft")
get_article("categories/hard")

print("Deleting duplicate rows...")
df.drop_duplicates(subset="title", inplace=True)

today = datetime.datetime.now().strftime("%y%m%d%H%M%S")
filename = today + ".csv"
df.to_csv(filename, index=False, encoding='utf-8')
print("DONE")

browser.quit()
```

## アドバイスを貰ったコード
```python:
# coding: UTF-8
from selenium.webdriver import Chrome, ChromeOptions
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import pandas as pd
import requests
import datetime
import time

options = ChromeOptions()
options.add_argument("--headless")
browser = Chrome(
    executable_path="chromedriverのある場所のパス", options=options)

class Newstopics:
    URL = "https://newstopics.jp/"

    def __init__(self, originUrl=None, verbose=False, debug=False):
        self.originUrl = originUrl or Newstopics.URL
        self.verbose = verbose
        self.debug = debug
        self.columns = ["title", "url"]
        self.data = pd.DataFrame(columns=self.columns)

    def scrape(self, path):
        page = 0
        while path:
            page += 1
            if self.verbose:
                print(f" page: {page} ".center(50, "#"))
                print("Starting to get posts...")
            path = self.scrape_page(path)
            if path:
                if self.verbose:
                    print(f"next path: {path}")
                    print("Moving to next page......")
                time.sleep(10)  # サイトへの負荷軽減(10秒待機)
            else:
                if self.verbose:
                    print("no pager exist anymore")
        if self.verbose:
            print("DONE")

    def scrape_page(self, path):
        url = urljoin(self.originUrl, path)
        html = requests.get(url)
        soup = BeautifulSoup(html.content, 'html.parser')
        self.stock_title_and_url(soup)
        return self.next_path(soup)

    def stock_title_and_url(self, soup):
        div = soup.find("div", {"class": "content_list"})
        tags = div.find_all("div", {"class": "list_item"})
        for tag in tags:
            title = tag.find("a", {"class": "title"}).text
            url = tag.find("div", {"class": "comment"}).find("a").get("href")
            row = pd.Series([title, url], self.columns)
            self.data = self.data.append(row, self.columns)
            if self.debug:
                print(row)

    def next_path(self, soup):
        nexttag = soup.find("a", {"rel": "next"})
        return nexttag and nexttag.get("href")

    def uniq(self):
        if self.verbose:
            print("Deleting duplicate rows...")
        self.data.drop_duplicates(subset="title", inplace=True)

    def save(self, filename, encoding='utf-8'):
        self.data.to_csv(filename, index=False, encoding=encoding)


def main():
    newstopics = Newstopics(verbose=True, debug=True)
    try:
        newstopics.scrape("categories/soft")  # ゆるいニュース
        newstopics.scrape("categories/hard")  # かたいニュース
    except KeyboardInterrupt:
        pass                                  # 中断されても保存する
    newstopics.uniq()                         # 重複行削除
    filename = datetime.datetime.now().strftime("%y%m%d%H%M%S") + ".csv"
    newstopics.save(filename)                 # 今日の日付をファイル名にして保存
    browser.quit()                            # ブラウザを終了する

if __name__ == '__main__':
    main()
```
##### 調べたこと
- [Python - `if __name__ == '__main__':` の意味](http://azuuun-memorandum.hatenablog.com/entry/2015/05/09/002549)
- プロゲートで復習してみたものの、関数、クラス、メソッドの造りはまだまだ理解できていない。
- verbose, debugは調べてみたものの、付けることでなにが出来ているのかよく分からない。また今度調べる。