Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

アドベントカレンダーのいいねをClojureでスクレイピングして数える

More than 3 years have passed since last update.

アドベントカレンダーのいいねをスクレイピングで数えるを読んで、Clojureで書いたらどうなるか試してみました。
HTTP KitとEnliveを使うことで、思っていたよりも手早く簡易スクレイパーを作成することができた為、記録として残します。

結果

  • WEB SCRAPING WITH CLOJUREのおかげで、Clojureでスクレイピングする際の最低限必要な知識をすぐに得ることができた。
  • Enliveで抽出したドキュメント(マップ)から目的の値を取得する方法に、煩わしさを感じる。
  • 普段Rubyでスクレイパーを書いているが、Clojureでも同じくらい手早く書くことができた。
  • マクロで書いた部分の恩恵は若干のタイプ数削減だけであった為、素直に関数定義で良かった。
    変数捕捉を使っている為、シンプルではなくなってしまった。
  • 再帰処理の為にマルチメソッドやパターンマッチを使わない方が見通しが良い。

ルール

元記事と同じ。

  • 対象アドベントカレンダーは株式会社エイチームさん
  • アドベントカレンダーの参加メンバーは除外
  • Qiitaの記事に限る

Clojureのバージョンと使用ライブラリ

project.clj
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [http-kit "2.2.0"]
                 [enlive "1.1.6"]]

コード

core.clj
(ns count-likes.core
  (:gen-class)
  (:require [net.cgrand.enlive-html :as html]
            [org.httpkit.client :as http]
            [clojure.string :as str]
            [clojure.data :as data]
            [clojure.pprint :as pp])
  (:import [java.nio.file Paths]))

(def qiita-url "https://qiita.com")
(def selector {:contents       [:.adventCalendarItem_calendarContent]
               :author         [:.adventCalendarItem_authorIcon]
               :url            [:.adventCalendarItem_entry :a]
               :next-page-link [:.js-next-page-link]
               :likers         [:.UserInfo__name]})

(defn make-qiita-url [path] (str qiita-url path))

(defn get-dom [url]
  (Thread/sleep 1000)
  (html/html-snippet (:body @(http/get url {:insecure? true}))))

(defmacro html-select [] `(html/select ~'dom (~'key selector)))
(defmacro extract-items
  ([]     `(distinct (html-select)))
  ([form] `(distinct (map ~form (html-select)))))

(defmacro extract-item
  ([]     `(first (extract-items)))
  ([form] `(first (extract-items ~form))))

(defn extract [key dom]
  (case key
    :contents       (extract-items)
    :author         (extract-item  #(some-> % :attrs :alt))
    :url            (extract-item  #(some-> % :attrs :href))
    :next-page-link (extract-item  #(some-> % fnext fnext :href))
    :likers         (extract-items #(some-> % :content first :content first))
    ()))

(defn get-articles [contents]
  (map #(array-map
         :author (extract :author %)
         :url    (extract :url %))
       contents))

(defn likers-page [url]
  (get-dom (str url "/likers")))

(defn next-page [path]
  (get-dom (make-qiita-url path)))

(defn collect-likers [likers dom]
  (concat likers (extract :likers dom)))

(defn find-likers [likers dom]
  (let [next-page-link (extract :next-page-link dom)
        current-likers (collect-likers likers dom)]
    (if (nil? next-page-link)
      current-likers
      (find-likers current-likers
                   (next-page next-page-link)))))

(defn remove-ignored-users [users ignored-users]
  (filter (complement nil?)
          (first
           (data/diff
            (set users)
            (set ignored-users)))))

(defn get-likers [article]
  (->> (likers-page (:url article))
       (find-likers ())
       distinct))

(defn valid-likers [article authors]
  (remove-ignored-users
   (get-likers article)
   authors))

(defn count-likes [article authors]
  (if (seq (:url article))
    (assoc article :likes (count (valid-likers article authors)))
    (assoc article :likes 0)))

(defn advent-calendar [year calendar-id]
  (get-dom
   (str qiita-url
        (.toString
         (Paths/get "/advent-calendar"
                    (into-array [year calendar-id]))))))

(defn get-authors [articles]
  (distinct (map #(:author %) articles)))

(defn total [key articles]
  (reduce + (map #(key %) articles)))

(defn articles-by [key val articles]
  (filter #(= (key %) val) articles))

(defn total-likes-by-author [articles]
  (->> (get-authors articles)
       (map
        #(let [articles-by-author (articles-by :author % articles)]
           {:author %
            :articles articles-by-author
            :total-likes (total :likes articles-by-author)}))))

(defn aggregate [dom]
  (let [articles (get-articles (extract :contents dom))]
    (-> (map #(count-likes % (get-authors articles)) articles)
        total-likes-by-author
        (#(array-map :total-likes (total :total-likes %)
                     :articles %)))))

(defn -main [& args]
  (let [[year calendar-id] args]
    (pp/pprint
     (aggregate (advent-calendar year calendar-id)))))

使い方

leiningenで実行する場合は、

$ lein run 2017 a-t-brides

または、uberjarで実行可能jarを作成して実行します。

$ lein uberjar
$ java -jar ./target/uberjar/count_likes-0.1.0-SNAPSHOT-standalone.jar 2017 a-t-brides

参考資料

yhsgw
PHPが苦手です
brides-a-tm
『一組でも多くのカップルに “理想の結婚式”のきっかけを』の使命の元、花嫁の理想(ユメ)を叶えるサービス「ハナユメ」「HIMARI」「ハナユメウエディングデスク」を運営しています。
http://brides.a-tm.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away