こうかの雑記

こうかの雑記

昔の懐かしいこと、ubuntuのこと、その他いろいろ

はてなブロググループの記事一覧を作成する

「はてなブログ」の新着を知りたい」という記事を昨年末にアップしました。これはこれで使いみちがあります。その後、「はてなブログ グループをスクレイピングして調べてみた」という記事を書いています。

 このスクレイピングが「はてなブログ」の新着を知るのに便利なので、今はこちらを主に使っています。今回の記事でこの時のpython3スクリプトを載せます。

 「はてなブログ グループ」に参加していて、python3が使える環境をお持ちの方でしたら利用できる筈です。ただ私の手元にWindowsのpython3とExcelが無いものですから、linux 環境で書かせて頂きます。多分、スクリプトの起動方法が違うだけで動かせると思います。

機能

 自分が参加している「はてなブログ グループ」で新しくアップされた記事のURL、記事タイトル等を取り出して記事一覧をCSVファイルとして出力します。

 出力されたCSVファイルを表計算ソフトに取り込んで一覧で確認すると興味のある記事を見つけやすくなります。また表計算ソフトにはハイパーリンクの機能がありますので、見つけたページを楽に閲覧することができます。

スクリプトの説明

必要なライブラリー

 スクレイピングについて書かれている多くの記事ではBeautifulSoupが紹介されていますが、できるだけ標準ライブラリーだけにしたかったので、HTMLParserを使っています。

ログイン情報

 自分が参加しているグループにアクセスするためにログインする必要が有ります。ログインに必要なIDとパスワードはスクリプト内に埋め込みます。外部ファイルにしても良かったけれど、管理の必要がない埋め込みにしました。

 埋め込みはテキストエディタスクリプトを編集して下さい。

 この記事の末尾にあるスクリプト最後の方の行に記述してある次の文の文字列、はてなIDパスワードを自分のものに置き換えて下さい。

if __name__ == '__main__':  
    myid       = 'はてなID'
    mypassword = 'パスワード'

仕様

  • python3で実行できます。
  • 対象グループはログインしたはてなIDで参加しているグループ全部です。
    無料で参加できるグループ数は3つだったと思います。
  • グループに含まれているタイトルは当日分と前日分の2日分のみを取り出します。
    全部を対象とすると大量になりすぎる恐れがあるため制限を掛けています。
  • それでも大量になる可能性があるので念の為、それぞれのグループ内のページ数を100件に制限しています。
  • 1ページあたり10件の記事になります。
  • サーバーに負荷を掛けないように1ページ取り込む毎に0.3秒の待機時間を入れています。
  • 非公開になっているブログが見つかった場合は無視し、出力されません。
  • 出力は項目をカンマで区切ったCSV形式で出力します。
  • 結果は現在いるカレントディレクトリに、ファイル名"Group参加blog一覧.csv"で出力されます。

使い方

 linuxの場合はスクリプトファイルに実行権限を与えておきます。そうするとGUIスクリプトファイルのアイコンをダブルクリックするだけで起動できます。

f:id:koukaforest:20200723164022p:plain
実行時の画面
 

表計算(Calc)での使い方

 "Group参加blog一覧.csv"をダブルクリックするとCalcが起動します。WindowsならExcelが起動します。Calcしか持ってないのでCalcで説明しますがExcelでも同等のことが出来ると思います。

Calcの場合

 Calcが起動するとテキストのインポート画面になり、読み込むことが出来ます。linuxの場合はエンコードUnicode(UTF-8)で読み込めます。たまにWindowsで作られたファイルを扱うとShift-JISまたはWindows-932になっている場合が有り、そのまま読み込むと文字化けすることがあります。その場合はUnicode(UTF-8)にして読み込みます。

 読み込むとセル幅が調整されてなくてすべての列が表示されないので、セル幅調整をします。
 項目は次の様になっています。

  グループ名, url, id, ブログ名, 記事日付, 記事URL, 記事タイトル

f:id:koukaforest:20200723165700p:plain
列幅調整後のCalc画面
 実際には、url, id, ブログ名は必要ないと言えば必要ありません。最低限、記事日付、記事URL,記事タイトルが見えるようにします。

 記事URLから該当記事を訪れるには、記事URLにハイパーリンクをつかっています。設定する操作が必要です。

  • 記事URLのセル内のURL文字列の最後にカーソルを起きます。
  • そのままマウスでクリックしたままに他のセル上に移動してクリックするとハイパーリンクが設定されて記事URL内の文字列の色が水色変わります。
  • ハイパーリンクが設定されると、Ctrlキーを押しながら記事URLのセルをクリックすると目的のブログが開きます。

 同じIDの人の記事が複数のグループに現れる場合が有ります。並べ替えるなどして適当に処置して下さい。

スクリプト

 以下のコードを取り出し、テキストファイルに保存した後、実行権限を付与して下さい。
 ファイル名はget_hatena_group.pyとしました。


#!/usr/bin/env python3
"""
get_hatena_group.py

はてなブログの「読者参加しているグループ」の一覧を作成する
Created on Wed Jan 15 11:59:45 2020
@author: koukaforest
"""

import requests
import time
from html.parser import HTMLParser
import csv
import datetime

sess = ''
login = ''

'''
    ログイン
'''
def HatenaLogin(myid, mypassword):
    global sess, login
    login_url = 'https://www.hatena.ne.jp/login?via=200125'
    login_data = {'name': myid, 'password': mypassword, }
    sess  = requests.session()
    sess.get(login_url)
    try:
        login = sess.post(login_url, data = login_data)
    except Exception as err:
        print('login 失敗!', err)
        return False
    
    return True

'''
    各グループのページのパース
'''
class GroupHTMLParser(HTMLParser):
    def __init__(self):
        HTMLParser.__init__(self)
        self.li          = False
        self.a_user_id   = False
        self.a_blog_name = False
        self.a_title     = False
        self.span_time   = False
        self.span_title  = False
        self.time        = False
        self.p_more      = False
        self.nextpage    = ''
        self.blog_list   = []
        self.i           = -1
        
    def handle_starttag(self, tag, attrs):
        attrs = dict(attrs) # タプルだと扱いにくいので辞書にする
        if tag == 'li':
            if ('class' in attrs) :
                if attrs['class'] == 'hentry':
                    self.li = True
        if tag == 'a' and self.li :
            if ( 'class' in attrs) :
                if attrs['class'] == 'user-id':
                    # ブログトップurlの取り出し
                    self.blog_list.append([attrs['href']])
                    self.i = self.i + 1
                    self.a_user_id = True
                if attrs['class'] == 'blog-name':
                    self.a_blog_name   = True
                if attrs['class'] == "btn btn-large btn-full":

                    if self.p_more:
                        self.nextpage = attrs['href']
            else:
                if self.span_title:
                    # 記事urlの取り出し
                    self.blog_list[self.i].append(attrs['href']) 
                    self.a_title = True
        if tag == 'a' and (self.li == False):
            if ('class' in attrs) :
                if attrs['class'] == "btn btn-large btn-full":
                    if self.p_more:
                        self.nextpage = attrs['href']
        if tag == 'span' and self.li:
            if ('class' in attrs) :
                if attrs['class'] == 'blog-timestamp':
                    self.span_time = True
                if attrs['class'] == 'blog-entry-title':
                    self.span_title = True
        if tag == 'time' and self.li and self.span_time:
            if ('class' in attrs):
                # 更新日の取り出し
                #self.blog_list[self.i].append(attrs['class'])
                if attrs['class'] == 'updated':
                    self.time = True
        if tag == 'p' and ('class' in  attrs ) :
            if attrs['class']== 'more-blogs more':
                self.p_more = True
                
    def handle_endtag(self, tag):
        if tag == 'li' and self.li :
            self.li = False
        if tag == 'a' :
            self.a_user_id   = False
            self.a_blog_name = False
            self.a_title     = False
        if tag == 'span' :
            self.span_time  = False
            self.span_title = False
        if tag == 'p' :
            self.p_more     = False
        if tag == 'time' and self.span_time:
            self.time = False

    def handle_data(self, data):
        if self.a_user_id:
            # ユーザーIDの取り出し
            self.blog_list[self.i].append(data)
        if self.a_blog_name:
            # ブログ名の取り出し
            self.blog_list[self.i].append(data)
        if self.a_title :
            # 記事タイトルの取り出し
            self.blog_list[self.i].append(data)
        if self.time :
            # 更新日の取り出し
            self.blog_list[self.i].append(data[0:10])

    def get_list(self):
        return self.blog_list
    
    def get_nextpage_url(self):
        return self.nextpage
'''
    「読者参加しているグループ」のページのパース
'''
class GrouplistHTMLParser(HTMLParser):
    def __init__(self):
        HTMLParser.__init__(self)
        self.div  = False
        self.a       = False
        self.group_list = []
        self.i = -1
        
    def handle_starttag(self, tag, attrs):
        attrs = dict(attrs) # タプルだと扱いにくいので辞書にする
        if tag == 'div':
            if ('class' in attrs) :
                if attrs['class'] == 'circle-name':
                    self.div = True
        if tag == 'a' and self.div:
            # グループのURL
            self.group_list.append([attrs['href']])
            self.i = self.i + 1
            self.a = True

    def handle_endtag(self, tag):
        if tag == 'div' :
            self.div = False
        if tag == 'a' :
            self.a = False
            
    def handle_data(self, data):
        if self.a :
            # グループ名
            self.group_list[self.i].append(data)
        
    def get_list(self):
        return self.group_list


'''
    「参加しているグループ」からリストを取り出す。
'''
def get_group_list(url):
    lst = []
    # 今日の年月を取得して前日を取得設定
    date1  = datetime.datetime.now() - datetime.timedelta(days=1)
    date_ymd1 = datetime.datetime.date(date1)

    while url != '':
        try:
            r = sess.get(url, timeout=3.5)  #timeout 3.5sec
        except ConnectionError:
            print('reqestsで接続エラー! url=', url)
            return []
        print('「参加しているグループ」に接続しました')
        htmlbody=r.text
        parser = GrouplistHTMLParser()
        parser.feed(htmlbody)
        parser.close()
        for l in parser.get_list():
            lst.append(l)
        url=''

    group_blog_list=[]
    for l in lst:
        page_cnt = 0
        url = l[0]
        group_name  = l[1]
        while url != '' :
            if page_cnt < 100:
                page_cnt = page_cnt + 1
            else :
                break

            try:
                r = sess.get(url, timeout=3.5)
            except ConnectionError:
                print('reqestsで接続エラー! url=', url)
                return []
            print(group_name, 'のページ', page_cnt, 'に接続しました。')
            htmlbody=r.text
            parser = GroupHTMLParser()
            parser.feed(htmlbody)
            parser.close()
            blogs = parser.get_list()
            time.sleep(0.3)
            change_sw = False
            for x in blogs:
                x.insert(0, l[1])
                '''
                0:グループ, 1:ブログurl, 2:id, 3:ブログ名, 4:更新日
                5:記事url, 6:記事title
                '''
                if len(x) == 7:
                    # 記事登録日
                    date2 = datetime.datetime.strptime(x[4], '%Y-%m-%d')
                    date_ymd2 = datetime.datetime.date(date2)
                    
                    date_sa = (date_ymd1 - date_ymd2)
                    if date_sa.days > 0:    # マイナスでなければ前日以前
                        change_sw = True    # このグループ最後まで無視
                        break
                    group_blog_list.append(x)
                else:
                    print('非公開に設定されたブログを無視しました。', x[1])
            if change_sw :
                url =''
            else:
                nextpage = parser.get_nextpage_url()
                if nextpage == '':
                    url = ''
                else:
                    p = url.find('?', 0)
                    if p == -1:
                        nextpage_url=url
                    else:
                        nextpage_url = url[0: p] 
                    url = nextpage_url + nextpage

    with open('Group参加blog一覧.csv', 'w') as f:
        writer = csv.writer(f)
        writer.writerow(['グループ名', 'url', 'id', 'ブログ名', '記事日付',
                         '記事URL', '記事タイトル'])
        for l in group_blog_list:
            writer.writerow(l)

    return lst

if __name__ == '__main__':  
    myid       = 'はてなID'
    mypassword = 'パスワード'
    url = 'https://blog.hatena.ne.jp/-/group/'
    if  HatenaLogin(myid, mypassword) :
        # ログインに成功したら
         group_list = get_group_list(url)
    else :
        print('ログインに失敗しました')
    print('終了しました。')
    input('Enterキーを押して下さい')

参考記事: 「はてなブログ」の新着を知りたい https://koukaforest.hatenablog.com/entry/2019/12/08/01000

「はてなブログ グループをスクレイピングして調べてみた」 https://koukaforest.hatenablog.com/entry/2020/01/18/233000

html.parser--- HTML および XHTML のシンプルなパーサー https://docs.python.org/ja/3/library/html.parser.html

Python3 HTMLParserによるウェブスクレイピング実践入門 https://qiita.com/Taillook/items/a0f2c59d8e17381fc835

Ubuntu スクリプトをダブルクリックで実行する方法 https://koukaforest.hatenablog.com/entry/2019/11/21/010000