Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

PythonでHatenaブックマークのホットエントリを取得して表示する

- はじめに -

この記事は、Xonsh Advent Calendar 2017 - Qiita 25日目最終日の記事です。

本記事では、PythonスクリプトでHatenaブックマークのホットエントリのリストを取得、xonshへ表示する内容を記載します。

f:id:vaaaaaanquish:20171225175808p:plain

追記:2018/11/23
ptk 2.xでは、本記事のコードが動作しないため、移行のための記事とrepositoryを公開しています。
vaaaaaanquish.hatenablog.com

 

- Hatenaホットエントリの取得 -

はてなにはブログの投稿や取得、はてブ数の取得等のAPIが用意されています。
はてなブックマークドキュメント一覧 - Hatena Developer Center

上記を見る限り、Hatenaホットエントリを取得することは出来ないので、requestsでスクレイピングしてくる必要がありそうです。

import requests
import bs4

# ホットエントリページの取得、解析
res = requests.get("http://b.hatena.ne.jp/hotentry")
bs_res = bs4.BeautifulSoup(res.text, "lxml")

# はてブ数とタイトルの取得
hotentry = []
for x in bs_res.findAll("li", attrs={"class":"entry-unit"}):
    a_tag = x.find("a", attrs={"class":"entry-link"})
    if a_tag is not None:
        hotentry.append((x.find("span").text, a_tag.attrs["title"], a_tag.attrs["href"]))
# はてブ数でソート
hotentry = sorted(hotentry, key=lambda x:int(x[0]), reverse=True)

# 表示
for x in hotentry:
    print('{} || {} \n {}'.format(x[0], x[1], x[2]))

上記コードでクローリングしてきた結果が以下のように出ます

1635 || Pythonの学び方と,読むべき本を体系化しました2018〜初心者から上級者まで - Lean Baseball 
 http://shinyorke.hatenablog.com/entry/python2018
627 || コンピューターで全漢字使用可に 6万字コード化 | NHKニュース 
 https://www3.nhk.or.jp/news/html/20171224/k10011270111000.html
567 || 日本テレビのみなさまへ、生活保護についての悪意のある番組放送はやめてください(大西連) - 個人 - Yahoo!ニュース 
 https://news.yahoo.co.jp/byline/ohnishiren/20171224-00079667/
534 || アニメ犯罪を追う海外ドラマ「ANIME CRIMES DIVISION」がカオスすぎて面白い「お前は地下遊戯王の危険さをわかっていない」 - Togetter 
 https://togetter.com/li/1183065
534 || みずほ銀行、人事評価と結びついた金融商品の押し売りの実態がNHKより流出 : 市況かぶ全力2階建 
 http://kabumatome.doorblog.jp/archives/65905156.html
...

常々良さそうです。
この結果を結果を使っていきたい。


 

- xonshのセレクタで選択したらBrowserで開く -

以下の記事で、python prompt toolkit (ptk)を利用した、シェル上で対話的選択する方法を記載しました。

vaaaaaanquish.hatenablog.com


これを利用して、xonshではてなホットエントリを取得してセレクトし、Browserで開くスクリプトとしてxonsh向けに書いてみます。

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.document import Document
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.interface import CommandLineInterface
from prompt_toolkit.key_binding.manager import KeyBindingManager
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.layout.margins import ScrollbarMargin
from prompt_toolkit.shortcuts import create_eventloop
from prompt_toolkit.filters import IsDone
from prompt_toolkit.layout.controls import TokenListControl
from prompt_toolkit.layout.containers import ConditionalContainer, ScrollOffsets, VSplit, HSplit
from prompt_toolkit.layout.screen import Char
from prompt_toolkit.layout.dimension import LayoutDimension as D
from prompt_toolkit.mouse_events import MouseEventTypes
from prompt_toolkit.token import Token
from prompt_toolkit.styles import style_from_dict
import webbrowser
import requests
import bs4

def _get_hotentry():
    res = requests.get("http://b.hatena.ne.jp/hotentry")
    bs_res = bs4.BeautifulSoup(res.text, "lxml")

    hotentry = []
    for x in bs_res.findAll("li", attrs={"class":"entry-unit"}):
        a_tag = x.find("a", attrs={"class":"entry-link"})
        if a_tag is not None:
            hotentry.append((x.find("span").text, a_tag.attrs["title"], a_tag.attrs["href"]))
    hotentry = sorted(hotentry, key=lambda x:int(x[0]), reverse=True)
    hotentry = [('{} || {}'.format(x[0], x[1]), x[2]) for x in hotentry]
    return hotentry

def _open_url(url):
    webbrowser.open(url)

def _if_mousedown(handler):
    def handle_if_mouse_down(cli, mouse_event):
        if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN:
            return handler(cli, mouse_event)
        else:
            return NotImplemented
    return handle_if_mouse_down


class InquirerControl(TokenListControl):
    selected_option_index = 0
    answered = False
    choices = TokenListControl

    def __init__(self, hotentrys, **kwargs):
        self.choices = [x[0] for x in hotentrys]
        self.urls = [x[1] for x in hotentrys]
        super(InquirerControl, self).__init__(self._get_choice_tokens, **kwargs)

    @property
    def choice_count(self):
        return len(self.choices) 

    def _get_choice_tokens(self, cli):
        tokens = []
        T = Token

        def append(index, label):
            selected = (index == self.selected_option_index)

            @_if_mousedown
            def select_item(cli, mouse_event):
                self.selected_option_index = index
                self.answered = True
                cli.set_return_value(None)

            token = T.Selected if selected else T
            tokens.append((T.Selected if selected else T, ' > ' if selected else '   '))
            if selected:
                tokens.append((Token.SetCursorPosition, ''))
            tokens.append((T.Selected if selected else T, '%-24s' % label, select_item))
            tokens.append((T, '\n'))

        for i, choice in enumerate(self.choices):
            append(i, choice)
        tokens.pop()  # Remove last newline.
        return tokens

    def get_selection(self):
        return self.choices[self.selected_option_index], self.urls[self.selected_option_index]


def _hotentry():
    hotentry = _get_hotentry()
    ic = InquirerControl(hotentry)

    def __get_prompt_tokens(cli):
        tokens = []
        T = Token
        tokens.append((Token.QuestionMark, '?'))
        tokens.append((Token.Question, ' hotentrys '))
        if ic.answered:
            tokens.append((Token.Answer, ' ' + ic.get_selection()[0]))
            _open_url(ic.get_selection()[1])
        else:
            tokens.append((Token.Instruction, ' (Use arrow keys)'))
        return tokens

    layout = HSplit([
        Window(height=D.exact(1), content=TokenListControl(__get_prompt_tokens, align_center=False)),
        ConditionalContainer(
            Window( ic, width=D.exact(43), height=D(min=3), scroll_offsets=ScrollOffsets(top=1, bottom=1)),
            filter=~IsDone())])
    
    manager = KeyBindingManager.for_prompt()
    @manager.registry.add_binding(Keys.ControlQ, eager=True)
    @manager.registry.add_binding(Keys.ControlC, eager=True)
    def _(event):
        event.cli.set_return_value(None)
    @manager.registry.add_binding(Keys.Down, eager=True)
    def move_cursor_down(event):
        ic.selected_option_index = (
            (ic.selected_option_index + 1) % ic.choice_count)
    @manager.registry.add_binding(Keys.Up, eager=True)
    def move_cursor_up(event):
        ic.selected_option_index = (
            (ic.selected_option_index - 1) % ic.choice_count)
    @manager.registry.add_binding(Keys.Enter, eager=True)
    def set_answer(event):
        ic.answered = True
        event.cli.set_return_value(None)

    inquirer_style = style_from_dict({
        Token.QuestionMark: '#5F819D',
        Token.Selected: '#FF9D00',  # AWS orange
        Token.Instruction: '',  # default
        Token.Answer: '#FF9D00 bold',  # AWS orange
        Token.Question: 'bold',
    })

    _app = Application(
        layout=layout,
        #buffers=buffers,
        key_bindings_registry=manager.registry,
        mouse_support=True,
        #use_alternate_screen=True
        style=inquirer_style
    )

    _eventloop = create_eventloop()
    try:
        cli = CommandLineInterface(application=_app, eventloop=_eventloop)
        cli.run(reset_current_buffer=False)
    finally:
        _eventloop.close()

aliases["hotentry"] = _hotentry


これでxonsh上でのhotentryコマンドが作れました。
いざ実行してみます。

 
f:id:vaaaaaanquish:20171225175447g:plain


クールっぽい。
Browserで開くならあんまり意味ない気もしなくもないので、textだけでよしなに取ってくるマンを作って組み合わせたい所ですね。


- おわりに -

 
よさそうなセレクタの使い方ができました。

今年はWebスクレイピング記事で技術記事歴代一位のはてブ数を獲得した年でもあるので、良い記事で締めくくれていると自負しています。

vaaaaaanquish.hatenablog.com


アドベントカレンダーも盛り上がって、xonsh関連の日本語情報はかなり充実したと思います。
Xonsh Advent Calendar 2017 - Qiita

皆さんxonshでよい年末を。