爬蟲代理小記與aiohttp代理嘗試

總結了一些爬蟲代理的資料和知識,并嘗試使用asyncio和aiohttp使用代理ip訪問目標網站,按代理IP的訪問效果實時更新代理IP得分,初始獲取3000左右代理IP,在穩定后,對摩拜單車信息的訪問可以達到40次/秒-100次/秒。

代理IP方案簡述

  • 快速抓取網站時,要應對IP每分鐘訪問次數有限制甚至會封IP的服務器,使用代理IP可以幫助我們。
  • Kaito爬蟲代理服務講得很清楚,推薦。
  • Github上現成的開源代理爬蟲也不少,如:
    qiyeboy/IPProxyPool
    jhao104/proxy_pool
    awolfly9/IPProxyTool
    fancoo/Proxy
    derekhe/mobike-crawler/modules/ProxyProvider.py
  • 思路也都很清楚,從更多的代理網站上爬取免費代理,存入自己的數據庫,定時更新。在拿到代理IP后,驗證該代理的有效性。并且提供簡單的API來獲取代理IP。
  • 七夜的博客python開源IP代理池--IPProxys詳細地闡釋了自己的代碼。
  • 大部分的代理網站的爬取還是比較簡單的,在上述開源的代碼中包含了不少代理網站的爬取與解析。困難點的有js反爬機制,也都被用selenium操作無頭webkit或者js代碼解析以及python的js代碼執行庫所解決。此外有趣的是,在上面的開源代碼中出現了用爬取得到的代理來訪問代理網站的情況。
  • 定時刷新代理,有自定義的代理定時刷新模塊,也可用celery定時任務。
  • 驗證有效性的方式有:
  • API的提供可以用BaseHTTPServer拓展下,也可用簡便的flask或者Django加上插件提供restful api服務。
  • 對于免費的代理IP來說,最重要的一是量大,就算有很大比例無效的,還是能拿到一些高質量的代理。一個網站的未篩選代理能有幾千個,多個網站就很可觀了,當然要考慮到重復。
  • 再就是代理IP的篩選機制,不少開源庫都添加了評分機制,這是非常重要的。例如利用對累計超時次數以及成功率的加權來評判代理IP的質量。在每次使用后都對代理IP的情況進行評價,以此來刷新數據庫,方便下一次選取優質的代理IP。
  • 如何對代理IP進行評價,在成功和失敗的各種情況中如何獎懲,篩選出最優質的代理IP是非常重要的。
  • 此外,每個代理IP的使用也要考慮是否要設置一定的使用間隔,避免過于頻繁導致失效。

嘗試

  • 自然,首先要做的就是從免費代理網站上獲取大量代理IP,我選擇了最方便的66ip,接口很簡單,一次性訪問可以拿到3000左右的代理,當然,頻繁訪問會導致js反爬機制,這時再簡單地使用selenium+phantomJs即可。
    url = ("http://m.66ip.cn/mo.php?tqsl={proxy_number}")
    url = url.format(proxy_number=10000)
    html = requests.get(url, headers=headers).content
    html = html.decode(chardet.detect(html)['encoding'])
    pattern = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}'
    all_ip = re.findall(pattern, html)
  • 然后就是設置代理IP的獎懲機制,我參考了摩拜單車爬蟲源碼及解析使用類,看起來簡單清晰,每個代理IP對象擁有自己的ip與分數,@property令它們可以被點操作符訪問。初始分數為100分,這里分數都弄成整數。如果代理成功,按照延時的大小來給予獎勵,即調用代理的相應方法,最高10分,而超時扣10分,連接錯誤扣30分,其它錯誤扣50分(可以酌情修改)。為了便于代理IP之間的比較,修改了__lt__方法。
class Proxy:
    def __init__(self, ip):
        self._url = 'http://' + ip
        self._score = 100

    @property
    def url(self):
        return self._url

    @property
    def score(self):
        return self._score

    def __lt__(self, other):
        '''
        由于優先隊列是返回最小的,而這里分數高的代理優秀
        所以比較時反過來
        '''
        return self._score > other._score

    def success(self, time):
        self._score += int(10 / int(time + 1))

    def timeoutError(self):
        self._score -= 10

    def connectError(self):
        self._score -= 30

    def otherError(self):
        self._score -= 50
  • 感覺上,最好的驗證方式是直接訪問目標網站。除去根本不能用的一部分,代理IP的有效性對不同的目標網站是有區別的,在我的嘗試中,豆瓣相比摩拜對代理IP的應對明顯更好,這里代碼為訪問摩拜。在第一輪的篩選中,對每個代理IP,訪問兩次目標網站,超時時間為10秒,依情況獎懲,保留分數大于50的。總共花費22秒左右時間,排除了一小部分根本不能使用的代理,也對所有代理的分數初步更新。
async def douban(proxy, session):
# 使用代理訪問目標網站,并按情況獎懲代理
    try:
        start = time.time()
        async with session.post(mobike_url,
                                data=data,
                                proxy=proxy.url,
                                headers=headers,  # 可以引用到外部的headers
                                timeout=10) as resp:
            end = time.time()
            # print(resp.status)
            if resp.status == 200:
                proxy.success(end - start)
                print('%6.3d' % proxy._score, 'Used time-->', end - start, 's')
            else:
                proxy.otherError()
                print('*****', resp.status, '*****')
    except TimeoutError as te:
        print('%6.3d' % proxy._score, 'timeoutError')
        proxy.timeoutError()
    except ClientConnectionError as ce:
        print('%6.3d' % proxy._score, 'connectError')
        proxy.connectError()
    except Exception as e:
        print('%6.3d' % proxy._score, 'otherError->', e)
        proxy.otherError()
# ClientHttpProxyError

# TCPConnector維持鏈接池,限制并行連接的總量,當池滿了,有請求退出再加入新請求,500和100相差不大
# ClientSession調用TCPConnector構造連接,Session可以共用
# Semaphore限制同時請求構造連接的數量,Semphore充足時,總時間與timeout差不多


async def initDouban():

    conn = aiohttp.TCPConnector(verify_ssl=False,
                                limit=100,  # 連接池在windows下不能太大
                                use_dns_cache=True)
    tasks = []
    async with aiohttp.ClientSession(loop=loop, connector=conn) as session:
        for p in proxies:
            task = asyncio.ensure_future(douban(p, session))
            tasks.append(task)

        responses = asyncio.gather(*tasks)
        await responses
    conn.close()


def firstFilter():
    for i in range(2):
        s = time.time()
        future = asyncio.ensure_future(initDouban())
        loop.run_until_complete(future)
        e = time.time()
        print('----- init time %s-----\n' % i, e - s, 's')

    num = 0
    pq = PriorityQueue()
    for proxy in proxies:
        if proxy._score > 50:
            pq.put_nowait(proxy)
            num += 1
    print('原始ip數:%s' % len(all_ip), '; 篩選后:%s' % num)
    return pq
  • 然后就是正式的訪問了,這里我使用了基于堆的asyncio優先隊列(非線程安全)。通過asyncio.Semaphore限制并發請求連接的數量,不斷地從隊列中拿取最優質的代理IP,在訪問結束后再將它放回隊列。結果是,多個連接不會同時使用一個代理IP,如果代理成功,它將會很快被放回隊列,再次使用。(如果需要設置成功代理的使用間隔,可以改為在訪問成功后,先釋放連接與信號量,然后使用asyncio.sleep(x)等待一段時間再放入優先隊列,如果在genDouban函數里實現,可設置為range(concurrency)一定程度上大于Semaphore(concurrency))獎懲一直進行,一開始會有一段篩選的過程,穩定后的輸出如下:
pq = firstFilter()


async def genDouban(sem, session):
    # Getter function with semaphore.
    while True:
        async with sem:
            proxy = await pq.get()
            await douban(proxy, session)
            await pq.put(proxy)


async def dynamicRunDouban(concurrency):
    '''
    TCPConnector維持鏈接池,限制并行連接的總量,當池滿了,有請求退出再加入新請求
    ClientSession調用TCPConnector構造連接,Session可以共用
    Semaphore限制同時請求構造連接的數量,Semphore充足時,總時間與timeout差不多
    '''
    conn = aiohttp.TCPConnector(verify_ssl=False,
                                limit=concurrency,
                                use_dns_cache=True)
    tasks = []
    sem = asyncio.Semaphore(concurrency)

    async with aiohttp.ClientSession(loop=loop, connector=conn) as session:
        try:
            for i in range(concurrency):
                task = asyncio.ensure_future(genDouban(sem, session))
                tasks.append(task)

            responses = asyncio.gather(*tasks)
            await responses
        except KeyboardInterrupt:
            print('-----finishing-----\n')
            for task in tasks:
                task.cancel()
            if not conn.closed:
                conn.close()


future = asyncio.ensure_future(dynamicRunDouban(200))
loop.run_until_complete(future)
  • 最后,我們中斷程序,查看下代理IP的得分情況:
scores = [p.score for p in proxies]
scores.sort(reverse=True)
print('Most popular IPs:\n ------------\n', scores[:50],
      [i for i in scores if i > 100])
loop.is_closed()

其它方案概覽:

  • 在Scrapy官方文檔避免被封的建議提到了Tor - 洋蔥路由

    use a pool of rotating IPs. For example, the free Tor project or paid services like ProxyMesh. An open source alterantive is scrapoxy, a super proxy that you can attach your own proxies to.

  • 在知乎python 爬蟲 ip池怎么做?的回答中,提到了Squid與修改x-forward-for標簽的方法:
    1. 使用squid的cache_peer機制,把這些代理按照一定格式(具體格式參考文檔)寫入到配置文件中,配置好squid的端口,那么squid就可以幫你調度代理了,而且還可以摒棄失效的代理。
    2. 在訪問的http request里添加x-forward-for標簽client隨機生成,宣稱自己是一臺透明代理服務器
  • 如何突破豆瓣爬蟲限制頻率?中提到:

    用帶 bid (可以偽造)的 cookie 去訪問 - github

其他資料

代碼

from selenium import webdriver
import time
import aiohttp
from aiohttp.client_exceptions import ClientConnectionError
from aiohttp.client_exceptions import TimeoutError
import asyncio
from asyncio.queues import PriorityQueue
import chardet
import re
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

headers = {'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                          'AppleWebKit/537.36 (KHTML, like Gecko) ')}
loop = asyncio.get_event_loop()


class Proxy:
    def __init__(self, ip):
        self._url = 'http://' + ip
        self._score = 100

    @property
    def url(self):
        return self._url

    @property
    def score(self):
        return self._score

    def __lt__(self, other):
        '''
        由于優先隊列是返回最小的,而這里分數高的代理優秀
        所以比較時反過來
        '''
        return self._score > other._score

    def success(self, time):
        self._score += int(10 / int(time + 1))

    def timeoutError(self):
        self._score -= 10

    def connectError(self):
        self._score -= 30

    def otherError(self):
        self._score -= 50


def getProxies():
    url = ("http://m.66ip.cn/mo.php?tqsl={proxy_number}")
    url = url.format(proxy_number=10000)
    html = requests.get(url, headers=headers).content
    html = html.decode(chardet.detect(html)['encoding'])
    pattern = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}'
    all_ip = re.findall(pattern, html)
    if len(all_ip) == 0:
        driver = webdriver.PhantomJS(
            executable_path=r'D:/phantomjs/bin/phantomjs.exe')
        driver.get(url)
        time.sleep(12)  # js等待5秒
        html = driver.page_source
        driver.quit()
        all_ip = re.findall(pattern, html)
    with open('66ip_' + str(time.time()), 'w', encoding='utf-8') as f:
        f.write(html)
    return all_ip


all_ip = set(getProxies()) | set(getProxies())
proxies = [Proxy(proxy) for proxy in all_ip]

mobike_url = "https://mwx.mobike.com/mobike-api/rent/nearbyBikesInfo.do"
data = {  # 請求參數: 緯度,經度!
    'latitude': '33.2',
    'longitude': '113.4',
}
headers = {
    'referer': "https://servicewechat.com/",
}


async def douban(proxy, session):

    try:
        start = time.time()
        async with session.post(mobike_url,
                                data=data,
                                proxy=proxy.url,
                                headers=headers,  # 可以引用到外部的headers
                                timeout=10) as resp:
            end = time.time()
            # print(resp.status)
            if resp.status == 200:
                proxy.success(end - start)
                print('%6.3d' % proxy._score, 'Used time-->', end - start, 's')
            else:
                proxy.otherError()
                print('*****', resp.status, '*****')
    except TimeoutError as te:
        print('%6.3d' % proxy._score, 'timeoutError')
        proxy.timeoutError()
    except ClientConnectionError as ce:
        print('%6.3d' % proxy._score, 'connectError')
        proxy.connectError()
    except Exception as e:
        print('%6.3d' % proxy._score, 'otherError->', e)
        proxy.otherError()
# ClientHttpProxyError

# TCPConnector維持鏈接池,限制并行連接的總量,當池滿了,有請求退出再加入新請求,500和100相差不大
# ClientSession調用TCPConnector構造連接,Session可以共用
# Semaphore限制同時請求構造連接的數量,Semphore充足時,總時間與timeout差不多


async def initDouban():

    conn = aiohttp.TCPConnector(verify_ssl=False,
                                limit=100,  # 連接池在windows下不能太大, <500
                                use_dns_cache=True)
    tasks = []
    async with aiohttp.ClientSession(loop=loop, connector=conn) as session:
        for p in proxies:
            task = asyncio.ensure_future(douban(p, session))
            tasks.append(task)

        responses = asyncio.gather(*tasks)
        await responses
    conn.close()


def firstFilter():
    for i in range(2):
        s = time.time()
        future = asyncio.ensure_future(initDouban())
        loop.run_until_complete(future)
        e = time.time()
        print('----- init time %s-----\n' % i, e - s, 's')

    num = 0
    pq = PriorityQueue()
    for proxy in proxies:
        if proxy._score > 50:
            pq.put_nowait(proxy)
            num += 1
    print('原始ip數:%s' % len(all_ip), '; 篩選后:%s' % num)
    return pq


pq = firstFilter()


async def genDouban(sem, session):
    # Getter function with semaphore.
    while True:
        async with sem:
            proxy = await pq.get()
            await douban(proxy, session)
            await pq.put(proxy)


async def dynamicRunDouban(concurrency):
    '''
    TCPConnector維持鏈接池,限制并行連接的總量,當池滿了,有請求退出再加入新請求
    ClientSession調用TCPConnector構造連接,Session可以共用
    Semaphore限制同時請求構造連接的數量,Semphore充足時,總時間與timeout差不多
    '''
    conn = aiohttp.TCPConnector(verify_ssl=False,
                                limit=concurrency,
                                use_dns_cache=True)
    tasks = []
    sem = asyncio.Semaphore(concurrency)

    async with aiohttp.ClientSession(loop=loop, connector=conn) as session:
        try:
            for i in range(concurrency):
                task = asyncio.ensure_future(genDouban(sem, session))
                tasks.append(task)

            responses = asyncio.gather(*tasks)
            await responses
        except KeyboardInterrupt:
            print('-----finishing-----\n')
            for task in tasks:
                task.cancel()
            if not conn.closed:
                conn.close()


future = asyncio.ensure_future(dynamicRunDouban(200))
loop.run_until_complete(future)


scores = [p.score for p in proxies]
scores.sort(reverse=True)
print('Most popular IPs:\n ------------\n', scores[:50],
      [i for i in scores if i > 100])
loop.is_closed()
  • 訪問百度
async def baidu(proxy):
    '''
    驗證是否可以訪問百度
    '''
    async with aiohttp.ClientSession(loop=loop) as session:
        async with session.get("http://baidu.com",
                               proxy='http://' + proxy,
                               timeout=5) as resp:
            text = await resp.text()
            if 'baidu.com' not in text:
                print(proxy,
                      '\n----\nis bad for baidu.com\n')
                return False
            return True
  • 訪問icanhazip
async def testProxy(proxy):
    '''
    http://aiohttp.readthedocs.io/en/stable/client_reference.html#aiohttp.ClientSession.request
    '''
    async with aiohttp.ClientSession(loop=loop) as session:
        async with session.get("http://icanhazip.com",
                               proxy='http://' + proxy,
                               timeout=5) as resp:
            text = await resp.text()
            if len(text) > 20:
                return
            else:
                if await baidu(proxy):
                    firstFilteredProxies.append(proxy)
                    # print('原始:', proxy, '; 結果:', text)
  • 訪問HttpBin
async def httpbin(proxy):
    '''
    訪問httpbin獲取headers詳情, 注意訪問https 代理仍為http
    參考資料: https://imququ.com/post/x-forwarded-for-header-in-http.html
    http://www.cnblogs.com/wenthink/p/HTTTP_Proxy_TCP_Http_Headers_Check.html
    '''
    async with aiohttp.ClientSession(loop=loop) as session:
        async with session.get("https://httpbin.org/get?show_env=1",
                               proxy='http://' + proxy,
                               timeout=4) as resp:
            json_ = await resp.json()
            origin_ip = json_['origin']
            proxy_ip = json_['headers']['X-Forwarded-For']
            via = json_['headers'].get('Via', None)
            print('原始IP:', origin_ip,
                  '; 代理IP:', proxy_ip,
                  '---Via:', via)
            if proxy_ip != my_ip and origin_ip == proxy_ip:
                annoy_proxies.append(proxy)
  • 訪問豆瓣API
async def douban(proxy):

    async with aiohttp.ClientSession(loop=loop) as session:
        try:
            async with session.get(('https://api.douban.com/v2/movie/top250'
                                    '?count=10'),
                                   proxy='http://' + proxy,
                                   headers=headers,
                                   timeout=4) as resp:
                print(resp.status)
        except TimeoutError as te:
            print(proxy, te, 'timeoutError')
        except ClientProxyConnectionError as pce:
            print(proxy, pce, 'proxyError')
        except ClientConnectionError as ce:
            print(proxy, ce, 'connectError')
  • 循環訪問豆瓣導致暫時被封IP
headers = {'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                          'AppleWebKit/537.36 (KHTML, like Gecko) ')}
while True:
    r = requests.get('http://douban.com', headers=headers)
    print(r.status_code)
    r = requests.get('https://movie.douban.com/j/search_subjects?'
                     'type=movie&tag=%E8%B1%86%E7%93%A3%E9%AB%9',
                     headers=headers)
    print(r.status_code)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容