101実験室

あそんでわくわく、つくってわくわく

アイドルグループのTwitterアカウントをデータベース化する [Python][PostgreSQL][beautifulsoup]

つくるもの

主成分分析するために、アイドルグループのTwitterアカウントをデータベース化する。

f:id:shkemok3:20180716215520p:plain
▲データベースの構造

処理の流れ

おおまかな流れとしては、

  → Wikipediaのグループ個別ページのURLをデータベース化

  → TwitterアカウントのURLをデータベース化

なお、アイドルグループには1から順にidを割り振り管理する。
テーブルは後から項目を追加しやすいように4つに分割した。

テーブルの説明

データベース名 : iddata

  • idol_group_name

 アイドルグループの名前を管理するテーブル

  • idol_group_wiki_url

 アイドルグループのwikipediaのURLを管理するテーブル

  • not_idol_group_wiki_url

 アイドルグループのwikipediaのURLでないものを管理するテーブル
 再度スクレイピングするときに、このURLを弾くようにする

 アイドルグループのTwitterのURLを管理するテーブル
 公式アカウントのaccount_typeを「official」とし、それ以外は「other」とする
 「official」はグループの代表アカウントにするため、1つのみとする
 

準備

動作環境

データベース

データベース管理ソフトのPostgreSQLをインストール

自分は下記サイトを参考にした。

データベースをGUI管理するpgadmin 4 をインストール

pgadmin 4 : download

Python環境設定

  • Python3系をインストール (Anacondaを使うとよい)
  • Pythonの各種ライブラリをインストール

方法

データベースを作成

「iddata」という名前のデータベースを作成する

コマンドプロンプトで以下のように入力する。

psql -U postgres
pass   #postgreSQLインストール時に設定したPASSを入力
create database iddata;

もしくは、下記のdatabase.pyのif __name__ == '__main__':以下のコメントアウトを外して実行する。

データベースの確認

pgadmin 4を開いて、以下のようにiddataというデータベースが作成されていることを確認する。

f:id:shkemok3:20180728172541p:plain

スクリプトを作成

「iddata」など適当に名前をつけてフォルダを作成し、直下に以下のスクリプトを入れる

  • iddata
    • database.py
    • wiki_crawler.py
database.py
  • これはpythonからpostgreSQLへ接続するときに使用する。
  • password="" には、postgreSQLインストール時のものを入力しておく。
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import psycopg2

class DB():
    def __init__(self, dbname):
        # データベースに接続
        self.conn = psycopg2.connect(host="localhost", dbname=dbname, user="postgres", password="password")
        self.cur = self.conn.cursor()

    # データベースを新規作成
    def create_db(self, new_dbname):
        # ※データベース新規作成時は、dbname="postgres"などの既存のデータベースに接続すること
        self.conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
        self.cur = self.conn.cursor()
        self.cur.execute('CREATE DATABASE %s' % new_dbname)
        self.conn.commit()

    # SQL実行処理 (CREATE, DROP, DELETE, UPDATE,..など)
    def execute_sql(self, sql):
        self.cur.execute(sql)
        self.conn.commit()

    # リスト型データの挿入処理
    # 例 insert('INSERT INTO twiurl (idol_group_id,twitter_name,twitter_url,account_type) VALUES (%s,%s,%s,%s)', [1002, 'myidol', 'https://hoge.com', 'other'])
    def insert(self, sql, list):
        self.cur.execute(sql, list)
        self.conn.commit()

    # リスト型データの取得処理
    # 例 select('SELECT ota_follow_id, follow_num FROM otafollow WHERE idol_group_id = %s ORDER BY follow_num DESC LIMIT 100' % (idol_group_id))
    def select(self, sql):
        self.cur.execute(sql)

        # tupleをlistに変換した形式で返す [(a,b),(c,d),..] => [[a,b],[c,d],..]
        rows = [list(i) for i in self.cur.fetchall()]

        # 1次元のリストならネストを削除 [[1],[2],[3]] => [1, 2, 3]
        if len(rows) >= 1:
            if len(rows[0]) == 1:
                values = list()
                for l in rows:
                    values.append(l[0])
                return values

        return rows

    # データベースを閉じる処理
    def close(self):
        self.cur.close()
        self.conn.close()

if __name__ == '__main__':
    
    """ データベース新規作成時に実行
    db = DB("postgres")
    db.create_db("iddata")
    db.close()
    """
wiki_crawler.py

これは、WikipediaからアイドルグループのWikipediaURLとTwitterURLをスクレイピングする
WikiClawl()クラスをインスタンス化し、その中の以下のメソッドを実行している

  • idol_group_wiki_url()で個別アイドルグループのWikipediaURLを取得
    • アイドルグループのリストが入っている<div>タグをclass名を指定して抜き出し、さらにそこから<a>タグを抜き出す
    • <a>タグからURLとタイトルを抜き出し、データベースへ挿入
    • ひとつずつ手作業で確認し、y (登録)/ n(登録しない) / skip (スキップ)で処理を選択
  • idol_group_twitter_url()で個別アイドルグループのTwitterURLを取得
    • データベースから個別アイドルグループのWikipediaURLを取得
    • 個別ページをクロールし、twitter.comを含む<a>タグを抜き出す
    • 複数のURLがある場合は["公式", "運営", "オフィシャル", ..]などが含まれるものを優先的にソートする
    • ひとつずつ手作業で確認し、y(officialアカウントとして登録) / n(otherアカウントとして登録)で処理を選択
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
from bs4 import BeautifulSoup
import re
import difflib
from database import DB
from urllib.parse import urljoin

class WikiCrawl():
    def __init__(self):
        pass

    # アイドルグループのwikipediaのURLを取得
    def idol_group_wiki_url(self):
        db = DB('iddata')
        # 女性アイドルグループのURL
        base_url = 'https://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC%E3%81%AE%E5%A5%B3%E6%80%A7%E3%82%A2%E3%82%A4%E3%83%89%E3%83%AB%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%81%AE%E4%B8%80%E8%A6%A7'
        r = requests.get(base_url)
        content = r.content
        soup = BeautifulSoup(content, 'html.parser')

        # すべての該当クラスの<div>タグをリストで返す ex. [<div class="hoge">~</div>, <div>~</div>,...,<div>~</div>]
        divs = soup.find_all('div', class_='div-col columns column-count column-count-2')

        # 各<div>タグの要素から<a>タグを抜き出し、グループコード,グループ名,URLを抜き出す(80,90年代はパス)
        for div in divs[2:]:
            idol_groups = div.find_all('a')
            for idol_group in idol_groups:

                # 相対パスを絶対パスに変換して取得
                url = urljoin(base_url, idol_group.get('href'))
                name = idol_group.text

                # データベースに登録済みか確認
                pass_url = list()
                pass_url.extend(db.select('SELECT url FROM idol_group_wiki_url;'))
                pass_url.extend(db.select('SELECT url FROM not_idol_group_wiki_url;'))
                if url in pass_url:
                    continue

                # idol_group_idを設定
                max_id = db.select('SELECT MAX(idol_group_id) FROM idol_group_name;')[0]
                if max_id is None:
                    id = 1
                else:
                    id = max_id + 1

                # データベースへの登録処理
                print(id, name, url)
                command = input('新規アイドルグループに登録しますか? (y/n/skip) >>')
                if command is 'y':
                    db.insert('INSERT INTO idol_group_name (idol_group_id, idol_group_name) VALUES (%s,%s)', [id, name])
                    db.insert('INSERT INTO idol_group_wiki_url (idol_group_id, url) VALUES (%s,%s)', [id, url])
                    print('登録しました')
                elif command is 'n':
                    db.insert('INSERT INTO not_idol_group_wiki_url (not_idol_group_name, url) VALUES (%s,%s)', [name, url])
                    print('URLを除外リストに挿入しました')
                else:
                    print('スキップしました')

        db.close()

    # アイドルグループのtwitterのURLを取得
    def idol_group_twitter_url(self):
        db = DB('iddata')
        id_name_wikiurls = db.select('SELECT N.idol_group_id, N.idol_group_name, W.url FROM idol_group_name AS N INNER JOIN idol_group_wiki_url AS W ON N.idol_group_id = W.idol_group_id')
        for id_name_wikiurl in id_name_wikiurls:

            # wikipediaの個別アイドルグループのURLをクロール
            res = requests.get(id_name_wikiurl[2])
            content = res.content
            soup = BeautifulSoup(content, 'html.parser')

            # twitter.comを含む<a>タグをlistで取得
            twitter_a = soup.find_all('a', href=re.compile("twitter.com"))

            # <a>タグからTwitterURLとツイッター名を取得しlist化
            twitter_name_urls = list()
            for a in twitter_a:
                twitter_url = a.get('href')
                twitter_name = a.text

                # すでにURLがDBに登録されていたらスキップ
                db_twitter_url = db.select("SELECT url FROM idol_group_twitter_url WHERE idol_group_id = %s AND url = '%s'" % (id_name_wikiurl[0], twitter_url))
                if len(db_twitter_url) > 0:
                    continue

                # URLに特定の文字列が含まれていれば、スキップ
                if '/status' in twitter_url:
                    continue

                twitter_name_urls.append([twitter_name, twitter_url])

            # 追加twitter_name_urlsが空ならスキップ
            if len(twitter_name_urls) == 0:
                continue

            # twitter_name_urlsリストの各先頭にtargetsリストのマッチ度を挿入
            targets = ["公式", "運営", "オフィシャル", "スタッフ", "staff","OFFICIAL"]
            targets.append(id_name_wikiurl[1])
            for i, name_url in enumerate(twitter_name_urls):
                match_ratio = 0
                for target in targets:
                    match_ratio += difflib.SequenceMatcher(None, name_url[0], target).ratio()
                twitter_name_urls[i].insert(0, match_ratio)

            # [[match_ratio, idol_group_name, twitter_url],..]のリストをマッチ度の高い順にソート
            twitter_match_name_urls = twitter_name_urls
            twitter_match_name_urls.sort(reverse=True)
            print(id_name_wikiurl[0], id_name_wikiurl[1])
            print('データベースのTwitterURLリスト')
            print(db.select('SELECT idol_group_id, twitter_name, url, account_type FROM idol_group_twitter_url WHERE idol_group_id = %s' % id_name_wikiurl[0]))
            print('追加するTwitterURLリスト')
            print(twitter_match_name_urls)


            command = input('%sをofficialにしますか?(y/n) >>' % (twitter_match_name_urls[0]))
            if command is 'y':
                # データベース内の特定アイドルグループのtwitterURLのアカウントタイプをすべてotherに更新
                db.execute_sql("UPDATE idol_group_twitter_url SET account_type = 'other' WHERE idol_group_id = %s" % id_name_wikiurl[0])

                # TwitterURLを挿入
                for count, match_name_url in enumerate(twitter_match_name_urls):

                    # マッチ度が高いURLはofficialにして挿入
                    if count == 0:
                        db.insert('INSERT INTO idol_group_twitter_url (idol_group_id, twitter_name, url, account_type) VALUES (%s,%s,%s,%s)', [id_name_wikiurl[0], match_name_url[1], match_name_url[2], 'official'])
                        continue
                    db.insert('INSERT INTO idol_group_twitter_url (idol_group_id, twitter_name, url, account_type) VALUES (%s,%s,%s,%s)', [id_name_wikiurl[0], match_name_url[1], match_name_url[2], 'other'])
                print('officialで登録しました')
            else:
                # TwitterURLをotherにして挿入
                for match_name_url in twitter_match_name_urls:
                    db.insert(
                        'INSERT INTO idol_group_twitter_url (idol_group_id, twitter_name, url, account_type) VALUES (%s,%s,%s,%s)', [id_name_wikiurl[0], match_name_url[1], match_name_url[2], 'other'])
                print('otherで登録しました')

# テーブルの作成処理
def create_table():
    db = DB('iddata')
    db.execute_sql("CREATE TABLE idol_group_name (idol_group_id integer PRIMARY KEY, idol_group_name varchar(255)) WITH OIDS;")
    db.execute_sql("CREATE TABLE idol_group_wiki_url (idol_group_id integer PRIMARY KEY, url varchar(255)) WITH OIDS;")
    db.execute_sql("CREATE TABLE not_idol_group_wiki_url (not_idol_group_name varchar(255), url varchar(255)) WITH OIDS;")
    db.execute_sql("CREATE TABLE idol_group_twitter_url (idol_group_id integer, twitter_name varchar(255), url varchar(255), account_type varchar(255)) WITH OIDS;")
    db.close()


if __name__ == '__main__':
    # テーブルの作成処理(初回のみ実行する)
    create_table()

    # WikipediaからグループのWikipedia個別URLをスクレイピングする
    crawl = WikiCrawl()
    crawl.idol_group_wiki_url()

    # WikipediaからTwitterURLをスクレイピングする
    crawl.idol_group_twitter_url()

実行例

idol_group_wiki_url()
1 ハロー!プロジェクト https://ja.wikipedia.org/wiki/%E3%83%8F%E3%83%AD%E3%83%BC!%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88
新規アイドルグループに登録しますか? (y/n/skip) >>n
URLを除外リストに挿入しました
1 モーニング娘。 https://ja.wikipedia.org/wiki/%E3%83%A2%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0%E5%A8%98%E3%80%82
新規アイドルグループに登録しますか? (y/n/skip) >>y
登録しました
2 タンポポ https://ja.wikipedia.org/wiki/%E3%82%BF%E3%83%B3%E3%83%9D%E3%83%9D_(%E3%83%8F%E3%83%AD%E3%83%BC!%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88)
新規アイドルグループに登録しますか? (y/n/skip) >>
スキップしました

f:id:shkemok3:20180728174403p:plainf:id:shkemok3:20180728181120p:plain

idol_group_twitter_url()
15 カントリー・ガールズ
データベースのTwitterURLリスト
[]
追加するTwitterURLリスト
[[1.125, 'カントリー・ガールズ', 'https://twitter.com/countrygirls_uf']]
[1.125, 'カントリー・ガールズ', 'https://twitter.com/countrygirls_uf']をofficialにしますか?(y/n) >>y
officialで登録しました
22 ℃-ute
データベースのTwitterURLリスト
[]
追加するTwitterURLリスト
[[1.2, '℃-ute', 'https://twitter.com/Cute_upfront']]
[1.2, '℃-ute', 'https://twitter.com/Cute_upfront']をofficialにしますか?(y/n) >>y
officialで登録しました

f:id:shkemok3:20180728183446p:plain

後処理

アイドルグループ名やURLを直接編集する場合はpgadminからテーブルを開き、直接編集することができる。
このとき、左上のSaveを押さなければ保存されないので注意。

まとめ

このように、半手作業ではあるがアイドルグループのアカウントをデータベース化できた。
次回以降は、このデータを使ってアイドルグループの主成分分析をする。