グラブ-サむトを解析するための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



All Articles