[python]新手寫爬蟲v2.5(使用代理的異步爬蟲)


開始

開篇:爬代理ip v2.0(未完待續),實現了獲取代理ips,並把這些代理持久化(存在本地)。同時使用的是tornado的HTTPClient的庫爬取內容。

中篇:開篇主要是獲取代理ip;中篇打算使用代理ip,同時優化代碼,並且異步爬取內容。所以接下來,就是寫一個:異步,使用代理的爬蟲。定義為:爬蟲 v2.5

為什么使用代理

開篇中我們爬來的代理ip怎么用?

在需要發送請求的時候,需要把請求,先發送到代理服務器(通過代理ip和端口),再由代理服務器請求目標網站。目標網站返回響應的時候也是先返回給代理服務器。所以代理(代理服務器)做的事情就是轉發請求,在轉發請求的時候有三種方式,也就是分別對應着,透明代理、匿名代理、高匿名代理,解釋請看開篇

所以,如果使用高匿名代理,就不會暴露真實的ip,目標網站只知道代理的ip。讓爬蟲循環使用把爬來的高匿名ip,從而降低,單一ip爬蟲的被封ip和訪問頻率的問題。

使用代理

1.事情要一步步的做,首先我需要驗證代理IP是否可用!最簡單的方法就是用Request庫,下面的例子,我就用Request官方文檔的示例

import requests

# 測試代理是否可用的URL,TEST_PROXY這個網站只返回訪問者的ip
TEST_PROXY = 'http://icanhazip.com'

proxies = {
  "http": "http://10.10.1.10:3128",
  "https": "http://10.10.1.10:1080",
}

requests.get(TEST_PROXY, proxies=proxies)

2.現在把爬取代理的方法和測試代理是否可用的方法,寫成一個Proxy類。Proxy類做的事情就是‘返回經過測試的可用代理’,整理后代碼如下:

class Proxy(object):
    """
    獲取代理ips
    """
    def __init__(self, url, **kwargs):
        self.response = Spider(url, **kwargs).get()

    def test_proxy(self):
        """ 返回經測試可用的代理 """
        fail_num = 1
        success_num = 1
        success_proxy = []
        for ip_info in self.ips_info:
            proxy_str = ip_info['proxy_host']+':'+ip_info['proxy_port']
            proxies = dict(http='http://'+proxy_str)
            try:
                requests.get("http://icanhazip.com", timeout=5, proxies=proxies)
            except Exception:
                print '失敗數:{}'.format(fail_num)
                fail_num += 1
                continue
            else:
                print '成功數:{}!'.format(success_num)
                success_num += 1
                success_proxy.append(ip_info)

        # 返回測試過,可用的代理
        print '結束:成功獲取{}個代理'.format(len(success_proxy))
        return success_proxy

    @property
    def ips_info(self):
        """ 清理內容得到IP信息 """
        ips_list = []
        html_body = self.response.body
        soup = BeautifulSoup(html_body, "html.parser")
        ip_list_table = soup.find(id='ip_list')
        for fi_ip_info in ip_list_table.find_all('tr'):
            ip_detail = fi_ip_info.find_all('td')
            if ip_detail:
                # 注意:為什么我用list和str方法?否則就是bs4對象!!!
                ips_list.append(dict(proxy_host=str(list(ip_detail)[2].string),
                                     proxy_port=str(list(ip_detail)[3].string)))
        return ips_list

3.現在有Proxy類和Content類(開篇存儲代理),下面寫一個方法,把得到的,可用的代理ip,存到數據庫中或許文本中。Content類,我修改一些地方,代碼整理如下:

def get_proxy_ips():
    """ 獲取代理ips,並存儲 """
    try:
        proxy = Proxy(url=URL, headers=CLIENT_CONFIG['headers'])
        ips_list = proxy.test_proxy()
        print ips_list
    except HTTPError as e:
        print '{}:Try again!!!'.format(e)
        get_proxy_ips()
    else:
        # 存到數據庫中
        t = Content(models.Proxy)
        for ip_data in ips_list:
            t.save(ip_data)
        # 默認存到運行運行腳本的目錄,文件名:data.txt
        t = Content()
        t.save_to_file(ips_list)

好了,下面就要開始寫異步的代理爬蟲了。

異步的代理爬蟲

tornado的所有HTTPClient中,只有CurlAsyncHTTPClient支持代理。具體的實現邏輯請查看curl_httpclient源碼,它也支持異步,所以接下來就是使用CurlAsyncHTTPClient的get方法,帶着代理的host和port(注意:不需要指明協議)。代碼如下:

@gen.coroutine
def main():
    flag = 1
    ips_list = models.Proxy.find_all()
    for ip in ips_list:
        while 1:
            print 'proxy_ip {}:{}'.format(ip['proxy_host'], ip['proxy_port'])
            try:
                s = Spider(TEST, headers=CLIENT_CONFIG['headers'],
                           proxy_host=ip['proxy_host'], request_timeout=5,
                           proxy_port=int(ip['proxy_port']))

                response = yield s.async_get()
                print 'NO:{}: status {}'.format(flag, response.code)

            except HTTPError, e:
                print '換代理,錯誤信息:{}'.format(e)
                break
            else:
                flag += 1

if __name__ == '__main__':
	get_proxy_ips()
    IOLoop().run_sync(main)

優化改進

寫完這個主爬蟲,我思考了一下,那是否test_proxy方法是否也可以用異步呢?還有就是不應該使用requests庫,因為tornado使用的PycURL庫,同時我去看官方文檔的時候關於速度的說明,PycURL文檔

Speed - libcurl is very fast and PycURL, being a thin wrapper above libcurl, is very fast as well. PycURL was benchmarked to be several times faster than requests.

最后,支持直接用行腳本,兼容不使用數據庫。全部代碼如下:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
#   Author  :   XueWeiHan
#   E-mail  :   595666367@qq.com
#   Date    :   16/3/31 下午4:21
#   Desc    :   爬蟲 v2.5
from bs4 import BeautifulSoup
from tornado.httpclient import HTTPRequest, HTTPClient, HTTPError
from tornado.curl_httpclient import CurlAsyncHTTPClient
from tornado import gen
from tornado.ioloop import IOLoop

try:
    from model import db
    from model import models
    from config import configs
    NO_DB = 1
    # 連接數據庫
    db.create_engine(**configs['db'])
except ImportError:
    # NO_DB表示不用數據庫
    NO_DB = 0
    print "Can't use db"
from client_config import CLIENT_CONFIG

# 測試用的訪問目標(github API)
TEST = 'https://api.github.com/search/users?q=tom+repos:%3E42+followers:%3E1000'

# 測試代理是否可用的URL
TEST_PROXY = 'http://icanhazip.com'

# 獲取代理的目標網站
URL = 'http://www.xicidaili.com/nn/'  # 高匿ip
#URL = 'http://www.xicidaili.com/nt/'  # 透明ip


class Spider(object):
    """
    爬
    """
    def __init__(self, url, **kwargs):
        self.request = HTTPRequest(url, **kwargs)

    @gen.coroutine
    def async_get(self, **kwargs):
        """ 異步get """
        ## 注意:只有CurlAsyncHTTPClient支持代理,所以這里用它
        response = yield CurlAsyncHTTPClient().fetch(self.request, **kwargs)
        raise gen.Return(response)

    def get(self, **kwargs):
        """ 同步get """
        return HTTPClient().fetch(self.request, **kwargs)

    def post(self):
        """ post暫時沒用,先占坑 """
        self.request.method = "POST"
        return HTTPClient().fetch(self.request)


class Content(object):
    """
    存儲(持久化)相關操作
    """
    def __init__(self, model=None):
        self.model = model

    def save(self, save_dict=None):
        """ 存到數據庫 """
        if self.model:
            if save_dict:
                data = self.model(**save_dict)
                data.insert()
            else:
                print 'no save_dict'
        else:
            print 'no model'

    @staticmethod
    def save_to_file(all_content, str_split=':', path='./data.txt'):
        """
        把數據存到文件中
        :param all_content: 需要是list類型
        :param str_split: 分割符號
        :param path: 文件位置,默認為當前腳本運行的位置,文件名:data.txt
        """
        with open(path, 'w') as fb:
            print '開始寫入文件'
            for content in all_content:
                content_str = ''
                for k, v in content.items():
                    content_str += v + str_split
                fb.write(content_str+'\n')
            print '寫入文件完成'


class Proxy(object):
    """
    獲取代理ips
    """
    def __init__(self, url, **kwargs):
        self.response = Spider(url, **kwargs).get()

    @gen.coroutine
    def test_proxy(self):
        """ 返回經測試可用的代理 """
        # flag用於計數
        flag = 1
        all_ips = self.ips_info()
        print '初始化爬到{}個代理,下面開始測試這些代理的可用性:'.format(len(all_ips))
        success_proxy = []
        for ip_info in all_ips:
            try:
                s = Spider(TEST_PROXY, headers=CLIENT_CONFIG['headers'],
                           proxy_host=ip_info['proxy_host'], request_timeout=5,
                           proxy_port=int(ip_info['proxy_port']))

                yield s.async_get()
            except Exception:
                print '第{}個,失敗。'.format(flag)
                continue
            else:
                print '第{}個:成功!'.format(flag)
                success_proxy.append(ip_info)
            finally:
                flag += 1

        # 返回測試過,可用的代理
        print '經測試:{}個可用,可用率:{}%'.format(len(success_proxy),
                                         len(success_proxy)/len(all_ips))
        raise gen.Return(success_proxy)

    def ips_info(self):
        """ 清理內容得到IP信息 """
        ips_list = []
        html_body = self.response.body
        soup = BeautifulSoup(html_body, "html.parser")
        ip_list_table = soup.find(id='ip_list')
        for fi_ip_info in ip_list_table.find_all('tr'):
            ip_detail = fi_ip_info.find_all('td')
            if ip_detail:
                # 注意:為什么我用list和str方法?否則就是bs4對象!!!
                ips_list.append(dict(proxy_host=str(list(ip_detail)[2].string),
                                     proxy_port=str(list(ip_detail)[3].string)))
        return ips_list


@gen.coroutine
def get_proxy_ips():
    """ 獲取代理ips,並存儲 """
    try:
        proxy = Proxy(url=URL, headers=CLIENT_CONFIG['headers'])
        ips_list = yield proxy.test_proxy()
    except HTTPError as e:
        print 'Try again! Error info:{}'.format(e)
    else:
        if NO_DB:
            # 存到數據庫中
            t = Content(models.Proxy)
            for ip_data in ips_list:
                t.save(ip_data)
        # 默認存到運行運行腳本的目錄,文件名:data.txt
        t = Content()
        t.save_to_file(ips_list)
    raise gen.Return(ips_list)


@gen.coroutine
def main():
    """ 使用代理的異步爬蟲 """
    flag = 1
    ips_list = yield get_proxy_ips()
    for ip in ips_list:
        while 1:
            print 'Use proxy ip {}:{}'.format(ip['proxy_host'], ip['proxy_port'])
            try:
                # 這里就是異步的代理爬蟲,利用代理獲取目標網站的信息
                s = Spider(TEST, headers=CLIENT_CONFIG['headers'],
                           proxy_host=ip['proxy_host'], request_timeout=10,
                           proxy_port=int(ip['proxy_port']))

                # response爬蟲返回的response對象,response.body就是內容
                response = yield s.async_get()
                print 'NO:{}: status {}'.format(flag, response.code)
            except HTTPError, e:
                print '換代理,錯誤信息:{}'.format(e)
                break
            else:
                flag += 1

if __name__ == '__main__':
    IOLoop().run_sync(main)

運行效果:

TODO

  1. 這個腳本可用性還是很差,因為爬的是免費代理,代理的穩定性很差,很差,很差!!!會導致,測試可用的代理。但是在使用的時候就連不上了!所以,后面想學習怎么自己搞到代理。我發現使用代理的爬蟲,代理的穩定性,至關重要!應該會用到的技術:掃端口?計算機網絡的知識。

  2. 現在的流程是:爬取並測試所有的代理,然后持久化可用的代理。后面想做成,如果有成功的代理,后面的爬蟲就工作,使用成功的代理爬取目標URL。應該會用到的技術:隊列,多線程

  3. 速度還是很慢。

暫時就想到這么多。所有的代碼都在我的github


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM