見出し画像

【初心者向け】Pythonでスクレイピングする環境を作る⑦ 検索結果のページのタイトルを取得する

Naruhikoです。

google検索結果のURL一覧を取得するところまでできましたね。
このコードを使ってURL先のタイトルを取得してみましょう。

取得の仕方はもうできていますね。
あとは、何を取得するかで変わってきますが、これはBeautiful soupを勉強していくことでいろいろなものが取得できるようになるはずです。

まずは、今までのコードを見てみます。

import time
from urllib.robotparser import RobotFileParser
from urllib.parse import urlparse

import requests
from bs4 import BeautifulSoup

# User-Agent
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
              AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100"

# robots.txt 用パーサー
rp = RobotFileParser()

def get_robots_txt(url):
    """ get_robots_txt
    url: robots.txt を確認するサイトURL
    """
    try:
        # robots の url 取得
        parsed_url = urlparse(url)
        robots_url = "{0.scheme}://{0.netloc}/robots.txt".format(parsed_url)
        # robots.txt 取得
        rp.set_url(robots_url)
        rp.read()
        # 取得していいか確認
        return rp.can_fetch("*", url)
    except Exception as e:
        return False

def get_html(url, params=None, headers=None):
    """ get_html
    url: データを取得するサイトのURL
    [params]: 検索サイトのパラメーター {x: param}
    [headers]: カスタムヘッダー情報
    """
    try:
        # 待機
        time.sleep(5)
        # データ取得
        resp = requests.get(url, params=params, headers=headers)
        # 要素の抽出
        soup = BeautifulSoup(resp.text, "html.parser")
        return soup
    except Exception as e:
        return None

def get_search_url(word, engine="google"):
    """ get_search_url
    word: 検索するワード
    [engine]: 使用する検索サイト(デフォルトは google)
    """
    try:
        if engine == "google":
            # google 検索
            search_url = "https://www.google.co.jp/search"
            search_params = {"q": word}
            search_headers = {"User-Agent": user_agent}
            # データ取得
            soup = get_html(search_url, search_params, search_headers)
            if soup != None:
                tags = soup.select(".r > a")
                urls = [tag.get("href") for tag in tags]
                return urls
            else:
                return None
        else:
            return None
    except Exception as e:
        return None


try:
    result = get_search_url("python")
    if result != None:
        print(result)
    else:
        print("取得できませんでした")
except Exception as e:
    print("エラーになりました")

これを実行すると、URLの一覧がリスト形式で取得できました。

root@e973f742a9b6:/workspaces/Workspaces/crawler# python crawler.py
['https://www.python.jp/', 'https://ja.wikipedia.org/wiki/Python', 'https://www.python.org/', 'https://www.atmarkit.co.jp/ait/articles/1904/02/news024.html', 'https://www.sejuku.net/blog/7720', 'https://prog-8.com/languages/python', 'https://prog-8.com/docs/python-env-win', 'https://www.javadrive.jp/python/', 'https://proengineer.internous.co.jp/content/columnfeature/12344', 'https://www.internetacademy.jp/it/programming/programming-basic/what-is-python.html']

タイトル取得

では、この取得したURLリストからサイトデータを取得して、
タイトルを表示させてみましょう。

try:
    result = get_search_url("python")
    if result != None:
        print(result)
    else:
        print("取得できませんでした")
except Exception as e:
    print("エラーになりました")

result という変数でもいいですが、ちょっと変えておきます。
url の複数形で urls にしておきます。

try:
    urls = get_search_url("python")
    if urls != None:
        print(urls)
    else:
        print("取得できませんでした")
except Exception as e:
    print("エラーになりました")

まずは取得した urls を for で回します。

if urls != None:
    for url in urls:
        print(url)

リスト型だったデータが一覧になりました。

root@e973f742a9b6:/workspaces/Workspaces/crawler# python crawler.py
https://www.python.jp/
https://ja.wikipedia.org/wiki/Python
https://www.python.org/
https://www.atmarkit.co.jp/ait/articles/1904/02/news024.html
https://www.sejuku.net/blog/7720
https://prog-8.com/languages/python
https://prog-8.com/docs/python-env-win
https://www.javadrive.jp/python/
https://proengineer.internous.co.jp/content/columnfeature/12344
https://www.internetacademy.jp/it/programming/programming-basic/what-is-python.html

url のなかにそれぞれのアドレスが入ります。
そのアドレスを get_html() でさらにデータを取得してみましょう。
取得したデータのタイトルもそのまま取得します。

if urls != None:
    for url in urls:
        soup = get_html(url)
        print(soup.title.get_text())
root@e973f742a9b6:/workspaces/Workspaces/crawler# python crawler.py
Top - python.jp
Python - Wikipedia
Welcome to Python.org
PythonÁÄÇñȾêÈÌH (1/2)FPythonüå - IT
Pythonã¨ã¯ï¼¨èª®ç¹å¾´ããå­¦ç¿æ³ã¾ã§åè
åãã«ãããã
       ãã解説 | ä¾ã¨ã³ã¸ãã¢å¡¾ãã­ã°ï¼Samurai Blogï¼ - ãã­ã°ã©ã³ã°å
¥éè
åããµã¤ã
Python
| プログラミングの入門なら基礎から学べるProgate[プロゲート]
Pythonの開発環境を用意しよう!(Windows)
| プログラミングの入門なら基礎から学べるProgate[プロゲート]
Pythonå
¥é ï½®ã¤ã³ã¹ãã¼ã«æ¹æ³ã
                     Pythonã使ã£ãã­ã°ã©ã³ã°ã®æ¹æ³ã«ã¤ã
                                                       ã¦è§£èª¬ãã¾ãï½門者はココで勉強しよう!学習サイト最強6選【2019年最新】 | サービス | プロエンジニア
Pythonã¨ã¯ï¼¤§äººæ°ãã­ã°ã©ã³ã°è¨èª®ã¡ãªããã
                                         æ´»ç¨äºä¾ããç´¹ä»

タイトルそれぞれのタイトルタグが取得できましたね。
しかし文字化けしているデータも取得してしまいました。

requests には文字コードを調整してくれるものがあります。
get_html() 内の requests.get した後にすぐ下記のコードを入れておきます。

# データ取得
resp = requests.get(url, params=params, headers=headers)
resp.encoding = resp.apparent_encoding
root@e973f742a9b6:/workspaces/Workspaces/crawler# python crawler.py
Top - python.jp
Python - Wikipedia
Welcome to Python.org
Pythonってどんな言語なの? (1/2):Python入門 - @IT
Pythonとは?言語の特徴から学習法まで初心者向けにわかりやすく解説 | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
Python
| プログラミングの入門なら基礎から学べるProgate[プロゲート]
Pythonの開発環境を用意しよう!(Windows)
| プログラミングの入門なら基礎から学べるProgate[プロゲート]
Python入門 ~Pythonのインストール方法やPythonを使ったプログラミングの方法について解説します~ | PythonWeb
Python入門者はココで勉強しよう!学習サイト最強6選【2019年最新】 | サービス | プロエンジニア
Pythonとは?大人気プログラミング言語のメリットや活用事例をご紹介

きちんとした文字がでてくれました。

robots.txt 対応をしておく

前回作った robots.txt 対応関数の get_robots_txt() を適用させておきます。
もし、get_robots_txt(url) が True だったら、データを取得します。
False だったら拒否されたことを表示させます。

if get_robots_txt(url):
    soup = get_html(url)
    print(soup.title.get_text())
else:
    print("クロールが拒否されました [{}]".format(url))
root@e973f742a9b6:/workspaces/Workspaces/crawler# python crawler.py
Top - python.jp
Welcome to Python.org
Python - Wikipedia
Pythonってどんな言語なの? (1/2):Python入門 - @IT
クロールが拒否されました [https://www.sejuku.net/blog/7720]
クロールが拒否されました [https://prog-8.com/languages/python]
クロールが拒否されました [https://prog-8.com/docs/python-env-win]
クロールが拒否されました [https://www.javadrive.jp/python/]
クロールが拒否されました [https://proengineer.internous.co.jp/content/columnfeature/12344]
クロールが拒否されました [https://qiita.com/tags/python]

結構拒否されましたね。
有名所は結構クロール拒否されます。
ココらへんをどうするかは自己責任でお願いします。

ここまでくれば、requests でのスクレイピングがどのような感じか理解できたのではないでしょうか。

今の時点での基本部分のコードはこうなりました。

try:
    urls = get_search_url("python")
    if urls != None:
        for url in urls:
            if get_robots_txt(url):
                soup = get_html(url)
                print(soup.title.get_text())
            else:
                print("クロールが拒否されました [{}]".format(url))
    else:
        print("取得できませんでした")
except Exception as e:
    print("エラーになりました")

エラー時の対応をもう少し考える(ログとか)

try/except でのエラー時対処は None を返すことでした。

except Exception as e:
    return None

None が返ってくれば失敗したんだなとわかるのですが
結局どんな内容のエラーなのかがわからないですよね。

なんで None が返ってくるんだろうと悩んでもわからないんです。
このようなときにどうするかちょっと考えてみましょう。

get_html()を考えた場合、
この関数に求めるものは url 先の html データの取得です。
データがあるか、ないか。
もし、データがなかったときはなぜデータがとれなかったんでしょうか。

def get_html(url, params=None, headers=None):
    """ get_html
    url: データを取得するサイトのURL
    [params]: 検索サイトのパラメーター {x: param}
    [headers]: カスタムヘッダー情報
    """
    try:
        # 待機
        time.sleep(5)
        # データ取得
        resp = requests.get(url, params=params, headers=headers)
        resp.encoding = resp.apparent_encoding
        # 要素の抽出
        soup = BeautifulSoup(resp.text, "html.parser")
        return soup
    except Exception as e:
        return None

ここでのエラーになる条件はなんでしょうか。
・requests.get でデータが取得できなかった。
・beautiful soup で要素が抽出できなかった。
このくらいでしょうか。

この関数では、どんなエラーが来たとしても None を返すようにしています。
しかし関数によっては、エラーを raise で渡す場合があります。

except Exception as e:
    raise Exception(e.args[0])

こうすることで、エラー内容をエラーとして返します。
この場合は関数を使用した側でエラーの対応をする必要があります。

しかし今回はエラーを渡してエラー画面を表示させたりすることはないので、対応としては処理とは関係なくログとして吐き出したほうがいいと思いつきます。

ログの吐き出し方は logging モジュールを使うというのが一般的でしょう。
開発時のログレベルは DEBUG になります。

from logging import basicConfig, getLogger, DEBUG

# ログレベル
basicConfig(level=DEBUG)

コンソールに渡す場合は、

logger = getLogger(__name__)
logger.warning(e)

などとして、値は None で返すが、コンソール画面ではエラー内容を表示させておきます。

root@e973f742a9b6:/workspaces/Workspaces/crawler# python crawler.py
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): googlea.com:443
WARNING:urllib3.connection:Certificate did not match expected hostname: googlea.com. Certificate: {'subject': ((('jurisdictionCountryName', 'US'),), (('jurisdictionStateOrProvinceName', 'Arizona'),), (('businessCategory', 'Private Organization'),), (('serialNumber', 'R17247303'),), (('countryName', 'US'),), (('stateOrProvinceName', 'Arizona'),), (('localityName', 'Scottsdale'),), (('organizationName', 'Special Domain Services, LLC'),), (('commonName', 'shortener.secureserver.net'),)), 'issuer': ((('countryName', 'US'),), (('stateOrProvinceName', 'Arizona'),), (('localityName', 'Scottsdale'),), (('organizationName', 'Starfield Technologies, Inc.'),), (('organizationalUnitName', 'http://certs.starfieldtech.com/repository/'),), (('commonName', 'Starfield Secure Certificate Authority - G2'),)), 'version': 3, 'serialNumber': 'EF5927289A9FDADB', 'notBefore': 'Sep 26 22:40:51 2018 GMT', 'notAfter': 'Sep 26 22:40:51 2020 GMT', 'subjectAltName': (('DNS', 'shortener.secureserver.net'), ('DNS', 'www.shortener.secureserver.net')), 'OCSP': ('http://ocsp.starfieldtech.com/',), 'caIssuers': ('http://certificates.starfieldtech.com/repository/sfig2.crt',), 'crlDistributionPoints': ('http://crl.starfieldtech.com/sfig2s3-1.crl',)}
WARNING:__main__:HTTPSConnectionPool(host='googlea.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError("hostname 'googlea.com' doesn't match either of 'shortener.secureserver.net', 'www.shortener.secureserver.net'")))

ログの内容やレベル、出力先などは詳しく設定することが可能です。

ここなどを見ると勉強になりますので見ておいたほうがいいですね。

今の状態で DEBUG にするとコンソールが大変なことになりますので、
リリース時などは basicConfig(level=DEBUG) を変更しておくことをおすすめします。

ログの出力フォーマット

ログを出力する時にもうひと手間かけたいと思います。
それは、ログのフォーマットです。

ログフォーマットを定義して、basicConfig に渡します。

# ログフォーマットを定義
formatter = "[%(asctime)s][%(name)s][%(levelname)s]%(message)s"
# ログレベル
basicConfig(level=DEBUG, format=formatter)

このフォーマットにすると、エラーになったときには下記のように表示されます。

[2020-02-16 15:55:06,081][get_html()][WARNING]HTTPSConnectionPool(host='googlea.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError("hostname 'googlea.com' doesn't match either of 'shortener.secureserver.net', 'www.shortener.secureserver.net'")))

このフォーマットは自分の好きなように作り変えてください。

同じ return をどうにかしたい

get_search_url() のところで、少し気になるところがあります。

            else:
               return None
       else:
           return None
   except Exception as e:
       logger.warning(e)
       return None

return None がたくさんあるんです。

これって、どこの return None でもエラーでデータなしということです。
なんとかできないでしょうか。

よく見ると、logger が except 部にしかありません。
上2つにも入れてもいいのですが、
最善策は、上の2つの return もエラーとして渡してしまうことです。

            else:
               raise Exception("No Data")
       else:
           raise Exception("No Engine")
   except Exception as e:
       logger.warning(e)
       return None

こうすることで、return を一つにして logger にも出力できるようになります。

最後に

今回は作り方の例として、Google検索した結果先のサイトのタイトルと表示させるところと、ログ出力のところまでしました。

自分でやりたかったことがあれば参考にして作ってみてください。

規模が大きくなってくれば来るほどログはファイルに保存しておいた方がいいです。今回はファイルに保存するとか logging についての詳しい説明はしませんでした。
また機会があれば説明したいと思います。

最後に、ここまでのコードを載せておきます。

import time
from urllib.robotparser import RobotFileParser
from urllib.parse import urlparse
from logging import basicConfig, getLogger, DEBUG

import requests
from bs4 import BeautifulSoup

# User-Agent
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
             AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100"

# ログフォーマットを定義
formatter = "[%(asctime)s][%(name)s][%(levelname)s]%(message)s"
# ログレベル
basicConfig(level=DEBUG, format=formatter)

# robots.txt 用パーサー
rp = RobotFileParser()

def get_robots_txt(url):
    """ get_robots_txt
    url: robots.txt を確認するサイトURL
    """
    logger = getLogger("get_robots_txt()")
    try:
        # robots の url 取得
        parsed_url = urlparse(url)
        robots_url = "{0.scheme}://{0.netloc}/robots.txt".format(parsed_url)
        # robots.txt 取得
        rp.set_url(robots_url)
        rp.read()
        # 取得していいか確認
        return rp.can_fetch("*", url)
    except Exception as e:
        logger.warning(e)
        return False

def get_html(url, params=None, headers=None):
    """ get_html
    url: データを取得するサイトのURL
    [params]: 検索サイトのパラメーター {x: param}
    [headers]: カスタムヘッダー情報
    """
    logger = getLogger("get_html()")
    try:
        # 待機
        time.sleep(5)
        # データ取得
        resp = requests.get(url, params=params, headers=headers)
        resp.encoding = resp.apparent_encoding
        # 要素の抽出
        soup = BeautifulSoup(resp.text, "html.parser")
        return soup
    except Exception as e:
        logger.warning(e)
        return None

def get_search_url(word, engine="google"):
    """ get_search_url
    word: 検索するワード
    [engine]: 使用する検索サイト(デフォルトは google)
    """
    logger = getLogger("get_search_url()")
    try:
        if engine == "google":
            # google 検索
            search_url = "https://www.google.co.jp/search"
            search_params = {"q": word}
            search_headers = {"User-Agent": user_agent}
            # データ取得
            soup = get_html(search_url, search_params, search_headers)
            if soup != None:
                tags = soup.select(".r > a")
                urls = [tag.get("href") for tag in tags]
                return urls
            else:
                raise Exception("No Data")
        else:
            raise Exception("No Engine")
    except Exception as e:
        logger.warning(e)
        return None


try:
    urls = get_search_url("python")
    if urls != None:
        for url in urls:
            if get_robots_txt(url):
                soup = get_html(url)
                print(soup.title.get_text())
            else:
                print("クロールが拒否されました [{}]".format(url))
    else:
        print("取得できませんでした")
except Exception as e:
    print("エラーになりました")

---

気に入っていただけたら、フォローや好きをお願いします!

note 連載目次

【初心者向け】Pythonでスクレイピングする環境を作る① はじめに
【初心者向け】Pythonでスクレイピングする環境を作る② Dockerの使い方
【初心者向け】Pythonでスクレイピングする環境を作る③ VSCodeでDocker環境を構築する
【初心者向け】Pythonでスクレイピングする環境を作る④ requestsでデータを取得してみる
【初心者向け】Pythonでスクレイピングする環境を作る⑤ Google検索をしてみる
【初心者向け】Pythonでスクレイピングする環境を作る⑥ スクレイピングでの注意事項
【初心者向け】Pythonでスクレイピングする環境を作る⑦ 検索結果のページのタイトルを取得する
【初心者向け】Pythonでスクレイピングする環境を作る⑧ クラスにまとめてみる
【初心者向け】Pythonでスクレイピングする環境を作る⑨ テストしてみる
【初心者向け】Pythonでスクレイピングする環境を作る⑩ crawler と scraper を分ける

ここから先は

0字

¥ 100

この記事が気に入ったらサポートをしてみませんか?