【Pythonスクレイピング】特定のキーワードが含まれるサイトのURLとタイトルをテキストファイルへ出力

# 概要

Google 検索を実施し、ヒットしたページ内の `title` `og:description` `h1` ~ `h4` のいずれかに特定のキーワードが含まれる場合、対象ページのタイトルと URL をタブ区切りでテキストファイルに出力します。
URL が既にテキストファイル内に含まれている場合、処理をスキップします。

※Google はスクレイピングを禁止しているため、サーバーに負荷をかけず、個人的な範囲で、自己責任でご利用ください。
https://support.google.com/webmasters/answer/66357

# Pythonバージョン

3.8.2

# 実行手順

venv などで仮想環境を構築後、

```
pip install -r requirements.txt
python main.py
```

# コード

## requirements.txt

```text
beautifulsoup4 == 4.9.1
requests == 2.24.0
```

## settings.py

```python
settings = {
    # Google 検索に使用するキーワード
    'google_search_keywords': ['医療', 'コロナ'],

    # 検索件数
    'google_search_num': 10,

    # ヒットしたページ内から探すキーワード
    'search_keywords_in_page': ['医療']
}
```

## main.py

```python
import urllib.parse
import re

import requests
import bs4

from settings import settings
from output import OutputText


def get_ogdesc_from_soup(soup: bs4.BeautifulSoup) -> str:
    """
    BeautifulSoup インスタンスから
    <meta property="og:description" content="...">
    を探し、content の内容を返す。
    見つからなかった場合、空文字を返す。
    """
    og_desc = soup.find('meta', attrs={'property': 'og:description', 'content': True})
    if og_desc:
        return og_desc['content']
    return ''


def get_href_from_soup(soup: bs4.BeautifulSoup):
    href = soup.get('href')
    href = re.search('(http)(.+)(&sa)', href).group()[0:-3]  # 不要な文字列除去
    href = urllib.parse.unquote(href)  # デコード
    return href


def do_google_search(keywords: [str], search_num: int) -> [str]:
    """
    keywords で Google 検索を実施し、
    ヒットした URL をリストで返す
    """
    # Google 検索の実施
    url = 'https://www.google.co.jp/search'
    params = {
        'hl': 'ja',
        'num': search_num,
        'q': ' '.join(keywords)
    }
    response = requests.get(url, params=params)

    # ヒットした URL をリストで返す
    # `.kCrYT` は Google の仕様変更により修正が必要な場合あり
    soup = bs4.BeautifulSoup(response.content, 'html.parser')
    soups = soup.select('.kCrYT > a')
    return [get_href_from_soup(soup) for soup in soups]


def main():
    output_text = OutputText('output.txt')
    urls = do_google_search(settings['google_search_keywords'], settings['google_search_num'])

    for url in urls:
        # テキストファイル内に既に URL が含まれているなら処理をスキップ
        if url in output_text.get_urls():
            continue

        try:
            response = requests.get(url)
            response.encoding = 'utf-8'
            response.raise_for_status()
        except:
            # 接続エラーが発生した場合は処理をスキップ
            continue

        soup = bs4.BeautifulSoup(response.content, 'html.parser')

        titles = [a.text for a in soup.select('title')]
        desc = get_ogdesc_from_soup(soup)
        h1s = [a.text for a in soup.select('h1')]
        h2s = [a.text for a in soup.select('h2')]
        h3s = [a.text for a in soup.select('h3')]
        h4s = [a.text for a in soup.select('h4')]

        # キーワードがページ内に含まれていないなら処理をスキップ
        no_keyword = True
        for keyword in settings['search_keywords_in_page']:
            for text in titles + [desc] + h1s + h2s + h3s + h4s:
                if keyword in text:
                    no_keyword = False
        if no_keyword:
            continue

        # テキストファイルに書き込む
        title = '**No title**' if len(titles) <= 0 else titles[0].strip().replace('\n', '')
        output_text.write(title, url)

    # 読みやすい形式のテキストファイルを出力
    output_text.output_readable_file()


if __name__ == '__main__':
    main()
```

## output.py

```python
import myutil as u
import os


class OutputText:
    file_path = None

    def __init__(self, file_path):
        self.file_path = file_path

        if not os.path.isfile(file_path):
            file = open(self.file_path, 'w', encoding='utf-8')
            file.close()

    def write(self, title, url):
        with open(self.file_path, mode='a', encoding='utf-8') as f:
            u.write_with_tab(f, title, url)
            f.write('\n')

    def get_urls(self):
        lines = self.get_lines()
        return [self.get_url(line) for line in lines]

    def output_readable_file(self):
        file = self.file_path.replace('.txt', '_readable.txt')
        with open(file, mode='w', encoding='utf-8') as f:
            lines = self.get_lines()
            for line in lines:
                f.write(self.get_title(line) + '\n' + self.get_url(line) + '\n')
                f.write('\n------------------------------\n\n')

    def get_lines(self):
        with open(self.file_path, mode='r', encoding='utf-8') as f:
            text = f.read()
            lines = text.strip().split('\n')
            return lines

    def get_title(self, line):
        texts_in_line = line.split('\t')
        return texts_in_line[0] if len(texts_in_line) >= 1 else ''

    def get_url(self, line):
        texts_in_line = line.split('\t')
        return texts_in_line[1] if len(texts_in_line) >= 2 else ''
```

## myutil.py

```python
def write_with_tab(file, *strings):
    """
    ファイルにタブ区切りで文字列を書き込む
    """
    for i, string in enumerate(strings):
        file.write(string)
        if i != len(strings) - 1:  # 最後のループでないなら
            file.write('\t')
    return file
```