LoginSignup
264

More than 5 years have passed since last update.

goqueryでお手軽スクレイピング!

Last updated at Posted at 2013-12-04

こんにちは、Qiita二回目の投稿です、 yosuke_furukawa と申します、Golang勉強中です。

Qiitaの一回目の投稿もScrapingネタだったのですが、二回目もGolang勉強中ということでQiitaのScrapingネタで行きます。

まず、goqueryの説明に行く前に単純なやり方でscrapingしてみます。

Golangで単純なスクレイピングをするためには、以下のモジュールを利用するとできます。
- net/http httpリクエストを送るためのモジュール
- code.google.com/p/go.net/html html解析をするためのモジュール

超シンプルなサンプルとして、該当のurlのaタグのhrefにある値だけ取得する場合は以下の様な感じになるかと思います。

go.net/htmlで頑張る

package main

import (
     "code.google.com/p/go.net/html"
     "fmt"
     "io"
     "net/http"
)

type Result struct {
     Url string
}

func ParseItem(r io.Reader) []Result {
     results := []Result{}
     doc, err := html.Parse(r)
     if err != nil {
          fmt.Println(err)
     }

     var result Result
     var f func(*html.Node)
     f = func(n *html.Node) {
          // n.Typeでノードの型をチェックできる、ElementNodeでHTMLタグのNode。
          // n.Dataでノートの値をチェックする、aタグをチェックしている
          if n.Type == html.ElementNode && n.Data == "a" {
               // n.Attrで属性を一覧する
               // ここでもう少し頑張るとparseできる
               for _, a := range n.Attr {
                    if a.Key == "href" {
                         result.Url = a.Val
                         results = append(results, result)
                    }
               }
          }
          for c := n.FirstChild; c != nil; c = c.NextSibling {
               f(c)
          }
     }
     f(doc)
     return results
}

func GetPage(url string) []Result {
     //http.GetでGetリクエストを発行する
     res, err := http.Get(url)
     if err != nil {
          fmt.Println(err)
     }
     // deferでやるとReaderを関数の終わりで必ずCloseしてくれる。便利!!
     defer res.Body.Close()
     results := ParseItem(res.Body)
     return results
}

func main() {
     url := "http://qiita.com/advent-calendar/2013/"
     results := GetPage(url)
     for _, result := range results {
          fmt.Println(result.Url)
     }
}

go-html-transformを使う

また、単純にページから情報を抽出する、というよりもDOMの中から不要なものを削ったり、ページを加工するならcode.google.com/p/go.net/htmlよりもcode.google.com/p/go-html-transform/html/transformを使うのがいいかと思います。
CSSセレクタが使えるので、いい感じにfilterを書けます。(ただ名前の通りhtmlの加工目的なので、htmlから必要な情報を抽出するのには向かない気もします。)

package main

import (
     "code.google.com/p/go-html-transform/html/transform"
     "fmt"
     "io"
     "net/http"
)

type Result struct {
     Url string
}

func ParseItem(r io.Reader) {
     // transformのインスタンスを作る
     t, _ := transform.NewFromReader(r)
     // Applyメソッドで自分のDOMに反映する。
     // Applyメソッド内では、TransformFuncを受け付けるようになっており、
     // Replaceの他にもDOMを追加するAppendChildrenやPrependChildrenなどもある。
     // ページを加工するならこっちのが便利。
     // ちなみに以下の処理で不要なページを削っている
     t.Apply(transform.Replace(), "script")
     t.Apply(transform.Replace(), "footer")
     t.Apply(transform.Replace(), "meta")
     t.Apply(transform.Replace(), "ul")
     t.Apply(transform.Replace(), "li")
     t.Apply(transform.Replace(), "link")
     t.Apply(transform.Replace(), "div.day")
     t.Apply(transform.Replace(), "div.advent-calendar-breadcrumb")
     t.Apply(transform.Replace(), "a.post-user-icon")
     fmt.Println(t.String())
}

func GetPage(url string) {
     //http.GetでGetリクエストを発行する
     res, err := http.Get(url)
     if err != nil {
          fmt.Println(err)
     }
     // deferでやるとReaderを関数の終わりで必ずCloseしてくれる。便利!!
     defer res.Body.Close()
     ParseItem(res.Body)
}

func main() {
     url := "http://qiita.com/advent-calendar/2013/"
     GetPage(url)
}

goqueryを使う

と、色々と書きましたが、jQueryライクなセレクタを持ち、httpのリクエストの処理も兼ね備えているgoqueryを使うのがスクレイピングには一番向きます。Golangの勉強ということで、なるべくモジュール組み合わせて、試したかったのです。すいません。

package main

import (
     "fmt"
     "github.com/PuerkitoBio/goquery"
)

func GetPage(url string) {
     doc, _ := goquery.NewDocument(url)
     doc.Find("a").Each(func(_ int, s *goquery.Selection) {
          url, _ := s.Attr("href")
          fmt.Println(url)
     })
}

func main() {
     url := "http://qiita.com/advent-calendar/2013/"
     GetPage(url)
}

めちゃくちゃ短くできる!!!!!!!!
しかも直感的!!!!!!!!!

goqueryは既にmattnさんが記事にしてくれてるので、それも参考になると思います。
Go言語で jQuery ライクな操作が出来る goquery を試した。

goquery

最後にgoqueryとgoroutine、channelで並列処理でQiitaのこれまでのエントリをscrapingして出力して終わります。コードはコチラ。

package main

import (
    "fmt"
    "github.com/PuerkitoBio/goquery"
    "os"
    "sync"
)

type Result struct {
    CalTitle string
    Title    string
    Url      string
}

func GetPage(url string) []Result {
    results := []Result{}
    doc, _ := goquery.NewDocument(url)
    doc.Find("a.calendar-name").Each(func(_ int, s *goquery.Selection) {
        url, exists := s.Attr("href")
        if exists {
            caltitle := s.Text()
            entryPage, _ := goquery.NewDocument("http://qiita.com" + url)
            entryPage.Find("div.body h1>a").Each(func(_ int, s *goquery.Selection) {
                url, exists := s.Attr("href")
                if exists {
                    result := Result{caltitle, s.Text(), url}
                    results = append(results, result)
                }
            })
        }
    })
    return results
}

func GoGet(urls []string) <-chan []Result {
    var wg sync.WaitGroup
    ch := make(chan []Result)
    go func() {
        for _, url := range urls {
            wg.Add(1)
            go func(url string) {
                ch <- GetPage(url)
                wg.Done()
            }(url)
        }
        wg.Wait()
        close(ch)
    }()
    return ch
}

func main() {
    args := os.Args
    if len(args) < 2 {
        panic("usage : goquery <url>")
    }

    urls := []string{}
    for index, arg := range args {
        if index != 0 {
            urls = append(urls, arg)
        }
    }

    ch := GoGet(urls)
    for {
        results, ok := <-ch
        if !ok {
            return
        }
        calTitle := ""
        for _, result := range results {
            if calTitle != result.CalTitle {
                fmt.Println("#" + result.CalTitle)
                calTitle = result.CalTitle
            }
            fmt.Println("[" + result.Title + "](" + result.Url + ")")
        }
    }
}

リポジトリはコチラ:
https://github.com/yosuke-furukawa/goquery_sample

使い方:

$ goquery http://qiita.com/advent-calendar/2013 http://qiita.com/advent-calendar/2012

Qiita アドベントカレンダー一覧(2013一覧):

1分で実現できる有用な技術

1分でPAGER環境変数を設定したあとページャーを知る
1分で実現できるtmuxのTips x3 (ついでにinstall to Mac,CentOS,Debian/Ubuntu)
1分で実現できるjavascriptにおけるconsoleまわりの有用なテクニック4つ

Amazon Web Service

AWSクレジットの引き換えエラーメッセージまとめ
AWS CLIの --query オプションが便利。
Cross-Zone Load Balancing を有効にしない理由がない件
AWSを便利に使う為の情報・ツール

AngularJS Startup

AngularJSを使ったWebアプリのアーキテクチャ設計
AngularJSのTutorialのstep-1とstep-2よりData Bindingの仕組みをレポート
AngularJSのはじめの一歩(詳細)
AngularJSを選択する見解

Ansible

Ansibleちょっとしたメモ
Ansibleの事例とちょっとしたTips

Ceylon

Ceylonモジュール作成やクラスなどの基本コード
Ceylonの良いとこ説明コードその1
Ceylon開発環境セットアップ!
Java大好きな人が作ったCeylon言語ってどんなもの?

Civic Tech

Open Government Licenceの紹介
21世紀型の都市が持つべき7つの戦略とCivic Hackerへのマインドシフト
Beyond Transparency の紹介
地域課題解決の新しい形、Civic Tech と Code for Japan

Clojure

Clojureで音楽組織プログラミングについて
Clojureを練習するためのオンライン問題集
clj-webdriverの紹介

cocos2d-x

【まとめ】cocos2d-Xmas Special by #gumistudy〜 Chukong Technologies(coco2d-x開発元)他登壇!
簡単なカスタムボタンの作り方(2)
透過画像に対応したコリジョン判定
簡単なカスタムボタンの作り方
cocos2d-xでのJSON利用方法

CodeIgniter

自作クラスでCodeIgniterオブジェクトを使用する
【基礎】CodeIgniterでライブラリを作成する
【基礎】CodeIgniterでコアクラスを作成する

Corona

Corona SDK + Parse.com でプッシュ通知をする
Kwikを買いました。
今年のCoronaSDKの振返り

Delphi

カメラの画角を変える方法

Doc-ja

Translate Toolkitで翻訳ツールを作る
続・フリーなオフィススィートを翻訳すること
WordPressプラグインを題材にGettextでの翻訳方法を紹介
フリーなオフィススィートを翻訳すること

D言語

D言語で、コンパイル時単体テスト
DustMiteで、バグ再現最小コードを作る
D言語 Language Update 2013

Elixir

mix newで作られるファイル探索
elixir on Android
fluent-logger-elixirのはなし

Fluentd

Fluentd のベンチマークテストに使える dummy_log_generator の紹介

G*(Groovy, Grails ..)

Cucumber-Groovyでfeatureの書き方を手順を追ってみる
Cucumber-groovy設定編

Git

Gitのコミットログ再考
プルリクエストを自動補完してcheckoutする

Google Apps Script

Google Formでクイズして、自動で答え合わせして、メールまで送っちゃうお
公式ガイドの「Dialogs and Sidebars in Google Apps」を制覇する!(その1)
2013年度版 5分で始めるWebアプリケーション(Google Apps Script)

GorillaScript

GorillaScript簡易ガイドブック

Grunt Plugins

CSSプロパティの重複を解析してくれるgrunt-csscssについて紹介するよ
CSS書く人なら絶対入れとけのgrunt-contrib-csslintについて紹介するよ
タスクが正常に終わったらチャットワークにメッセージを通知する grunt-chatwork を紹介するよ
散乱した@mediaをまとめてくれるgrunt-combine-media-queriesを紹介するよ
フロントエンドだけじゃない! サーバサイドの開発も手助けしてくれる grunt-connect-proxy を紹介するよ
JSONファイルの管理をちょっとだけ楽にしてくれるgrunt-sync-versionを紹介するよ
なんでも開ける grunt-open について紹介するよ
イケてるスタイルガイドを簡単に作れるgrunt-kssについて紹介するよ
CSSプロパティをソートしてくれるgrunt-csscombについて紹介するよ
高いCSS圧縮率を誇るgrunt-cssoについて紹介するよ
JSコードの品質チェックをしてくれるgrunt-platoについて紹介するよ
ページ表示速度の問題チェックをしてくれるgrunt-pagespeedについて紹介するよ
Gitフックを仕込むgrunt-githooksについて紹介するよ

Haxe

Haxeの関数値について雑多なこと
Haxeの文字列
知ってると色々と世界が広がるかもしれないHaxeのコンパイルオプション

HDL

Verilog HDLでの数値リテラル(定数)の書き方
SFLのstageとNSLのseqを攻略する
[SystemVerilog]即時アサーションでコネクティビティをチェックする。
[HDL][雑記] 言語トレンドを見てみよう

hubot-scripts

Hubot-scriptsでサイコロを投げる
Hubot-scriptsのmsg.randomを学ぶ
はじめてのHubot
Hubot-scriptsの学びかた

iBeacon

iBeacon開発ハマリどころポイントまとめ
iBeacon で忍者が密会する
iBeaconを利用したアプリ開発でチェックしておきたい!良記事・ソースコードまとめ
1行もコードを書かずにiBeaconで遊んでみる

IntelliJ IDEA

IntelliJ IDEA 13がリリースされました!
設定、プラグイン導入した後にこれだけはやっとけってやつ
IntelliJ IDEAをチームで導入するために私が行ったこと
IntelliJ IDEAをインストールしたら設定してること(Java/Groovy編)

Jenkins CI

ALMiniumをVirtualBox上のCentOSにインストールするのにJenkinsを使ってみたお話
Jenkinsビルド後の処理でRedmineにチケット登録ができるプラグインを作った話

JSX

JSX + npm
ライブラリからみるJSXの特徴

Laravel

Model で Validation したい? それならば Ardent だ!
Laravel の現状と拡張の話(読み物)
最小構成で始めるLaravel
日本人に優しいLaravel

Lisp

'`'と','

Max/MSP/Jitter

Max/MSPの様々なGUIパーツ
Max/MSPの便利な操作方法
Max/MSPの入門レシピいろいろ
Max/MSP/Jitterをはじめるメモ

Mojolicious

Mojoliciousのテンプレートでレイアウトを自在に操る
Mojoliciousのセッションの話 2013年年末版
Mojoliciousの様々な立ち上げ方

MongoDB

Mongo shellの裏技色々
MongoEngineでMongoDBを触ってみる基礎編
Ruby初心者がRuby on Rails + Mongoidを試してみた
MongoDBとブラウザだけで旅の記録をつけてみるか その1

mruby

GUIアプリケーションなどが保持するmrubyのオブジェクトのGC対策
mruby-redisでランキングを実装
mrubyのビルド方法

NEET

Cryptocurrency Mining、始めてみませんか?
NEET Advent Calendar作りました
NEETでGeekな情報収集術
全国のNEET達に送る、プログラミングの始め方。

openFrameworks

ofxFaceTracker で顔をリアルタイムにトラッキングする
openFrameworks for iOS のサンプル一覧
openFrameworks for iOS ことはじめ

OpenStreetMap

ライセンス表記に注意しよう(自戒の念を込めて)
ライトマッパーが1年を振り返るよ
Surveyorジャケットの日本版作成とか

Perl

普通のデーモンを 1) Server::Starterでホットデプロイ+ 2) slow-restart対応にする
Twiggy で応答の遅いサーバーを模倣する
HTTPクライアントとStream::Bufferedの合わせ技
HTTP::Message::PSGIでPSGIアプリのテストを書く
Log::StringFormatter でログ文字列のフォーマット

PostgreSQL

PostgreSQLのあまり知られていない型3種

Python

LXCをPythonから操作する
テキストファイルから指定した文字列を含む行を出力する

Ruby on Rails

てめえらのRailsはオブジェクト指向じゃねえ!まずはCallbackクラス、Validatorクラスを活用しろ!
websocket-railsで簡単なPush通知を実装する
Rails4に対応したgem doorkeeperついて調べてみた。
Railsのコンソールをより便利にするpry-rails gem

RubyMotion

Parse.com で Push Notification with Rubymotion
motion-modeの紹介とruby-modeについて
RubyMotionアプリ開発に、motion-mode + Rubocop を導入
Rubyist が RubyMotion で iOS アプリの開発を始める方法

Rust

Rust Advent Calendar 12/2分を20分ででっちあげる

TDD

ノーマルにMSTestを使おう

WebPay

WebPayLiteを使ってiOSから簡単にクレジットカード決済する
少しのコードでWebPayを導入する PHP Ver.
少しのコードでWebPayを導入する

Windows Azure

Windows Azure Cloud ServiceでPython/Djangoを使おう!
Windows Azure仮想マシンのディスクをPowerShellでバックアップする
私が Windows Azure Web サイトを大好きな 7 つの理由

Windows Store App

スタート画面っぽくいろんなサイズのタイルを GridView に表示しちゃうやり方
WinJS.Bindingについて
ストアアプリで手軽に画像を加工しよう
markSupportedForProcessingについて調べたかった

Xamarin

Xamarin.iOSでUITableViewの罠 / Xamarinの進化に望む事
XamarinでParse SDKを利用する
Xamarin(ザマリン) とはなんぞや

zsh

zshにオプションや引数を補完できるキーバインドを設定しよう
"select-word-style bash"の不満を解消する
bundle exec を打たなくて良くなる zsh プラグイン書いた
zsh で find を使わずに簡単にファイルを絞り込む

カーネル/VM

カーネル/VM Advent Calendar 2013 3日目:LinuxカーネルのlockrefというLock機能を試してみよう
カーネル/VM Advent Calendar 2013 1日目

ディストリビューション/パッケージマネージャー

aptitude検索パターンの紹介
debian-goodies
抄訳 games-misc/fortune-mod-gentoo-dev
Gentoo Wikiの翻訳

どう書く過去問

等差数列(2013.5.17の過去問)
ボールカウント:野球(2012.8.8の過去問)
のんびり座りたい (2013.2.2の過去問)

ドキュメンテーション

プロジェクト管理WebアプリBrabio!のガントチャートをプレゼン用に綺麗に出力する

マイナー言語

明日から使えるasm.js - Low Level JavaScript - 「LLJS」 マイナー言語アドベントカレンダー・一日目

みんなでやるRiak

Riak2を複数ノードで動かしてみる
Riak2にデータを出し入れしてみる
Riak2をインストールしてみる

ラテン語プログラミング

あらこんな所にラテン語が。いつも見るあの語が実は・・・
自由に使えるラテン語辞書データ (I)
ラテン語文解析プログラムを書くことを目的としたラテン語学習(前編)
VB5で作ったラテン語アプリを発掘

全文検索エンジンGroonga

リポジトリを横断してコミットメッセージを眺めるツールgglogを使ってみた
GObject Introspectionを使っていろいろな言語からGroongaを使う方法

設定ファイル「dotfiles」の作り方

vim-powerline (lightline.vim)
zsh-powerline
tmux-powerline

iOS Second Stage

iOS 7でバーコード・QRコードを読み取る方法と生成する方法(おまけもあるよ)
複数のStoryboardを使ってタブの遷移を作成する
iOSで使える日本語OKな音声読み上げエンジン8種(TTS,音声合成)
Xcodeと自動化

.emacs

emacs のリージョンや編集中のバッファを DayOne の Dropbox に保存する拡張をつくった
自分流の .emacs管理
anzu.elの紹介
ぼくの.emacs 2013

Android

BeagleBone BlackにAndroidをインストールする
Gradleことはじめ

Go

Androidでʕ ◔ϖ◔ʔGo~
goyaccを使う
Go言語で顔認識してみた

iOS

XIBからもコードからもカスタムViewインスタンスを生成する方法
「顔以外」のものを画像認識する
IB/Storyboard使わない派のlayoutSubviewsによるレイアウト調整

JavaScript - Client Side -

BakboneJSだってDOMとModelをBindingしたい!
なぜクライアントサイドJavaScriptにはHTML/CSSの知識が必要か?
Ember.jsのコアな機能を学ぶためのサンプルプロジェクト

Machine Learning

Random Forest とその派生アルゴリズムの紹介
Dropoutの実装と重みの正則化(MLAC 2013 3日目)
たぶん1分くらいでできる形態素解析とtfidf(テストコードつき)
ベイズ線形回帰(PRML§3.3)の図版再現

PHP

PHPの開発に使えるVagrantfileのまとめ

PhpStorm

コードフォーマットを使いこなす
PhpStorm EAPを使ってみる
PhpStormをVimキーマップで使う
PhpStormとは?

Ruby

websocket-client-simple でPush通信を使ってJenkinsさんと会話
Ruby 2.1.0-preview2で追加されたException#causeの紹介
RailsとGrapeで行う最高のWeb API開発

Scala

Lift の RestHelper でサクサクAPI 開発
2014年こそScalaを始めよう

Unity

Unity 途別2Dアセット
Transform拡張
MoveAssetToTrashでUndo/Redo

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
264