Python으로 웹사이트를 크롤링하기 위한 실용적인 팁

高洛峰
풀어 주다: 2017-02-25 13:41:51
원래의
1248명이 탐색했습니다.

머리말

제가 작성한 스크립트에는 한 가지 공통점이 있습니다. 모두 웹과 관련이 있으며, 항상 많이 사용됩니다. 앞으로 작업을 반복할 필요가 없도록 웹사이트의 경험을 여기에 요약하겠습니다.

1.가장 기본적인 사이트 잡기

2. 프록시 서버를 사용하세요

IP가 차단되거나 IP 방문 횟수가 제한되는 등 특정 상황에서 더 유용합니다.

아아아아

3. 로그인이 필요한 상황

로그인 상황이 더 귀찮습니다. 문제를 분석해 보겠습니다.

3.1 쿠키 처리

import urllib2
content = urllib2.urlopen('http://XXXX').read()
로그인 후 복사

예, 프록시와 쿠키를 동시에 추가한 다음 proxy_support을 추가하고 operner

import urllib2
proxy_support = urllib2.ProxyHandler({'http':'http://XX.XX.XX.XX:XXXX'})
opener = urllib2.build_opener(proxy_support, urllib2.HTTPHandler)
urllib2.install_opener(opener)
content = urllib2.urlopen('http://XXXX').read()
로그인 후 복사

로 변경합니다. 3.2 양식 처리

로그인을 위해서는 양식이 필요합니다. 양식은 어떻게 작성하나요? 먼저 도구를 사용하여 작성할 양식의 내용을 가로챕니다. <… 요청 및 POST 양식 항목:

verycd가 보이면

사용자 이름, 비밀번호, continueURI, fk, login_submitPython으로 웹사이트를 크롤링하기 위한 실용적인 팁을 입력해야 합니다. , 그 중 fk는 무작위로 생성됩니다(실제로는 그다지 무작위가 아니며 단순히 에포크 시간을 인코딩하여 생성된 것처럼 보입니다). 이는 웹 페이지에서 먼저 방문하여 사용해야 함을 의미합니다. 반환된 데이터를 가로채는 정규식과 같은 도구입니다.

continueURI는 이름에서 알 수 있듯이 아무렇게나 작성할 수 있는 반면, login_submit은 소스 코드에서 볼 수 있는 고정

입니다. 그리고 사용자 이름, 비밀번호는 분명합니다. 자, 입력할 데이터를 가지고 postdata를 생성해야 합니다

import urllib2, cookielib
cookie_support= urllib2.HTTPCookieProcessor(cookielib.CookieJar())
opener = urllib2.build_opener(cookie_support, urllib2.HTTPHandler)
urllib2.install_opener(opener)
content = urllib2.urlopen(&#39;http://XXXX&#39;).read()
로그인 후 복사

그런 다음 http 요청을 생성하고 요청:

opener = urllib2.build_opener(proxy_support, cookie_support, urllib2.HTTPHandler)
로그인 후 복사

3.3 브라우저인 척 방문

일부 웹사이트는 크롤러의 방문에 혐오감을 느낍니다. , 그래서 그들은 모든 크롤러의 요청을 거부합니다. 현재 우리는 http 패키지의 헤더를 수정하여 브라우저로 위장해야 합니다:

import urllib
postdata=urllib.urlencode({
 &#39;username&#39;:&#39;XXXXX&#39;,
 &#39;password&#39;:&#39;XXXXX&#39;,
 &#39;continueURI&#39;:&#39;http://www.verycd.com/&#39;,
 &#39;fk&#39;:fk,
 &#39;login_submit&#39;:&#39;登录&#39;
})
로그인 후 복사

3.4 Anti- "anti-hotlinking"

일부 사이트에는 소위 안티 리칭 설정이 있습니다. 실제로 요청 헤더에 리퍼러 사이트 자체가 있는지 확인하는 것은 매우 간단합니다. 보내기 때문에 3.3과 동일한 작업만 하면 됩니다. 헤더의 리퍼러를 웹사이트로 변경하면 됩니다. 유명한 shady cnbeta를 예로 들어 보겠습니다.

req = urllib2.Request(
 url = &#39;http://secure.verycd.com/signin/*/http://www.php.cn/&#39;,
 data = postdata
)
result = urllib2.urlopen(req).read()
로그인 후 복사

헤더는 dict 데이터 구조이므로 변장하려는 헤더를 넣을 수 있습니다. 예를 들어, 일부 스마트 웹사이트는 항상 사람들의 개인 정보를 엿보는 것을 좋아합니다. 누군가가 프록시를 통해 액세스하는 경우 헤더의 X-Forwarded-For를 읽어서 그 사람의 실제 IP를 확인합니다. X-Forwarded-For 바꾸세요. 그 사람을 괴롭히고 괴롭히는 재미있는 것으로 바꾸시면 됩니다, 하하.

3.5 최종 트릭

가끔 3.1~3.4를 해도 접속이 차단되는 경우가 있으니 다른 방법은 없고 보이는 헤더만 제거하면 됩니다 httpfox에서 모두 적어두면 일반적으로 문제가 없습니다. 작동하지 않으면 Selenium은 브라우저에서 액세스를 직접 제어할 수 있는 유일한 방법을 사용합니다. 유사한 것에는 pamie, watir 등이 포함됩니다.

4. 멀티 스레드 동시 크롤링

단일 스레드가 너무 느리면 멀티 스레드가 필요합니다. pool 템플릿 프로그램은 단순히 1~10까지 출력하지만 동시에 수행되는 것을 볼 수 있다.

rree

5. 인증코드 처리

이런 경우 어떻게 해야 하나요? 인증코드요? 여기서 처리해야 할 상황은 두 가지입니다. 1. Google과 같은 인증 코드, 멋지네요

2. 단순 인증 코드: 문자 수는 제한되어 있으며 단순 번역 또는 회전 + 노이즈만 있습니다. 왜곡 없이 사용하면 이런 문제를 해결할 수 있습니다. 일반적인 아이디어는 다시 회전시켜 노이즈를 제거한 다음 분할이 완료된 후 특징 추출 방법을 사용하는 것입니다. PCA로) 차원을 줄이고 기능 라이브러리를 생성한 다음 확인 코드를 기능 데이터베이스와 비교합니다. 이는 매우 복잡하고 한 블로그 게시물에서 설명할 수 없으므로 여기서는 구체적인 방법을 연구하기 위해 관련 교과서를 구해 자세히 설명하지 않겠습니다.

사실 일부 인증코드는 아직 매우 취약하기 때문에 여기서는 이름을 밝히지 않겠습니다. 어쨌든 2번 방법을 통해 아주 높은 정확도로 인증코드를 추출했으니 실제로는 2번이 가능합니다.

6gzip/deflate 지원

现在的网页普遍支持gzip压缩,这往往可以解决大量传输时间,以 VeryCD 的主页为例,未压缩版本247K,压缩了以后45K,为原来的1/5。这就意味着抓取速度会快5倍。

然而python的urllib/urllib2默认都不支持压缩,要返回压缩格式,必须在request的header里面写明'accept-encoding',然后读取response后更要检查header查看是否有'content-encoding'一项来判断是否需要解码,很繁琐琐碎。如何让urllib2自动支持gzip, defalte呢?

其实可以继承 BaseHanlder 类,然后build_opener的方式来处理:

import urllib2
from gzip import GzipFile
from StringIO import StringIO
class ContentEncodingProcessor(urllib2.BaseHandler):
 """A handler to add gzip capabilities to urllib2 requests """
 
 # add headers to requests
 def http_request(self, req):
 req.add_header("Accept-Encoding", "gzip, deflate")
 return req
 
 # decode
 def http_response(self, req, resp):
 old_resp = resp
 # gzip
 if resp.headers.get("content-encoding") == "gzip":
  gz = GzipFile(
     fileobj=StringIO(resp.read()),
     mode="r"
     )
  resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code)
  resp.msg = old_resp.msg
 # deflate
 if resp.headers.get("content-encoding") == "deflate":
  gz = StringIO( deflate(resp.read()) )
  resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code) # &#39;class to add info() and
  resp.msg = old_resp.msg
 return resp
 
# deflate support
import zlib
def deflate(data): # zlib only provides the zlib compress format, not the deflate format;
 try:    # so on top of all there&#39;s this workaround:
 return zlib.decompress(data, -zlib.MAX_WBITS)
 except zlib.error:
 return zlib.decompress(data)
로그인 후 복사

然后就简单了,

encoding_support = ContentEncodingProcessor
opener = urllib2.build_opener( encoding_support, urllib2.HTTPHandler )
 
#直接用opener打开网页,如果服务器支持gzip/defalte则自动解压缩
content = opener.open(url).read()
로그인 후 복사

7. 更方便地多线程

总结一文的确提及了一个简单的多线程模板,但是那个东东真正应用到程序里面去只会让程序变得支离破碎,不堪入目。在怎么更方便地进行多线程方面我也动了一番脑筋。先想想怎么进行多线程调用最方便呢?

1、用twisted进行异步I/O抓取

事实上更高效的抓取并非一定要用多线程,也可以使用异步I/O法:直接用twisted的getPage方法,然后分别加上异步I/O结束时的callback和errback方法即可。例如可以这么干:

from twisted.web.client import getPage
from twisted.internet import reactor
 
links = [ &#39;http://www.verycd.com/topics/%d/&#39;%i for i in range(5420,5430) ]
 
def parse_page(data,url):
 print len(data),url
 
def fetch_error(error,url):
 print error.getErrorMessage(),url
 
# 批量抓取链接
for url in links:
 getPage(url,timeout=5) \
  .addCallback(parse_page,url) \ #成功则调用parse_page方法
  .addErrback(fetch_error,url)  #失败则调用fetch_error方法
 
reactor.callLater(5, reactor.stop) #5秒钟后通知reactor结束程序
reactor.run()
로그인 후 복사

twisted人如其名,写的代码实在是太扭曲了,非正常人所能接受,虽然这个简单的例子看上去还好;每次写twisted的程序整个人都扭曲了,累得不得了,文档等于没有,必须得看源码才知道怎么整,唉不提了。

如果要支持gzip/deflate,甚至做一些登陆的扩展,就得为twisted写个新的 HTTPClientFactory 类诸如此类,我这眉头真是大皱,遂放弃。有毅力者请自行尝试。

2、设计一个简单的多线程抓取类

还是觉得在urllib之类python“本土”的东东里面折腾起来更舒服。试想一下,如果有个Fetcher类,你可以这么调用

f = Fetcher(threads=10) #设定下载线程数为10
for url in urls:
 f.push(url) #把所有url推入下载队列
while f.taskleft(): #若还有未完成下载的线程
 content = f.pop() #从下载完成队列中取出结果
 do_with(content) # 处理content内容
로그인 후 복사

这么个多线程调用简单明了,那么就这么设计吧,首先要有两个队列,用Queue搞定,多线程的基本架构也和“技巧总结”一文类似,push方法和pop方法都比较好处理,都是直接用Queue的方法,taskleft则是如果有“正在运行的任务”或者”队列中的任务”则为是,也好办,于是代码如下:

import urllib2
from threading import Thread,Lock
from Queue import Queue
import time
 
class Fetcher:
 def __init__(self,threads):
  self.opener = urllib2.build_opener(urllib2.HTTPHandler)
  self.lock = Lock() #线程锁
  self.q_req = Queue() #任务队列
  self.q_ans = Queue() #完成队列
  self.threads = threads
  for i in range(threads):
   t = Thread(target=self.threadget)
   t.setDaemon(True)
   t.start()
  self.running = 0
 
 def __del__(self): #解构时需等待两个队列完成
  time.sleep(0.5)
  self.q_req.join()
  self.q_ans.join()
 
 def taskleft(self):
  return self.q_req.qsize()+self.q_ans.qsize()+self.running
 
 def push(self,req):
  self.q_req.put(req)
 
 def pop(self):
  return self.q_ans.get()
 
 def threadget(self):
  while True:
   req = self.q_req.get()
   with self.lock: #要保证该操作的原子性,进入critical area
    self.running += 1
   try:
    ans = self.opener.open(req).read()
   except Exception, what:
    ans = &#39;&#39;
    print what
   self.q_ans.put((req,ans))
   with self.lock:
    self.running -= 1
   self.q_req.task_done()
   time.sleep(0.1) # don&#39;t spam
 
if __name__ == "__main__":
 links = [ &#39;http://www.verycd.com/topics/%d/&#39;%i for i in range(5420,5430) ]
 f = Fetcher(threads=10)
 for url in links:
  f.push(url)
 while f.taskleft():
  url,content = f.pop()
  print url,len(content)
로그인 후 복사

8. 一些琐碎的经验

1、连接池:

opener.open和urllib2.urlopen一样,都会新建一个http请求。通常情况下这不是什么问题,因为线性环境下,一秒钟可能也就新生成一个请求;然而在多线程环境下,每秒钟可以是几十上百个请求,这么干只要几分钟,正常的有理智的服务器一定会封禁你的。

然而在正常的html请求时,保持同时和服务器几十个连接又是很正常的一件事,所以完全可以手动维护一个 HttpConnection 的池,然后每次抓取时从连接池里面选连接进行连接即可。

这里有一个取巧的方法,就是利用squid做代理服务器来进行抓取,则squid会自动为你维护连接池,还附带数据缓存功能,而且squid本来就是我每个服务器上面必装的东东,何必再自找麻烦写连接池呢。

2、设定线程的栈大小

栈大小的设定将非常显著地影响python的内存占用,python多线程不设置这个值会导致程序占用大量内存,这对openvz的vps来说非常致命。stack_size必须大于32768,实际上应该总要32768*2以上

from threading import stack_size
stack_size(32768*16)
로그인 후 복사

3、设置失败后自动重试

 def get(self,req,retries=3):
  try:
   response = self.opener.open(req)
   data = response.read()
  except Exception , what:
   print what,req
   if retries>0:
    return self.get(req,retries-1)
   else:
    print &#39;GET Failed&#39;,req
    return &#39;&#39;
  return data
로그인 후 복사

4、设置超时

 import socket
 socket.setdefaulttimeout(10) #设置10秒后连接超时
로그인 후 복사

登陆更加简化了,首先build_opener中要加入cookie支持,如要登陆 VeryCD ,给Fetcher新增一个空方法login,并在 init ()中调用,然后继承Fetcher类并override login方法:

def login(self,username,password):
 import urllib
 data=urllib.urlencode({&#39;username&#39;:username,
       &#39;password&#39;:password,
       &#39;continue&#39;:&#39;http://www.verycd.com/&#39;,
       &#39;login_submit&#39;:u&#39;登录&#39;.encode(&#39;utf-8&#39;),
       &#39;save_cookie&#39;:1,})
 url = &#39;http://www.verycd.com/signin&#39;
 self.opener.open(url,data).read()
로그인 후 복사

于是在Fetcher初始化时便会自动登录 VeryCD 网站。

9. 总结

如此,以上就是总结Python으로 웹사이트를 크롤링하기 위한 실용적인 팁的全部内容了,本文内容代码简单,使用方便,性能也不俗,相信对各位使用python有很大的帮助。

更多Python으로 웹사이트를 크롤링하기 위한 실용적인 팁相关文章请关注PHP中文网!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!