はじめに
ここではWebスクレイピングに必要なWebの知識をまとます。Webスクレイピングを行うためには、Scrapyの使い方やXpathでの値の指定の仕方以外に、多くのことを知っていないと効率よく、かつ、迷惑をかけずにスクレイピングすることはできないように思います。
HTTPについて
HTTPは、WebブラウザがWebサーバーと通信するためのプロトコルです。HTTPは、TCP/IPにおけるアプリケーションがやり取りを行う層のプロトコルで、トランスポート層の中に位置します。TCP/IPのことよりも、HTTPについて理解を深めることが、スクレイピングには大切かと思いますので、これ以降はHTTPについてまとめていきます。
いつも何のことなく使っているWebブラウザからです。Webブラウザといえば、Google chrome、Internet Explorer、Firefoxなどが有名なWebブラウザとしてあげられますが、これ何でしょうか。そもそもWebとは、World Wide Webを略して表現したもので、そのWebの中に、Webページがあります。WebページははHTML(HyperText Markup Language)という言語で構成されています。そのWebページを閲覧するために使うのものがブラウザです。
WikipediaのHTMLサンプル をお借りします。<xxx>hoge</xxx>
という方法でテキストをマークアップしていくことでHTMLは構成されます。そのため、HTMLというのは、マークアップ言語とも呼ばれます。マークアップ言語は、人間が見やすいものではありません。
コピー <!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="author" href="mailto:mail@example.com">
<title lang="en">HyperText Markup Language - Wikipedia</title>
</head>
<body>
<article>
<h1 lang="en">HyperText Markup Language</h1>
<p>HTMLは、<a href="http://ja.wikipedia.org/wiki/SGML">SGML</a>
アプリケーションの一つで、ハイパーテキストを利用してワールド
ワイドウェブ上で情報を発信するために作られ、
ワールドワイドウェブの<strong>基幹的役割</strong>をなしている。
情報を発信するための文書構造を定義するために使われ、
ある程度機械が理解可能な言語で、
写真の埋め込みや、フォームの作成、
ハイパーテキストによるHTML間の連携が可能である。</p>
</article>
</body>
</html>
そのため、マークアップ言語を解釈して、表示し直してくれるのが、Webブラウザの機能です。そこで、ブラウザがWebサーバーから、HTMLを含め様々な情報を取得するために使う手段がHTTPです。
WebブラウザがHTTPというプロトコルに従って、Webサーバーに「リクエスト」を送ります。Webサーバーは、そのリクエストを受け取って、Webブラウザに「レスポンス」を返します。これがWebブラウザとWebサーバーの1つのやり取りで、その通信方法がHTTPです。
HTTPの内容
HTTPが実際にどのような内容をWebサーバーに送っているのか確認してみます。ターミナルからcurl
コマンドを利用します。curl
コマンドは、ファイルを送信または受信するためのコマンド。バージョンはこちら。
コピー $ curl --version
curl 7.64.1 (x86_64-apple-darwin19.0) libcurl/7.64.1 ( SecureTransport ) LibreSSL/2.8.3 zlib/1.2.11 nghttp2/1.39.2
Release-Date: 2019-03-27
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS GSS-API HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz MultiSSL NTLM NTLM_WB SPNEGO SSL UnixSockets
http://example.com にリクエストを送ってみます。>
の部分がリクエストで、<
の部分がレスポンスの部分です。なにやら沢山出力されたので、リクエストとレスポンスを分けて、内容を見ていきます。
コピー $ curl --verbose http://example.com
* Trying 93.184.216.34...
* TCP_NODELAY set
* Connected to example.com (93.184.216.34) port 80 (#0)
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Age: 532987
< Cache-Control: max-age=604800
< Content-Type: text/html; charset=UTF-8
< Date: Thu, 28 May 2020 08:36:36 GMT
< Etag: "3147526947+ident"
< Expires: Thu, 04 Jun 2020 08:36:36 GMT
< Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
< Server: ECS (oxr/832B)
< Vary: Accept-Encoding
< X-Cache: HIT
< Content-Length: 1256
<
<!doctype html>
< html >
< head >
< title >Example Domain</ title >
< meta charset = "utf-8" />
< meta http-equiv = "Content-type" content = "text/html; charset=utf-8" />
< meta name = "viewport" content = "width=device-width, initial-scale=1" />
< style type = "text/css" >
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width : 600 px;
margin: 5 em auto;
padding: 2 em;
background-color: #fdfdff;
border-radius: 0.5 em;
box-shadow: 2 px 3 px 7 px 2 px rgba( 0 , 0,0,0.02);
}
a:link, a:visited {
color : # 38488 f;
text-decoration: none;
}
@media (max-width: 700 px) {
div {
margin : 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h 1 >Example Domain</h 1 >
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href= "https://www.iana.org/domains/example" >More information...</a></p>
</div>
</body>
</html>
* Connection # 0 to host example.com left intact
* Closing connection 0
HTTPリクエスト
まずはリクエストの部分です。4行目が「リクエスト行」で、5~7行目は「HTTPヘッダー」、8行目は「空白行」でヘッダーの終わりを意味し、POSTであれば、その下にwebサーバーにデータを送るための「メッセージボディ」が配置されます。
example.comに80番ポートから、GETというリクエストメソッドで接続を試みていることがわかります。HTTP1.1というのはHTTPのバージョンです。User-Agentというのは後で詳しく扱いますが、誰がしているのかを示します。ここでは、curl
コマンドで行っていることがわかります。
コピー * Trying 93.184.216.34...
* TCP_NODELAY set
* Connected to example.com (93.184.216.34) port 80 (#0)
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/7.64.1
> Accept: */*
>
リクエストメソッドはGET以外にもよく使われるものとしてPOST、PUTなどがあります。コンテンツを取得する際に利用するものとして、GETとPOSTがありますが、GETはURLの後ろに情報を加える一方で、POSTはメッセージボディに情報を加えて通信します。基本的に見られて困る情報はPOSTで送ります。
GETリクエストと同じレスポンスを求めますが、レスポンス本文はなく、ヘッダのみ。
TCPトンネルを接続する。暗号化したメッセージをプロキシサーバを経由して転送する際に用いる。
リクエストメソッドのPOST(Create)、GET(Read)、PUT(Update)、DELETE(Delete)の操作をまとめてCRUD操作として考えることをRESTアーキテクチャと呼びます。
HTTPレスポンス
次はHTTPレスポンスの中身を見ていきます。1行目が「ステータス行」で、2〜12行目が「HTTPヘッダー」で、13行目が「空白行」でヘッダーの終わりを示す。14行目が「メッセージボディ」でHTMLが返されます。
ステータスコードが200で成功したことを意味し、コンテンツの内容はHTMLで、キャラセットはutf8ということがわかります。日付、キャッシュ、コンテンツの長さなどが書かれています。
コピー < HTTP/1.1 200 OK
< Age: 532987
< Cache-Control: max-age=604800
< Content-Type: text/html; charset=UTF-8
< Date: Thu, 28 May 2020 08:36:36 GMT
< Etag: "3147526947+ident"
< Expires: Thu, 04 Jun 2020 08:36:36 GMT
< Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
< Server: ECS (oxr/832B)
< Vary: Accept-Encoding
< X-Cache: HIT
< Content-Length: 1256
<
<!doctype html>
< html >
< head >
【略】
</ head >
< body >
【略】
</ body >
</ html >
* Connection #0 to host example.com left intact
* Closing connection 0
HTMLのデータを受け取り、Webブラウザは解析を行い、人間が見やすい形で表示します。その中に更に取得しなければ行けない情報(画像など)があると、再度、HTTPリクエストを送り、取得するように動きます。このHTTPリクエストとHTTPレスポンスを何度も行い、Webページを整えます。
このように、HTTPリクエストを受けつけ、HTTPレスポンスを返すことで、コンテンツを表示します。このように交互に通信を行うことを「同期通信」と呼びます。同期通信の欠点は、サーバーが処理している間は、待つしかなく、ページを更新するのに時間がかかります。これを解決するのがAjaxです。
Ajax(Asynchronous JavaScript + XML)は、Webブラウザ上で動作するjavascriptが直接Webサーバーと通信を行い、表示するHTMLを更新する仕組みです。この仕組を使うことで、HTTPレスポンスを待つ間に、レスポンスと関係ないところはjavascriptが更新する事ができるため、「非同期通信」が可能になり、ページの更新が速くなります。
HTTPステータスコード
HTTPレスポンスにはステータスコードが含まれます。先程の例では200が返されましたが、その他にも多くのステータスコードがあります。100番台から500番台までが使われます。
リクエストが正常に完了していることを意味するステータス
リクエストされたコンテンツが移動してことを意味するステータス
リクエストされたコンテンツが一時的に移動していることを意味するステータス
リクエストされたコンテンツが未更新。Webブラウザに一時的に保存されたコンテンツが表示される。
リクエストされたコンテンツがないことを意味するステータス
そのメソッドでのリクエストが許可されていないことを意味するステータス
ヘッダーで指定した言語でレスポンスができないことを意味するステータス
クライアントとサーバー間の通信時間が設定時間をオーバーしたことを意味するステータス
リクエスト中にサーバーでエラーが発生していることを意味するステータス
そのメソッドがサーバーでサポートされていないことを意味するステータス
ゲートウェイやプロキシとして動作しているサーバーが上位のサーバーから不正なレスポンスを受け取ったことを意味するステータス。
リクエストしたサーバーが一時的に停止していることを意味するステータス
Twitterの503はよく目にするけど、それを通知する画像がかわいい。
HTTPヘッダー
HTTPリクエスト、HTTPレスポンスのいずれもHTTPヘッダーを持ちます。ここには、詳細な情報をもたせることが可能です。1行の情報を「ヘッダーフィールド」と呼び、「フィールド名」と「フィールド値」で構成されます。
HTTPヘッダーは大きく4つのブロックで構成されます。「一般ヘッダー」「リクエストヘッダー」「レスポンスヘッダー」「エンティティヘッダー」です。詳細はMDNのサイト を参照。
まずは一般ヘッダーです。
リクエスト後のTCPコネクションの接続状態に関する通知
次は、リクエストヘッダーです。
レスポンスヘッダーは下記のとおりです。
最後にエンティティヘッダーです。
User-Agentについて、少し触れておきます。User-Agentはアクセスしているブラウザのバージョンや種類を示すもので、Scrapyでスクレイピングしている際に、同じUser-Agentだとアクセスできなくなることがります。そのような場合に、User-Agentを切り替える(偽造)してアクセスする必要があります。偽造という表現が良くないですが、問題のある行為ではありません。
例えば、Google Chromeの場合、下記のようなUser-Agentになります。What is my User Agent? で調べることができます。ちなみにグローバルIPアドレスはWhat is my ip? で調べることができます。
コピー Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36
Safariなどは下記のようになります。
コピー Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15
HTTPコネクション
「クライアントであるブラウザがリクエストを送り、サーバーがレスポンスを返す」というこのHTTPの一連のやり取りは、TCPが「コネクション」と呼ばれる通信経路を確立して行います。
HTTPのバージョンによって、コネクションの方法は異なります。HTTP/1.0以前は、WebブラウザがHTTPリクエストを送信するためにコネクションを確立していましたが、HTTP/1.1以降はHTTP KeepAlive方式が採用されており、リクエストごとにコネクションを確立する必要がなくなりました。
HTTP KeepAlive方式でない場合、HTMLの中に画像があった場合、HTTPリクエストを一度送り、コネクションを確立し、レスポンスを受け取って、コネクションを閉じて、画像を受け取るために再度コネクションを確立するという方法になります。
一方でHTTP KeepAlive方式だと、一度リクエストを送れば、コネクションを閉じずとも、HTTPリクエストとHTTPレスポンスのやり取りを行うことが可能なため、無駄な時間が発生しません。また、HTTPパイプラインという技術のおかげで、「コネクションの確立→リクエスト1→レスポンス1→リクエスト2→レスポンス2→コネクションの切断」というやり取りではなく、「コネクションの確立→リクエスト1→リクエスト2→レスポンス1→レスポンス2→コネクションの切断」といように、HTTPレスポンスを待たずともHTTPリクエストを送れるようになっています。
HTTP自体は非常にシンプルな通信プロトコルですが、弱点があります。それは「ステートレス」なやりとりであるということです。これは文字通り、「状態を保持しない」通信ということで、ブラウザとのHTTP通信を考えると、サーバー側は毎回別人としてクライアントを扱うことになります。
一方で、「ステートフル」なやりとりを行うことも可能です。この場合、言葉の通り、「状態を保持する」通信であるため、ブラウザとのHTTP通信を考えると、サーバー側はクライアントが誰なのかを識別して、通信を行うことになります。この仕組みであれば、ECサイトでは、買い物かごに商品を入れてから離脱して再度、ECサイトに戻ってきた場合でも、買い物かごにいれた商品は、そのまま買い物かごに入ったままの状態を保つことが可能です。
HTTPはステートレスなプロトコルであるため、ステートフルなやりとりを実現するためにHTTP cookieというデータを用いて通信しています。
HTTPとWebアプリケーション
Webブラウザ上で機能するアプリケーションのことをWebアプリケーションと呼びます。Webアプリケーションについても、少しばかり裏側の仕組みおさらいしておきます。
Webアプリケーションは3層アーキテクチャと呼ばれる構造になっています。3層とは、「プレゼンテーション層」「アプリケーション層」「データ層」の3つです。
プレゼンテーション層は、ユーザーのインターフェースを担い操作などを受けつけ、Webサーバーが裏側で機能します。WebサーバーはWebブラウザからHTTPリクエストを受け取ると、静的コンテンツや動的コンテンツをHTTPレスポンスとして返します。このサーバーがダウンすると、HTTPリクエストを受け付けるサーバーがなくなるので、サービスが停止します。そうならなように、冗長化という構成をとります。冗長化は、Webサーバーを複数起動し、HTTPリクエストをさばく仕組みです。
アプリケーション層は、業務的な処理を担い、プレゼンテーション層からの操作を処理します。裏側ではアプリケーションサーバーが機能します。アプリケーションサーバーは、Webサーバーからの指示をデータベースサーバーに問い合わせて、データを返す役割です。先程でてきたセッションIDの管理もアプリケーションサーバーの役割です。
データ層は、アプリケーションサーバーからのデータに関する処理を担います。裏側では、データベースサーバーが機能します。データベースサーバーはアプリケーションサーバからの依頼に基づいてデータベースから情報を返します。Webサーバーと同じく、冗長化構成を取ります。冗長化の方法としては、「ミラーリング」、「レプリケーション」「シェアードディスク」があります。「ミラーリング」、「レプリケーション」もマスターサーバーがあり、そこでの更新をミラーサーバーないしスレーブサーバー転送します。ミラーリングは即時更新しますが、レプリケーションは任意のタイミングで更新します。
このような構成でWebアプリケーションは機能しますが、各サーバーへの負荷を下げる仕組みとして、「キャッシュサーバー」というものがあります。キャッシュとは「HTTPリクエストに対するHTTPレスポンスを記憶する」ことです。キャッシュにも種類があり、コンテンツをキャッシュするものは「コンテンツキャッシュ」と呼ばれ、DBサーバーへの検索クエリをキャッシュするものは「クエリキャッシュ」と呼ばれます。
コンテンツキャッシュサーバーは、WebブラウザとWebサーバーとの間に配置され、エリキャッシュサーバーはアプリケーションサーバーとデータベース・サーバーとの間に配置されます。
他にもサーバーへの負荷を下げる手段として、ファイアウォールがあります。ファイアウォールはどちらかというセキュリティの観点から説明されることが多いですが、通信を遮断するものなので、ここで合わせてまとめておきます。
一般的に用いられているのは、パケットフィルタ型のファイアウォールです。これはIPアドレスとポート番号をチェックし、通信の遮断・許可を行います。誰でも使用できるWebシステムであれば、WebサーバーアクセスにするIPアドレスを限定することは難しいので、HTTPの80番ポートとHTTPSの443番ポートだけを許可することになります。
MySQLなどのデータベースサーバーであれば、ポート番号は3306で、アクセスするIPアドレスを限定することで、アクセスできる人を限定したりすることが多いのかもしれません。
HTTPと認証
ここでは、基本的な認証の種類についてまとめていきます。認証がかかっているサイトはクローラーを走らせるのは好ましくないように思えます。認証がかかるということは、その先にあるのは個人情報や秘密情報であるためです。そのため、アカウントアグリゲーションサービスかなんかを作るとかになると、規約の範囲内で行うことになるかと思います。
Webページにアクセスすると、ポップアップと同時にアカウントとパスワードが求められる認証方式がHTTP認証です。HTTP認証の中でも広く使われる方法が、ベーシック認証です。ベーシック認証がかかっているURLにリクエストを送ると、401が返ってきます。
コピー # -I:HTTPレスポンスヘッダーの取得
$ curl -I http://pythonscraping.com/pages/auth/login.php
HTTP/1.1 401 Unauthorized
Date: Sun, 31 May 2020 03:08:50 GMT
Server: Apache
WWW-Authenticate: Basic realm="My Realm"
Content-Type: text/html; charset=UTF-8
ベーシック認証のアカウントとパスワードを送信する方法は-u id:pass
でおくります。ログインできているようです。
コピー ➜ curl -u 'tabaka:awsedrft' http://pythonscraping.com/pages/auth/login.php
<p>Hello tabaka.</p><p>You entered awsedrft as your password.</p>~
他にもフォームベース認証というものがあります。これは、名前の通りで、ログインフォームからIDとパスを認証する方法です。例えば、Githubはフォーム認証です。
フォームに該当する部分のHTMLを抜き出すと、このようになっています。1行目のform要素のaction属性、method属性を確認すると、method="post"
でaction="/session"
というパスに情報を送ることがわかります。
コピー <form action="/session" accept-charset="UTF-8" method="post"><input type="hidden" data-csrf="true" name="authenticity_token" value="****************==" /> <input type="hidden" name="ga_id" class="js-octo-ga-id-input">
<div class="auth-form-header p-0">
<h1>Sign in to GitHub</h1>
</div>
<div id="js-flash-container">
<template class="js-flash-template">
<div class="flash flash-full js-flash-template-container">
<div class="container-lg px-2" >
<button class="flash-close js-flash-close" type="button" aria-label="Dismiss this message">
<svg class="octicon octicon-x" viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48L7.48 8z"></path></svg>
</button>
<div class="js-flash-template-message"></div>
</div>
</div>
</template>
</div>
<div class="flash js-transform-notice" hidden>
<button class="flash-close js-flash-close" type="button" aria-label="Dismiss this message">
<svg class="octicon octicon-x" viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48L7.48 8z"></path></svg>
</button>
</div>
<div class="auth-form-body mt-3">
<label for="login_field">
Username or email address
</label>
<input type="text" name="login" id="login_field" class="form-control input-block" tabindex="1" autocapitalize="off" autocorrect="off" autocomplete="username" autofocus="autofocus" />
<label for="password">
Password <a class="label-link" href="/password_reset">Forgot password?</a>
</label>
<input type="password" name="password" id="password" class="form-control form-control input-block" tabindex="2" autocomplete="current-password" />
<input type="hidden" class="js-webauthn-support" name="webauthn-support" value="unknown">
<input type="hidden" class="js-webauthn-iuvpaa-support" name="webauthn-iuvpaa-support" value="unknown">
<input type="hidden" name="return_to" id="return_to" class="form-control" />
<input class="form-control" type="text" name="required_field_2cad" hidden="hidden" />
<input class="form-control" type="hidden" name="timestamp" value="1590894924840" />
<input class="form-control" type="hidden" name="timestamp_secret" value="35403dd16a13de0a200b8bedb46b64b4309326083002f2b7b73c3f5bfd96ce33" />
<input type="submit" name="commit" value="Sign in" tabindex="3" class="btn btn-primary btn-block" data-disable-with="Signing in…" />
</div>
</form>
Chromeの検証ツールを使うとname="login"
とname="password"
で値を送っていることがわかります。また、authenticity_token
は毎回更新する度に変更されるので、クローラーを走らせるには、この値を毎回スクレイピングしてから、ログインIDとパスワードを送って認証する必要があります。
Scrapyでやってみます。ここでは、食べログのサイトを参考にしてみます。scrapy shellでサイトにアクセスします。
コピー $ scrapy shell "https://tabelog.com/"
In [1]: from scrapy.http import FormRequest, Request
当たり前ですが、トップページがそのまま返ってきます。なので、検索条件を指定してフォームリクエストを行うことで、自分がほしいページが返ってくるようにします。まずは、どのような値でリクエストを送ればよいのか調べます。ここでは、「渋谷駅」「焼肉」「2020年7月1日」「20:00」「10名」という条件のレスポンスがほしいので、これを指定して、「検索」ボタンをおして、検証ツールからどのような値が必要なのか調べます。
画像の右下を見る限り、LstKind
からkey_datatype
まで様々な値を送っていることがわかります。なので、これを丸ごとコピーして辞書型に直し、FormRequest()
でリクエストします。そうすることで、検索条件を指定したあとのページをレスポンスとして受け取ることが可能です。検索ボタンのアクションはaction="https://tabelog.com/rst/rstsearch/"
となっているので、それをフォームリクエストでは使用します。
コピー scrapy shell "https://tabelog.com/"
from scrapy.http import FormRequest, Request
data = {
"LstKind": "1",
"voluntary_search": "1",
"lid": "yoyaku-search_header",
"sa": "渋谷駅",
"sk": "焼肉",
"vac_net": "1",
"search_date": "2020/7/1(水)",
"svd": "20200701",
"svt": "2000",
"svps": "10",
"hfc": "1",
"area_datatype": "RailroadStation",
"area_id": "4698",
"key_datatype": "keyword",
}
page = FormRequest('https://tabelog.com/rst/rstsearch/',
formdata = data)
fetch(page)
view(response)
画像はview(response)
の結果です。指定した条件でページを取得できています。食べログにはありませんでしたが、フォームリクエストでは、不正な攻撃を避けるために、Githubにあったような更新のたびに変更される値も必要になる場合があるので、それも必要であれば、スクレイピングして更新のたびに最新の値がとれるようにして、FormRequest()
でリクエストします。
HTTP Cookie
HTTP CookieはどのようにHTTP通信に関わってくるのかを見ていきます。WebブラウザからHTTPリクエストを送った際に、サーバーはHTTPレスポンスと合わせて、ブラウザに保存してほしい情報としてHTTP Cookieも送ります。
ECサイトであれば、HTTPリクエストを送った際に、HTTPレスポンスをブラウザを識別するためにブラウザにHTTP Cookieも送ります。その後、ブラウザがそのECサイトにアクセスする際に、保存しているHTTP Cookieを送ることで、サーバー側では、誰がアクセスしてきたのかを識別します。
実際の通信では、HTTPヘッダーに、HTTPレスポンスをブラウザに送る時は、Set-CookieヘッダーにHTTP Cookieの情報を付与します。Set-Cookieは複数あっても問題ありません。また、属性はセミコロンで区切られます。HTTPリクエスト側は、Cookieヘッダーに情報を含めることになります。ECサイトなどでは有効期限を定めたセッションCookieが一般的に用いられます。ちなみにcurl
コマンドのヘッダーオプション-H
を使うことで、Cookieを送信できます。
コピー $ curl --verbose -H 'Cookie: name=scrapy; ver=111' http://example.com
* Trying 93.184.216.34...
* TCP_NODELAY set
* Connected to example.com (93.184.216.34) port 80 (#0)
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/7.64.1
> Accept: */*
> Cookie: name=scrapy; ver=111
HTTP Cookieと関連して、Webブラウザとサーバーの一連のやり取りの流れを「セッション」と呼びます。セッション管理のために、WebサーバーはHTTP CookieにセッションIDを付与することでセッション管理を行います。そのため、リクエストを送る際にセッションIDも送ることで、サーバー側は「セッション」を識別していきます。またセッションにおける最小粒度のアクションを「トランザクション」と呼びます。複数のトランザクションが時系列的に集まったものがセッションと言えるかもしれません。
HTTPフォームとCookieについてもう少し深堀りしてまとめておきます。ここでは、Practical Web Scraping for Data Science: Best Practices and Examples with Python で使われているサンプルページをお借りして、Cookieの理解を深めます。requests.Session()
を使えば簡単ではありますが、ここでは使わず動かしながら確認します。
このページ はログインすることで秘密のページ へアクセスできるような仕組みになっています。ログインしていない状態だと、秘密のページにはアクセスできません。必要なCookie情報がないため、サーバーが秘密のページのURLを表示しません。
コピー import requests
url = "http://www.webscrapingfordatascience.com/cookielogin/secret.php"
r = requests . get (url)
print (r.text)
# Hmm... it seems you are not logged in
そのため、秘密のページへアクセスするために、ログインしてCookieを取得してから、そのCookieを使って秘密のページへHTTPリクエストを送信します。そうすることで、秘密のページへのアクセスを行います。
コピー import requests
# Login
url = "http://www.webscrapingfordatascience.com/cookielogin/"
data_post = { "username" : "testuser" , "password" : "password" }
r = requests . post (url, data = data_post)
# Cookieを取得して、secretページにアクセス
my_cookie = r . cookies
url = "http://www.webscrapingfordatascience.com/cookielogin/secret.php"
r = requests . get (url, cookies = my_cookie)
print (r.text)
# This is a secret code: 1234
このようなパターンであれば問題ないのですが、ログインが成功すれば同時に秘密のページへリダイレクトされる場合はどうすればよいでしょうか。
このような場合は、post()
のallow_redirects
引数を設定して、リダイレクトされないようにします。
コピー import requests
# Login
url = "http://www.webscrapingfordatascience.com/redirlogin/"
data_post = { "username" : "testuser" , "password" : "password" }
r = requests . post (url, data = data_post, allow_redirects = False )
# Cookieを取得して、secretページにアクセス
my_cookie = r . cookies
url = "http://www.webscrapingfordatascience.com/redirlogin/secret.php"
r = requests . get (url, cookies = my_cookie)
print (r.text)
# This is a secret code: 1234
最後の例として、ログインページにアクセスしたかどうか、ログイン後にCookieのセッションIDを変更するようなサイトの場合を見ていきます。このような場合でも、Cookieを取得して、Cookiを使ってログイン、ログイン後に変わるCookieを取得して、Cookieを更新してから、秘密のページへアクセスすれば、秘密のページへアクセス可能です。
コピー import requests
# Formを取得
url = "http://www.webscrapingfordatascience.com/trickylogin/"
r = requests . post (url)
my_cookie = r . cookies
print (my_cookie)
# 先程のCookieを使ってpostを実行
r = requests . post (url,
params = { "p" : "login" },
data = { "username" : "testuser" , "password" : "password" },
allow_redirects = False ,
cookies = my_cookie)
# Cookieを更新する
my_cookie = r . cookies
print (my_cookie)
# 秘密のページへアクセス
r = requests . get (url, params = { "p" : "protected" }, cookies = my_cookie)
print (r.text)
# <RequestsCookieJar[<Cookie PHPSESSID=8u3j3mh4sq286755uuo59gaj83 for www.webscrapingfordatascience.com/>]>
# <RequestsCookieJar[<Cookie PHPSESSID=ak6p8cpip0c0i9ee4vsmn6fet5 for www.webscrapingfordatascience.com/>]>
# Here is your secret code: 3838.
このような感じでCookieを処理すればよいのですが、Scrapyで同じように処理しようとすると、うまくいきません。こちらの動画 で詳しく説明されているので、これをなぞってみます。動画に沿って、まずはHTTPリクエストの内容を可視化できるRequestBin.com
を使って確認してみます。
サイトにアクセスして開発者ツールからヘッダーの情報をコピーして、
これを使ってリクエストを送ります。コードは下記のとおりです。
コピー import requests
# custom headers
headers = {
"accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" ,
"accept-encoding" : "gzip, deflate, br" ,
"accept-language" : "ja,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6" ,
"cache-control" : "max-age=0" ,
"cookie": "_gcl_au=1.1.2053070165.1592979574; _ga=GA1.2.1180196223.1592979574; _gid=GA1.2.1504925251.1592979574; _lr_uf_-dhjtrz=a5956b98-e3b3-41d8-ab3a-3f9c182bd391; _hjid=ec0ef6a8-a0b1-431d-abe1-01397ae26645; ki_r=; _uetsid=11adf75b-e96c-d7d7-9ea2-dfa133728451; _uetvid=d6cd5e1d-cdde-40f2-01b6-f60ff4361f57; _gat_UA-128559955-1=1; _lr_tabs_-dhjtrz%2Fpd={%22sessionID%22:0%2C%22recordingID%22:%224-be179f28-1ed2-4d5d-81ff-e6d0d409ac9a%22%2C%22lastActivity%22:1592986151170}; _lr_hb_-dhjtrz%2Fpd={%22heartbeat%22:1592986151172}; ki_t=1592979575629%3B1592979575629%3B1592986152027%3B1%3B5; amplitude_id_eadd7e2135597c308ef5d9db3651c843requestbin.com=eyJkZXZpY2VJZCI6IjdjYTczN2U3LTkyMzMtNDllOC1iOGUxLTEwMGRiZGM1YWEwM1IiLCJ1c2VySWQiOm51bGwsIm9wdE91dCI6ZmFsc2UsInNlc3Npb25JZCI6MTU5Mjk4NTczNzcyNiwibGFzdEV2ZW50VGltZSI6MTU5Mjk4NjE1MjEyNywiZXZlbnRJZCI6NiwiaWRlbnRpZnlJZCI6MCwic2VxdWVuY2VOdW1iZXIiOjZ9",
"if-modified-since" : "Wed, 01 Apr 2020 23:15:20 GMT" ,
"sec-fetch-dest" : "document" ,
"sec-fetch-mode" : "navigate" ,
"sec-fetch-site" : "none" ,
"sec-fetch-user" : "?1" ,
"upgrade-insecure-requests" : "1" ,
"user-agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36"
}
# requestbin.com URL
url = 'https://enldafevk2rhb.x.pipedream.net/'
# make HTTP GET request to requestbin.com
response = requests . get (url, headers = headers)
print (response.text)
実行してTRUEが返ってくると、画像のようにリクエストの内容が表示されます。画像を見ると、Cookieが問題なくリクエストとともに送られていることがわかります。
コピー $ python3 lesson . py
{ "success" : true }
Scrapyで同じようにやってみます。
コピー import scrapy
from scrapy . crawler import CrawlerProcess
# spider class
class HeadersCookies ( scrapy . Spider ):
name = "headerscookies"
url = "https://enldafevk2rhb.x.pipedream.net/"
# custom headers
headers = {
"accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" ,
"accept-encoding" : "gzip, deflate, br" ,
"accept-language" : "ja,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6" ,
"cache-control" : "max-age=0" ,
"cookie": "_gcl_au=1.1.2053070165.1592979574; _ga=GA1.2.1180196223.1592979574; _gid=GA1.2.1504925251.1592979574; _lr_uf_-dhjtrz=a5956b98-e3b3-41d8-ab3a-3f9c182bd391; _hjid=ec0ef6a8-a0b1-431d-abe1-01397ae26645; ki_r=; _uetsid=11adf75b-e96c-d7d7-9ea2-dfa133728451; _uetvid=d6cd5e1d-cdde-40f2-01b6-f60ff4361f57; _gat_UA-128559955-1=1; _lr_tabs_-dhjtrz%2Fpd={%22sessionID%22:0%2C%22recordingID%22:%224-be179f28-1ed2-4d5d-81ff-e6d0d409ac9a%22%2C%22lastActivity%22:1592986151170}; _lr_hb_-dhjtrz%2Fpd={%22heartbeat%22:1592986151172}; ki_t=1592979575629%3B1592979575629%3B1592986152027%3B1%3B5; amplitude_id_eadd7e2135597c308ef5d9db3651c843requestbin.com=eyJkZXZpY2VJZCI6IjdjYTczN2U3LTkyMzMtNDllOC1iOGUxLTEwMGRiZGM1YWEwM1IiLCJ1c2VySWQiOm51bGwsIm9wdE91dCI6ZmFsc2UsInNlc3Npb25JZCI6MTU5Mjk4NTczNzcyNiwibGFzdEV2ZW50VGltZSI6MTU5Mjk4NjE1MjEyNywiZXZlbnRJZCI6NiwiaWRlbnRpZnlJZCI6MCwic2VxdWVuY2VOdW1iZXIiOjZ9",
"if-modified-since" : "Wed, 01 Apr 2020 23:15:20 GMT" ,
"sec-fetch-dest" : "document" ,
"sec-fetch-mode" : "navigate" ,
"sec-fetch-site" : "none" ,
"sec-fetch-user" : "?1" ,
"upgrade-insecure-requests" : "1" ,
"user-agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36"
}
def start_requests ( self ):
# make HTTP GET request to requestbin.com
yield scrapy . Request (
url = self.url,
headers = self.headers,
callback = self.parse
)
def parse ( self , response ):
print (response.text)
if __name__ == "__main__" :
process = CrawlerProcess ()
process . crawl (HeadersCookies)
process . start ()
画像を見てもらうとわかるのですが、Cookieがありません。これはScrapyのstart_requests
では、辞書形式でCookieを送る必要があるためです。
そのためCookieを取り出して、セミコロンでCookieは繋がれるので、それ使って分割して辞書に変換します。
コピー import scrapy
from scrapy . crawler import CrawlerProcess
# spider class
class HeadersCookies ( scrapy . Spider ):
name = "headerscookies"
url = "https://enldafevk2rhb.x.pipedream.net/"
# custom headers
headers = {
"accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" ,
"accept-encoding" : "gzip, deflate, br" ,
"accept-language" : "ja,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6" ,
"cache-control" : "max-age=0" ,
"if-modified-since" : "Wed, 01 Apr 2020 23:15:20 GMT" ,
"sec-fetch-dest" : "document" ,
"sec-fetch-mode" : "navigate" ,
"sec-fetch-site" : "none" ,
"sec-fetch-user" : "?1" ,
"upgrade-insecure-requests" : "1" ,
"user-agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36"
}
raw_cookie = "_gcl_au=1.1.2053070165.1592979574; _ga=GA1.2.1180196223.1592979574; _gid=GA1.2.1504925251.1592979574; _lr_uf_-dhjtrz=a5956b98-e3b3-41d8-ab3a-3f9c182bd391; _hjid=ec0ef6a8-a0b1-431d-abe1-01397ae26645; ki_r=; _uetsid=11adf75b-e96c-d7d7-9ea2-dfa133728451; _uetvid=d6cd5e1d-cdde-40f2-01b6-f60ff4361f57; _gat_UA-128559955-1=1; _lr_tabs_-dhjtrz%2Fpd={%22sessionID%22:0%2C%22recordingID%22:%224-be179f28-1ed2-4d5d-81ff-e6d0d409ac9a%22%2C%22lastActivity%22:1592986151170}; _lr_hb_-dhjtrz%2Fpd={%22heartbeat%22:1592986151172}; ki_t=1592979575629%3B1592979575629%3B1592986152027%3B1%3B5; amplitude_id_eadd7e2135597c308ef5d9db3651c843requestbin.com=eyJkZXZpY2VJZCI6IjdjYTczN2U3LTkyMzMtNDllOC1iOGUxLTEwMGRiZGM1YWEwM1IiLCJ1c2VySWQiOm51bGwsIm9wdE91dCI6ZmFsc2UsInNlc3Npb25JZCI6MTU5Mjk4NTczNzcyNiwibGFzdEV2ZW50VGltZSI6MTU5Mjk4NjE1MjEyNywiZXZlbnRJZCI6NiwiaWRlbnRpZnlJZCI6MCwic2VxdWVuY2VOdW1iZXIiOjZ9"
# parse cookies
def parse_cookies ( self , raw_cookies ):
cookies = {}
for cookie in raw_cookies . split ( '; ' ):
try :
key = cookie . split ( '=' ) [ 0 ]
val = cookie . split ( '=' ) [ 1 ]
cookies [ key ] = val
except :
pass
return cookies
def start_requests ( self ):
# make HTTP GET request to requestbin.com
yield scrapy . Request (
url = self.url,
headers = self.headers,
cookies = self. parse_cookies (self.raw_cookie),
callback = self.parse
)
def parse ( self , response ):
print (response.text)
if __name__ == "__main__" :
process = CrawlerProcess ()
process . crawl (HeadersCookies)
process . start ()
リクエストの内容をみるとCookieも問題なくリクエストに含んで送れています。