グラブ-サイトを解析するためのPythonライブラリ

約5、6年前、私がまだPHPを主にプログラミングしていたときに、サイトを解析するためにcurlライブラリを使用し始めました。 サイトでユーザーセッションをエミュレートし、通常のブラウザーのヘッダーを送信し、POST要求を送信する便利な方法を提供できるツールが必要でした。 最初はcurl拡張機能を直接使用しようとしましたが、そのインターフェイスは非常に不便で、よりシンプルなインターフェイスでラッパーを作成しました。 時間が経ち、私はpythonに移動し、同じオークカール拡張APIに遭遇しました。 ラッパーをPythonで書き直す必要がありました。

グラブとは何ですか?


これは、サイトを解析するためのライブラリです。 その主な機能:


次に、各項目について詳しく説明します。 最初に、作業オブジェクトの初期化とネットワーク要求の準備について説明しましょう。 Yandexにページを要求し、ファイルに保存するコードの例を示します。
>>> g = Grab(log_file='out.html') >>> g.go('http://yandex.ru') 

実際、 `log_file`パラメーターはデバッグを目的としています-さらなる調査のために応答本文を保存する場所を示します。 ただし、それを使用してファイルをダウンロードできます。

Grabオブジェクトの構成方法は、コンストラクター内で確認しました。 そして、同じコードのいくつかのバリエーションがあります:
 >>> g = grab() >>> g.setup(url='http://yandex.ru', log_file='out.html') >>> g.request() 

または
 >>> g = Grab() >>> g.go('http://yandex.ru', log_file='out.html') 


最短:
 >>> Grab(log_file='out.html').go('http://yandex.ru') 


要約すると、Grabの構成は、コンストラクター、 `setup`メソッド、または` go`および `request`メソッドを介して指定できます。 `go`メソッドの場合、要求されたURLは位置引数として渡すことができます;他の場合では、名前付き引数として渡す必要があります。 「go」メソッドと「request」メソッドの違いは、「go」では最初のパラメーターが必要なのに対し、requestでは何も必要とせず、以前に設定したURLを使用することです。

log_fileオプションに加えて、log_dirオプションがあります。これにより、マルチステップパーサーのデバッグが非常に簡単になります。
 >>> import logging >>> from grab import Grab >>> logging.basicConfig(level=logging.DEBUG) >>> g = Grab() >>> g.setup(log_dir='log/grab') >>> g.go('http://yandex.ru') DEBUG:grab:[02] GET http://yandex.ru >>> g.setup(post={'hi': u', !'}) >>> g.request() DEBUG:grab:[03] POST http://yandex.ru 


ほら 各リクエストは番号を受け取りました。 各要求に対する応答は、ファイル/ tmp / [number 022.htmlに記録されました。また、/ tmp / [number 022.logファイルも作成されました。このファイルには、応答のhttpヘッダーが記録されました。 そして、上記のコードは何をしますか? 彼はYandexのメインページに移動します。 そして、同じページに対して無意味なPOSTリクエストを行います。 2番目のリクエストではURLを指定しないことに注意してください-以前のリクエストのURLがデフォルトで使用されます。

別のGrabデバッグ設定を見てみましょう。
 >>> g = Grab() >>> g.setup(debug=True) >>> g.go('http://youporn.com') >>> g.request_headers {'Accept-Language': 'en-us;q=0.9,en,ru;q=0.3', 'Accept-Encoding': 'gzip', 'Keep-Alive': '300', 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.3', 'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en; rv:1.9.0.2) Gecko/2008091620 Firefox/3.0.2', 'Accept-Charset': 'utf-8,windows-1251;q=0.7,*;q=0.7', 'Host': 'www.youporn.com'} 


youporn.comにリクエストを行いました。 デバッグオプションを使用すると、発信要求ヘッダーを保存できます。 不明な点がある場合は、サーバーに送信した内容を確認できます。 request_headers属性には、リクエストのhttpヘッダーのキーと値を含む辞書が含まれています。

クエリをコンパイルするための基本的な機能を検討してください。

HTTPリクエストメソッド


POSTリクエスト。 とても簡単です。 `post`オプションでキーと値を持つ辞書を指定します。 グラブは、要求タイプを自動的にPOSTに変更します。
 >>> g = Grab() >>> g.setup(post={'act': 'login', 'redirec_url': '', 'captcha': '', 'login': 'root', 'password': '123'}) >>> g.go('http://habrahabr.ru/ajax/auth/') >>> print g.xpath_text('//error')    


GETリクエスト。 POSTデータまたはリクエストメソッドが明示的に設定されていない場合、GrabはGETリクエストを生成します。

PUT、DELETE、HEADメソッド。 理論的には、オプションmethod = 'delete'、method = 'put'またはmethod = 'head'を設定するとすべてが機能します。 実際には、私はこれらのメソッドをほとんど使用しておらず、それらのパフォーマンスについて確信が持てません。

POSTリクエストに関する重要な注意。 Grabは、指定されたすべてのオプションを保存し、次のクエリで使用するように設計されています。 保存しない唯一のオプションは、 `post`オプションです。 彼がそれを保存した場合、次の例では、2番目のURLにPOSTリクエストを送信しますが、これはほとんど望んでいません。
 >>> g.setup(post={'login': 'root', 'password': '123'}) >>> g.go('http://example.com/login') >>> g.go('http://example.com/news/recent') 


HTTPヘッダーを構成する


次に、送信されたhttpヘッダーを構成する方法を見てみましょう。 `headers`オプションでヘッダー辞書を設定するだけです。 デフォルトでは、Grabは、Accept、Accept-Language、Accept-Charset、Keep-Aliveなどのブラウザーに似たヘッダーを生成します。 `headers`オプションで変更することもできます:
 >>> g = Grab() >>> g.setup(headers={'Accept-Encoding': ''}) >>> g.go('http://digg.com') >>> print g.response.headers.get('Content-Encoding') None >>> g.setup(headers={'Accept-Encoding': 'gzip'}) >>> g.go('http://digg.com') >>> print g.response.headers['Content-Encoding'] gzip 


Cookieを使用する


デフォルトでは、Grabは受信したCookieを保存し、次のリクエストでそれらを送信します。 ユーザーセッションエミュレーションはそのまま使用できます。 これが必要ない場合は、 `reuse_cookies`オプションを無効にしてください。 `cookies`オプションで手動でcookieを設定できます。それには辞書が含まれている必要があり、その処理は` post`オプションで送信されたデータの処理に似ています。
 >>> g.setup(cookies={'secureid': '234287a68s7df8asd6f'}) 


`cookiefile`オプションでCookieストレージとして使用されるファイルを指定できます。 これにより、プログラムを起動するたびにCookieを保存できます。

dump_cookiesメソッドを使用していつでもGrabオブジェクトのCookieをファイルに書き込むか、ファイルからload_cookiesメソッドをロードできます。 GrabオブジェクトのCookieをクリアするには、 `clear_cookies`メソッドを使用します。

ユーザーエージェント


デフォルトでは、Grabは実際のブラウザを装います。 さまざまなUser-Agent文字列のリストがあり、Grabオブジェクトの作成時にそのうちの1つがランダムに選択されます。 もちろん、 `user_agent`オプションでUser-Agentを設定できます。
 >>> from grab import Grab >>> g = Grab() >>> g.go('http://whatsmyuseragent.com/') >>> g.xpath('//td[contains(./h3/text(), "Your User Agent")]').text_content() 'The Elements of Your User Agent String Are:\nMozilla/5.0\r\nWindows\r\nU\r\nWindows\r\nNT\r\n5.1\r\nen\r\nrv\r\n1.9.0.1\r\nGecko/2008070208\r\nFirefox/3.0.1' >>> g.setup(user_agent='Porn-Parser') >>> g.go('http://whatsmyuseragent.com/') >>> g.xpath('//td[contains(./h3/text(), "Your User Agent")]').text_content() 'The Elements of Your User Agent String Are:\nPorn-Parser' 


プロキシサーバーを使用する


すべてが些細です。 プロキシオプションでは、「server:port」の形式でプロキシアドレスを渡す必要があります。proxy_typeオプションでは、そのタイプを渡します:http、socks4またはsocks5プロキシが認証を必要とする場合、proxy_userpwdオプション、値を使用します「user:password」という形式です。
Google検索に基づく最も単純なプロキシ検索エンジン:
 >>> from grab import Grab, GrabError >>> from urllib import quote >>> import re >>> g = Grab() >>> g.go('http://www.google.ru/search?num=100&q=' + quote('free proxy +":8080"')) >>> rex = re.compile(r'(?:(?:[-a-z0-9]+\.)+)[a-z0-9]+:\d{2,4}') >>> for proxy in rex.findall(g.drop_space(g.css_text('body'))): ... g.setup(proxy=proxy, proxy_type='http', connect_timeout=5, timeout=5) ... try: ... g.go('http://google.com') ... except GrabError: ... print proxy, 'FAIL' ... else: ... print proxy, 'OK' ... 210.158.6.201:8080 FAIL ... proxy2.com:80 OK …. 210.107.100.251:8080 OK …. 


回答作業


Grabを使用してネットワークリクエストを行ったとします。 次は? 「go」メソッドと「request」メソッドはResponseオブジェクトを返します。これは、Grabオブジェクトの「response」属性からも利用できます。 Responseオブジェクトの次の属性とメソッドに興味があるかもしれません:code、body、headers、url、cookies、charset。


グラブオブジェクトには、「response_unicode_body」というメソッドがあります。このメソッドは、Unicodeに変換された応答本文を返します。タイプ&のHTMLエンティティは、対応するUnicodeに変換されないことに注意してください。

最後のリクエストのレスポンスオブジェクトは、常に属性 `response` Grabオブジェクトに保存されます。
 >>> g = Grab() >>> g.go('http://aport.ru') >>> g.response.code 200 >>> g.response.cookies {'aportuid': 'AAAAGU5gdfAAABRJAwMFAg=='} >>> g.response.headers['Set-Cookie'] 'aportuid=AAAAGU5gdfAAABRJAwMFAg==; path=/; domain=.aport.ru; expires=Wed, 01-Sep-21 18:21:36 GMT' >>> g.response.charset 'windows-1251' 


応答テキスト(grab.ext.text拡張)の使用


検索メソッドでは、指定した文字列が応答本文に存在するかどうかを設定できます; search_rexメソッドは、パラメーターとして正規表現オブジェクトを受け入れます。 引数が見つからなかった場合、assert_substringおよびassert_rexメソッドはDataNotFound例外をスローします。 また、この拡張機能には、「find_number-最初の数値の出現を検索」、「drop_space」-空白文字を削除し、「normalize_space」-スペースのシーケンスを1つのスペースに置き換えます。
 >>> g = Grab() >>> g.go('http://habrahabr.ru') >>> g.search(u'Google') True >>> g.search(u'') False >>> g.search(u'') False >>> g.search(u'') False >>> g.search(u'') True >>> g.search('') Traceback (most recent call last): File "", line 1, in File "grab/ext/text.py", line 37, in search raise GrabMisuseError('The anchor should be byte string in non-byte mode') grab.grab.GrabMisuseError: The anchor should be byte string in non-byte mode >>> g.search('', byte=True) True >>> import re >>> g.search_rex(re.compile('Google')) <_sre.SRE_Match object at 0xb6b0a6b0> >>> g.search_rex(re.compile('Google\s+\w+', re.U)) <_sre.SRE_Match object at 0xb6b0a6e8> >>> g.search_rex(re.compile('Google\s+\w+', re.U)).group(0‌) u'Google Chrome' >>> g.assert_substring('  ') Traceback (most recent call last): File "", line 1, in File "grab/ext/text.py", line 62, in assert_substring if not self.search(anchor, byte=byte): File "grab/ext/text.py", line 37, in search raise GrabMisuseError('The anchor should be byte string in non-byte mode') grab.grab.GrabMisuseError: The anchor should be byte string in non-byte mode >>> g.assert_substring(u'  ') Traceback (most recent call last): File "", line 1, in File "grab/ext/text.py", line 63, in assert_substring raise DataNotFound('Substring not found: %s' % anchor) grab.grab.DataNotFound >>> g.drop_spaces('foo bar') Traceback (most recent call last): File "", line 1, in AttributeError: 'Grab' object has no attribute 'drop_spaces' >>> g.drop_space('foo bar') 'foobar' >>> g.normalize_space(' foo \n \t bar') 'foo bar' >>> g.find_number('12    ') '12' 


DOMツリー(grab.ext.lxml拡張機能)を操作する


最も興味深いものにアプローチします。 すばらしいlxmlライブラリのおかげで、Grabはxpath式を操作してデータを検索する機能を提供します。 非常に短い場合: `tree`属性を介して、ElementTreeインターフェイスでDOMツリーにアクセスできます。 ツリーは、lxmlライブラリのパーサーを使用して構築されます。 xpathとcssの2つのクエリ言語を使用してDOMツリーを操作できます。

xpathを使用する方法:

要素が見つからなかった場合、関数 `xpath`、` xpath_text`および `xpath_number`はDataNotFound例外をスローします。

関数 `css`、` css_list`、 `css_text`および` css_number`は同様に動作しますが、1つの例外を除き、引数はxpathパスではなくcssセレクターでなければなりません。
 >>> g = Grab() >>> g.go('http://habrahabr.ru') >>> g.xpath('//h2/a[@class="topic"]').get('href') 'http://habrahabr.ru/blogs/qt_software/127555/' >>> print g.xpath_text('//h2/a[@class="topic"]')  Qt Creator 2.3.0‌ >>> print g.css_text('h2 a.topic')  Qt Creator 2.3.0‌ >>> print 'Comments:', g.css_number('.comments .all') Comments: 5 >>> from urlparse import urlsplit >>> print ', '.join(urlsplit(x.get('href')).netloc for x in g.css_list('.hentry a') if not 'habrahabr.ru' in x.get('href') and x.get('href').startswith('http:')) labs.qt.nokia.com, labs.qt.nokia.com, thisismynext.com, www.htc.com, www.htc.com, droider.ru, radikal.ru, www.gosuslugi.ru, bit.ly 


フォーム(grab.ext.lxml_form拡張)


自動フォーム入力機能を実装したとき、私はとても幸せでした。 あなたも喜んでください! したがって、 `set_input`メソッドがあります-指定された名前でフィールドを埋める、` set_input_by_id`-id属性の値、および `set_input_by_number`-単に番号で。 これらのメソッドは、手動で設定できるフォームで機能しますが、通常、Grab自身がどのフォームを使用するかを正しく推測します。 フォームが1つの場合-すべてが明確ですが、複数の場合は? グラブは、ほとんどのフィールドが含まれる形式になります。 フォームを手動で指定するには、 `choose_form`メソッドを使用します。 submitメソッドを使用して、完成したフォームを送信できます。 グラブ自体は、明示的に入力しなかったフィールド(非表示フィールドなど)に対してPOST / GETリクエストを作成し、フォームのアクションとリクエストメソッドを計算します。 また、辞書のすべてのフィールドとフォームの値を返す `form_fields`メソッドもあります。
 >>> g.go('http://ya.ru/') >>> g.set_input('text', u' ') >>> g.submit() >>> print ', '.join(x.get('href') for x in g.css_list('.b-serp-url__link')) http://gigporno.ru/, http://drochinehochu.ru/, http://porno.bllogs.ru/, http://www.pornoflv.net/, http://www.plombir.ru/, http://vuku.ru/, http://www.carol.ru/, http://www.Porno-Mama.ru/, http://kashtanka.com/, http://www.xvidon.ru/ 


輸送


デフォルトでは、Grabはすべてのネットワーク操作にpycurlを使用します。 この機能は拡張の形でも実装されており、urllib2ライブラリを介したリクエストなど、別のトランスポート拡張機能を接続できます。 1つだけ問題があります。この拡張機能は事前に作成する必要があります:) urllib2拡張機能の作業は進行中ですが、非常にゆっくりです-私はpycurlに100%満足しています。 pycurl拡張機能とurllib2拡張機能の機能は似ていると思いますが、urllib2はSOCKSプロキシで動作できないことを除きます。 この記事のすべての例では、pycurlトランスポートを使用していますが、これはデフォルトで有効になっています。
 >>> g = Grab() >>> g.curl <pycurl.Curl object at 0x9d4ba04> >>> g.extensions [<grab.ext.pycurl.Extension object at 0xb749056c>, <grab.ext.lxml.Extension object at 0xb749046c>, <grab.ext.lxml_form.Extension object at 0xb6de136c>, <grab.ext.django.Extension object at 0xb6a7e0ac>] 


ハンマーモード


このモードはデフォルトで有効になっています。 グラブには、リクエストごとにタイムアウトがあります。 ハンマーモードでは、タイムアウトが発生した場合、Grabはすぐに例外をスローしませんが、タイムアウトを増やしながら要求を数回試行します。 このモードでは、プログラムの安定性を大幅に向上できます。 サイトの作業中のマイクロポーズやチャネルのギャップが非常に頻繁に発生します。 モードを有効にするには、 `hammer_mode`オプションを使用して、タイムアウトの数と長さを設定し、` hammer_timeouts`オプションを使用します。これに数値ペアのリストを渡します。応答を受け取ります。
 >>> import logging >>> logging.basicConfig(level=logging.DEBUG) >>> g = Grab() >>> g.setup(hammer_mode=True, hammer_timeouts=((1, 1), (2, 2), (30, 30))) >>> URL = 'http://download.wikimedia.org/enwiki/20110803/enwiki-20110803-stub-articles5.xml.gz' >>> g.go(URL, method='head') DEBUG:grab:[01] HEAD http://download.wikimedia.org/enwiki/20110803/enwiki-20110803-stub-articles5.xml.gz >>> print 'File size: %d Mb' % (int(g.response.headers['Content-Length']) / (1024 * 1024)) File size: 3 Mb >>> g.go(URL, method='get') DEBUG:grab:[02] GET http://download.wikimedia.org/enwiki/20110803/enwiki-20110803-stub-articles5.xml.gz DEBUG:grab:Trying another timeouts. Connect: 2 sec., total: 2 sec. DEBUG:grab:[03] GET http://download.wikimedia.org/enwiki/20110803/enwiki-20110803-stub-articles5.xml.gz DEBUG:grab:Trying another timeouts. Connect: 30 sec., total: 30 sec. DEBUG:grab:[04] GET http://download.wikimedia.org/enwiki/20110803/enwiki-20110803-stub-articles5.xml.gz >>> print 'Downloaded: %d Mb' % (len(g.response.body) / (1024 * 1024)) Downloaded: 3 Mb 


Django拡張機能(grab.ext.django)


はい、はい。 そのようなものが1つあります:-) ImageFieldフィールド `picture`を持つMovieモデルがあるとします。 画像をダウンロードしてムービーオブジェクトに保存する方法は次のとおりです。
 >>> obj = Movie.objects.get(pk=797) >>> g = Grab() >>> g.go('http://img.yandex.net/i/www/logo.png') >>> obj.picture = g.django_file() >>> obj.save() 


グラブには他に何がありますか?


他のチップもありますが、記事が大きすぎるのではないかと心配しています。 Grabライブラリのユーザーの主なルールは、何かが明確でない場合は、コードを調べる必要があるということです。 ドキュメントはまだ弱いです

開発計画


私は長年、Grabを使用してきました。これには、モスクワや他の都市で割引クーポンを購入できるアグリゲーターなどの生産現場も含まれます。 2011年に、テストとドキュメントの作成を開始しました。 多分、私はmulticurlに基づいた非同期リクエストの機能を書きます。 urllibトランスポートを終了するのもいいでしょう。

プロジェクトを支援するにはどうすればよいですか? 使用して、バグレポートとパッチを送ってください。 パーサー、グラバー 、情報処理スクリプトの作成を注文することもできます。 私はグラブを使ってこのようなことを定期的に書いています。

公式プロジェクトリポジトリ: bitbucket.org/lorien/grabライブラリはpypi.python.orgからも配信できますが、通常、リポジトリ内のコードは最新です。

UPD:コメントでは、シデに代わるあらゆる種類の選択肢を表明しました。 私はそれらをリスト+私の頭からの何かで要約することにしました。 実際、これらのワゴンと小型台車に代わるものがあります。 N人目のプログラマーは、いつの日か、ネットワークリクエスト用のユーティリティを用意することに決めたと思います。


UPD2:ライブラリに関する質問をgoogleグループに書いてください: groups.google.com/group/python-grab/他のグラブユーザーは、質問と回答を理解しておくと役立ちます。

UPD3:docs.grablib.org/で最新のドキュメントを入手できます

UPD4:現在のプロジェクトサイト: grablib.org

UPD5:記事のソースコードの例を修正しました。 次のアップグレード後、Habrahabrは、私が理解していなかった理由で古い記事のコードのフォーマットを修正し始めませんでした。 記事を修正してくれたAlexei Mazanovに感謝します。 また、彼はHabrに行きたいと思っています。招待があれば、彼のメール:egocentrist@me.com

Source: https://habr.com/ru/post/J127584/


All Articles