Robobrowser + α によるスクレイピングの忘備録
Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-』を読んでしばらく Robobrowser を使ってみようと思うので、その忘備録。
基本
browser = RoboBrowser( parser='html.parser', # Cookie が使用できないと表示されてログインできない問題を回避するため、通常のブラウザの User-Agent を使う。 user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:45.0) Gecho/20100101 Firefox/45.0)' ) broswer.open(スクレイピング対象のページのURL) # 取得したページの中身の確認。デバッグ時以外は不要。 # print(broswer.parsed)
user_agent の指定はサイトによっては不要。少なくとも Amazon.co.jp については必要(設定しない状態で open すると、取得できる html で Cookie を有効にして下さい、という旨のメッセージが入っていることがわかる)。
以降のコード例は基本的に上記の続きで broswer が Robobrowser インスタンスを表すものとする。
CSSセレクターを使った要素の取得
select
関数で CSSセレクターを使用可能。条件に一致する要素の配列が取得できる。
条件を指定して要素を取得する関数としては find_all
関数も利用できるので、好みで。find_all
の方が柔軟かもしれない。
実際に使用したことのある一部の例のみ記載。
探したい要素 | 書き方 | 例 |
---|---|---|
特定のクラスを持つ要素 | 要素.クラス | a.active |
特定の要素の子孫 | 要素 要素 | body h1 |
特定の要素の直接の子 | 要素 > 要素 | body > h1 |
特定のID属性 | #ID | #main |
属性が特定の値 | 要素[属性=“値”] | a[href=“/sample/index.html”] |
属性が特定の値で始まる | 要素[属性^=“値”] | span[id^=“itemPrice_” ] |
属性が特定の値で終わる | 要素[属性$=“値”] | img[src$=“.jpg” ] |
以下の例では g-item-details というクラスを持った div 要素を取得し、取得した各要素に更に select
を実行して必要な値を取得している
for item_info_element in browser.select('div.g-item-details'): title = item_info_element.select('a[id^="itemName_"]') assert len(title) == 1 # text でテキストを取得。 title = title[0].text.strip() # href 属性をキーにリンク先を取得 # 要 from urllib.parse import urljoin url = urljoin(browser.url, title[0]['href']) # URL の ref= 以降は不要。 url = item.url[:item.url.find('ref=')] price = item_info_element.select('span[id^="itemPrice_"]') # 必要な処理を続ける
特定のテキストを持ったリンク
get_link
関数の第一引数でテキストを指定する。取得したリンクを開くには follow_link
関数を使用する。
# この例ではリンク先(href の値)が特定の URL パターンに一致することも確認している sign_in_link = browser.get_link('サインイン', href=re.compile(r'https://www\.amazon\.co\.jp/ap/signin\?')) # assert sing_in_link is not None # browser.follow_link(sign_in_link)
フォーム
action をキーに get_form
関数を使って取得。
name 要素をキーに各入力欄にアクセスして値を設定し、submit_form
関数で実行
browser.follow_link(sign_in_link) form = browser.get_form(action='https://www.amazon.co.jp/ap/signin') if form is None: raise Exception('Failed to get signin link.') form['email'] = '.....@gmail.com' form['password'] = 'secret, of course!' browser.submit_form(form, headers={ 'Referer': browser.url, 'Accept-Language': 'ja,en-US;q=0.7;q=0.3' })
Amazon では Referer と Accept-Language の設定が必要。必要ないサイトもある。この辺りの必要性は実際に確かめるしか無い?
関数のリトライ設定
Retrying パッケージを使用して、関数に @retry デコレータを使用する
@retry(stop_max_attempt_number=3, wait_exponential_multiplier=1000) def signin() -> RoboBrowser: # 処理に失敗した場合は raise Exception('失敗の理由') などで例外を発生させる
golang : eval (数式の evaluation)を行う
go/token パッケージ、go/types パッケージ、go/constant パッケージを使用します。
import ( "fmt" "go/token" "go/types" "go/constant" ) fs := token.NewFileSet() tv, err := types.Eval(fs, nil, token.NoPos, "1 + 2 * 3 % 4") if err != nil { fmt.Println(err) return } val, ok := constant.Int64Val(tv.Value) if !ok { fmt.Println("Failed to get Int64Val") return } fmt.Println(val) // 結果は 3。
(Go 言語で プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問 をトライ中に 2問目を解くために方法を探しただけで、中身はあまりよくわかっていません。)
Angular2 : [ts] Property 'map' does not exist on type 'Observable<Response>'
http.get(url).map(...) を試みたところ発生。
[ts] Property 'map' does not exist on type 'Observable<Response>'
import rxjs/add/operator/map
を追加することで解決。
import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/map';
catch には import 'rxjs/add/operator/catch';
が必要になる。
一括でインポートするには import 'rxjs/Rx';
。
golang : template の range で index を使う
以下の様なデータを template.Execute のデータに渡したとすると
type data struct { Values []string OtherValues []string }
テンプレートで
<ul> {{range $i, $v := .Values}} <li>{{index $.OtherValues $i}} のようにするとインデックスでアクセスできる( OtherValues[i]の値)。{{$v}} はもちろん Values[i]</li> {{end}} </ul>
のようにすれば、Values にループをかけている最中にインデックスを使って他の配列やスライスの特定の値にアクセスできる
template - The Go Programming Language に書いている通り、テンプレート中の {{index スライスや配列の変数 インデックス値}}
がソースコード中のスライスや配列の変数[インデックス値]
に対応する。
index Returns the result of indexing its first argument by the following arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each indexed item must be a map, slice, or array.
また、$
が Execution に渡されたデータを指すので、range 中にデータの他の変数にアクセスするには $.変数名
となる。
golang : コミット時に gofmt を実行する(pre-commit hook)
misc/git/pre-commit - The Go Programming Language を参考に作成しました。
オリジナルではフォーマットされていないコードがある場合にコミットを中断していますが、以下のスクリプトでは gofmt を実行して git add するところまでやっています。
#!/bin/sh gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$') [ -z "$gofiles" ] && exit 0 unformatted=$(gofmt -l $gofiles) [ -z "$unformatted" ] && exit 0 echo >&2 "Info: Running gofmt as some Go files are not formatted with gofmt." for fn in $unformatted; do gofmt.exe -s -w $fn echo >&2 "gofmt -s -w $fn" git add $fn done exit 0
リンク先は、gofmt に加えて、gocyclo によるCyclomatic complexity のチェック(見るだけ)と goimports の実行も合わせたものになります。
Google App Engine : appcfg.py update 時の HTTP Error 403: Forbidden Unexpected HTTP status 403.
エラーの全文は以下の通り。
久々に GAE のプロジェクトを作成してデプロイしようと思ったら発生しました。
python appcfg.py -A YOUR_APP_ID -V v1 update src/ 06:44 PM Application: YOUR_APP_ID (was: None); version: v1 (was: None) 06:44 PM Host: appengine.google.com 06:44 PM Starting update of app: YOUR_APP_ID, version: v1 06:44 PM Getting current resource limits. 2016-04-23 18:44:08,184 ERROR appcfg.py:2402 An error occurred processing file '': HTTP Error 403: Forbidden Unexpected HTTP status 403. Aborting. Error 403: --- begin server output --- You do not have permission to modify this app (app_id=u's~YOUR_APP_ID'). --- end server output ---
python - Google App Engine app deployment - Stack Overflow に書かれている通り、%ユーザディレクトリ%/.appcfg_oauth2_tokens
を削除してから再実行することで、ブラウザで認証画面が開いてアップロードできました。
golang : URL短縮サービス goo.gl を使う
URL Shortener API の使い方はこちら → URL Shortener。
ここでは API Key を使った方法を載せています。
type responseParam struct { Kind string `json:"kind"` Id string `json:"id"` LongUrl string `json:"longUrl"` } func GetShortenedUrlImpl(longUrl string) (string, error) { values := url.Values{} values.Add("key", GoogleApiKey) // パッケージの変数として定義し、この関数を呼ぶ前にセットしておきます。 url := "https://www.googleapis.com/urlshortener/v1/url" + "?" + values.Encode() param := fmt.Sprintf(`{"longUrl":"%s"}`, longUrl); req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(param))) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() respBody, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } var respParam responseParam err = json.Unmarshal(respBody, &respParam) if err != nil { return "", err } return respParam.Id, nil }