Python學(xué)習(xí)編寫第一個網(wǎng)絡(luò)爬蟲

本內(nèi)容為《用Python寫網(wǎng)絡(luò)爬蟲》書籍內(nèi)容,有興趣的讀者可以購買本書,本章的代碼皆可在Python3中運行。
為了抓取網(wǎng)站,我們首先需要下載包含有感興趣數(shù)據(jù)的網(wǎng)頁,該過程一般稱為爬去(crawling)。爬去一個網(wǎng)站有很多種方法,而選用那種方法更加合適,則取決于目標(biāo)網(wǎng)站的結(jié)構(gòu)。我們首先探討如何安全地下載網(wǎng)頁,然后介紹3種爬去網(wǎng)站的常見方法:

  • 爬取網(wǎng)站地圖;
  • 遍歷每個網(wǎng)頁的數(shù)據(jù)庫ID;
  • 跟蹤網(wǎng)頁鏈接。

下載網(wǎng)頁

要想爬取網(wǎng)頁,我們首先需要將其下載下來。下面的示例腳本使用Python3的urllib.request模塊下載URL。

import urllib.request
def download(url):
    return urllib.request.urlopen(url).read()
if __name__ == '__main__':
    print(download('http://www.baidu.com'))

當(dāng)傳入URL參數(shù)時,print會輸出download函數(shù)獲取的網(wǎng)址源碼。不過,這個代碼片段存在一個問題,即當(dāng)下載網(wǎng)頁時,我們可能會遇到一些無法控制的錯誤,比如請求的頁面可能不存在。此時,urllib會拋出異常,然后退出腳本。安全起見,下面在給出一個更健壯的版本,可以捕獲這些異常。

def download(url):
    """捕獲錯誤的下載函數(shù)"""
    print("Downloading:", url)
    try:
        html = urllib.request.urlopen(url).read()
    except urllib.request.URLError as e:
        print("download error:", e.reason)
        html = None
    return html

現(xiàn)在,當(dāng)出現(xiàn)下載錯誤時,該函數(shù)能夠捕獲到異常,然后返回None。

重新下載

下載時遇到的錯誤經(jīng)常是臨時性的,比如服務(wù)器過載時返回的 503 Service Unavailable 錯誤。對于此類錯誤,我們可以嘗試重新下載,因為這個服務(wù)器問題現(xiàn)在可能已解決。不過,我們不需要對所有錯誤都嘗試重新下載。如果服務(wù)器返回的是 404 Not Found錯誤,則說明該網(wǎng)頁目前并不存在,再次嘗試同樣的請求一般也不會出現(xiàn)不同的結(jié)果?;ヂ?lián)網(wǎng)工程任務(wù)組(Internet Engineering Task Force)定義了HTTP錯誤的完整列表,詳情請點擊,從該文檔中,我們可以了解4xx錯誤發(fā)生在請求存在問題時,而5xx錯誤則發(fā)生在服務(wù)端存在問題時。所以,我們只需要確保download在發(fā)生5xx錯誤時重試下載即可。下面是支持重試下載功能的新版本代碼。

def download(url, num_retries=2):
    """下載函數(shù),也會重試5xx錯誤。參數(shù)二為重試次數(shù),默認(rèn)為2次"""
    print("Downloading", url)
    try:
        html = urllib.request.urlopen(url).read()
    except urllib.request.URLError as e:    
        print("Download error:", e.reason)
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code < 600:
                #重試 5xx http錯誤
                html = download3(url, num_retries-1)
    return html

現(xiàn)在,當(dāng)download函數(shù)遇到5xx錯誤碼時,將會遞歸調(diào)用函數(shù)自身進(jìn)行重試。此外,該函數(shù)還增加了一個參數(shù),用于設(shè)定重試下載次數(shù),其默認(rèn)值為兩次。我們在這里限制網(wǎng)頁下載的嘗試次數(shù),是因為服務(wù)器錯誤可能暫時還沒有解決。想要測試該函數(shù),可以嘗試下載http://httpstat.us/500,該網(wǎng)站會始終返回500錯誤碼。

設(shè)置用戶代理

默認(rèn)情況下,urllib使用Python-urllib/3.5作為用戶代理下載網(wǎng)頁內(nèi)容,其中3.5是Python的版本號。如果能使用可辨識的用戶代理則更好,這樣可以避免我們的網(wǎng)絡(luò)爬蟲碰到一些問題。此外,也許是因為曾經(jīng)經(jīng)歷過質(zhì)量不佳的Python網(wǎng)絡(luò)爬蟲造成的服務(wù)器過載。一些網(wǎng)站還會封禁這個默認(rèn)的用戶代理。比如,在使用Python默認(rèn)用戶代理的情況下,訪問http://www.meetup.com/,目前會返回的提示是Forbidden。
因此,為了下載更加可靠,我們需要控制用戶代理的設(shè)定。下面的代碼對download函數(shù)進(jìn)行了修改,設(shè)定了一個默認(rèn)的用戶代理"wswp"(即Web Scraping with Python的首字母縮寫)。

def download4(url, user_agent='wswp', num_retries=2):
    """包括用戶代理支持的下載函數(shù)"""
    print("Downloading:", url)
    headers = {'User-agent': user_agent}
    request = urllib.request.Request(url, headers=headers)
    try:
        html = urllib.request.urlopen(request).read()
    except urllib.request.URLError as e:
        print("Download error:", e.reason)
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code < 600:
                # 重試 5xx http錯誤
                html = download4(url, user_agent, num_retries-1)
    return html

現(xiàn)在,我們擁有了一個靈活的下載函數(shù),可以在后續(xù)示例中得到復(fù)用。該函數(shù)能夠捕獲異常、重試下載并設(shè)置用戶代理。

網(wǎng)站地圖爬蟲

在第一個簡單的爬蟲中,我們將使用示例網(wǎng)站robot.txt文件中發(fā)現(xiàn)的網(wǎng)站地圖來下載所有網(wǎng)頁。為了解析網(wǎng)站地圖,我們將會使用一個簡單的正則表達(dá)式,從<loc>標(biāo)簽中提取出URL。

import re
from common import download

def crawl_sitemap(url):
    #下載sitemap文件
    sitemap = download(url).decode('utf-8')
    #抓取站點地圖鏈接
    links = re.findall('<loc>(.*?)</loc>', sitemap)
    # 下載每一個鏈接
    for link in links:
        html = download(link)
        # scrape html here
        # ...

現(xiàn)在,運行網(wǎng)站地圖爬蟲,從示例網(wǎng)站中下載所有國家頁面。

>>> crawl_sitemap('http://example.webscraping.com/sitemap.xml')
ownloading: http://example.webscraping.com/sitemap.xml
Downloading: http://example.webscraping.com/view/Afghanistan-1
Downloading: http://example.webscraping.com/view/Aland-Islands-2
Downloading: http://example.webscraping.com/view/Albania-3
Downloading: http://example.webscraping.com/view/Algeria-4
...

可以看出,上述運行結(jié)果和我們的預(yù)期一致,不過正如前文所述,我們無法依靠Sitemap文件提供每個網(wǎng)頁的鏈接。下一節(jié)中,我們將會介紹另一個簡單的爬蟲,該爬蟲不在依賴于Sitemap文件。

ID遍歷爬蟲

本節(jié)中,我們將利用網(wǎng)站結(jié)構(gòu)的弱點,更加輕松地訪問所有內(nèi)容。下面是一些示例國家的URL。

# -×- coding: utf-8 -*-
import itertools
from common import download
def iteration():
    for page in itertools.count(1):
        url = 'http://example.webscraping.com/view/-{}'.format(page)
        html = download(url)
        if html is None:
            # 嘗試下載此網(wǎng)站時收到的錯誤
            # 所以假設(shè)已達(dá)到最后一個國家ID,并可以停止下載
            break
        else:
            # 成功 - 能夠刮結(jié)果
            # ...
            pass

在這段代碼中,我們對ID進(jìn)行遍歷,直到出現(xiàn)下載錯誤時停止,我們假設(shè)此時已到達(dá)最后一個國家的頁面。不過,這種實現(xiàn)方式存在一個缺陷,那就是某些記錄可能已被刪除,數(shù)據(jù)庫ID之間并不是連續(xù)的。此時,只要訪問到某個間隔點,爬蟲就會立即退出。下面是這段代碼的改進(jìn)版本,在該版本中連續(xù)發(fā)生多次下載錯誤后才退出程序。

def iteration2():
    max_errors = 5 # 允許最大連續(xù)下載錯誤數(shù)
    num_errors = 0 # 當(dāng)前連續(xù)下載錯誤數(shù)
    for page in itertools.count(1):
        url = 'http://example.webscraping.com/view/-{}'.format(page)
        html = download(url)
        if html is None:
            # 嘗試下載此網(wǎng)頁時出錯
            num_errors += 1
            if num_errors == max_errors:
                # 達(dá)到最大錯誤數(shù)時,退出
                break
            # 所以假設(shè)已達(dá)到最后一個ID,并可以停止下載
        else:
            # 成功 - 能夠刮到結(jié)果
            # ...
            num_errors = 0

上面代碼中實現(xiàn)的爬蟲需要連續(xù)5次下載錯誤才會停止遍歷,這樣就很大程度上降低了遇到被刪除記錄時過早停止遍歷的風(fēng)險。
在爬取網(wǎng)站時,遍歷ID是一個很便捷的方法,但是和網(wǎng)站地圖爬蟲一樣,這種方法也無法保證始終可用。比如,一些網(wǎng)站會檢查頁面別名是否滿足預(yù)期,如果不是,則會返回404 Not Found 錯誤。而另一些網(wǎng)站則會使用非連續(xù)大數(shù)作為ID,或是不使用數(shù)值作為ID,此時遍歷就難以發(fā)揮作用了。例如,Amazon使用ISBN作為圖書ID,這種編碼包好至少10位數(shù)字。使用ID對Amazon的圖書進(jìn)行遍歷需要測試數(shù)十億次,因此這種方法肯定不是抓取該網(wǎng)站內(nèi)容最高效的方法。

鏈接爬取

到目前為止,我們已經(jīng)利用示例網(wǎng)站的結(jié)構(gòu)特點實現(xiàn)了兩個簡單爬蟲,用于下載所有的國家頁面。只要這兩種技術(shù)可用,就應(yīng)當(dāng)使用其進(jìn)行爬取,因為這兩種方法最小化了需要下載的網(wǎng)頁數(shù)量。不過,對于另一些網(wǎng)站,我們需要讓爬蟲表現(xiàn)的更像普通用戶,跟蹤鏈接,訪問感興趣的內(nèi)容。
通過跟蹤所有鏈接的方式,我們可以很容易地下載種鴿網(wǎng)站的頁面。但是,這種方法會下載大量我們并不需要的網(wǎng)頁。例如,我們想要從一個在線論壇中抓取用戶賬號詳情頁,那么此時我們只需要下載賬號頁,而不需要下載討論帖的頁面。本節(jié)中的鏈接爬蟲將使用正則表達(dá)式來確定需要下載那些頁面。下面時這段代碼的初始版本。

import re
from common import download
def link_crawler(seed_url, link_regex):
    """從指定的種子網(wǎng)址按照link_regex匹配的鏈接進(jìn)行抓取"""
    crawal_queue = [seed_url] # 要下載的URL隊列
    while crawal_queue:
        url = crawal_queue.pop()
        html = download(url).decode('utf-8')
        # 使用過濾器來匹配我們的正則表達(dá)式
        for link in get_links(html):
            if re.match(link_regex, link):
                # 將這個鏈接添加到爬網(wǎng)隊列
                crawal_queue.append(link)
def get_links(html):
    """從HTML返回一個鏈接列表"""
    # 從網(wǎng)頁提取所有鏈接的正則表達(dá)式
    webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
    # 來自網(wǎng)頁的所有鏈接的列表
    return webpage_regex.findall(html)

要運行這段代碼,只需要調(diào)用link_crawler函數(shù),并傳入兩個參數(shù),要爬取的網(wǎng)站URL和用于跟蹤鏈接的正則表達(dá)式。對于示例網(wǎng)站,我們想要爬取的是國家列表索引頁和國家頁面。其中,索引頁鏈接格式如下。

>>> link_crawler('http://example.webscraping.com', '/(index|view)')
Downloading: http://example.webscraping.com
Downloading: /index/1
Traceback (most recent call last):
 ...
ValueError: unknown url type: '/index/1'

可以看出,問題處在下載/index/1時,該鏈接只有網(wǎng)頁的路徑部分,而沒有協(xié)議和服務(wù)器部分,也就是說這是一個相對鏈接。由于瀏覽器知道你正在瀏覽哪個網(wǎng)頁,所以在瀏覽器瀏覽時,相對鏈接是能夠正常工作的。但是,urllib是無法獲知上下文的。為了讓urllib能夠定位網(wǎng)頁,我們需要將鏈接轉(zhuǎn)換為決定鏈接的形式,以便包含定位網(wǎng)頁的所有細(xì)節(jié)。如你所愿,Python中確實有用來實現(xiàn)這一功能的模塊,該模塊稱為urlparse。下面是link_crawler的改進(jìn)版本,使用了urlparse模塊來創(chuàng)建絕對路徑。

import urllib.parse
def link_crawler(seed_url, link_regex):
    """從指定的種子網(wǎng)址按照link_regex匹配的鏈接進(jìn)行抓取"""
    crawal_queue = [seed_url] # 要下載的URL隊列
    while crawal_queue:
        url = crawal_queue.pop()
        html = download(url).decode('utf-8')
        for link in get_links(html):
            # 檢查鏈接是否匹配預(yù)期正則表達(dá)式
            if re.match(link_regex, link):
                # 形式絕對鏈接
                link = urllib.parse.urljoin(seed_url, link)
                crawal_queue.append(link)

當(dāng)你運行這段代碼時,會發(fā)現(xiàn)雖然網(wǎng)頁下載沒有出現(xiàn)錯誤,但是同樣的地點總是會被不斷下載到。這是因為這些地點相互之間存在鏈接。比如,澳大利亞鏈接到了南極洲,而南極洲也存在到澳大利亞的鏈接,此時爬蟲就會在它們之間不斷循環(huán)下去。要想避免重復(fù)爬取相同的鏈接,我們需要記錄哪些鏈接已經(jīng)被爬取過。下面是修改后的link_crawler函數(shù),已具備存儲已發(fā)現(xiàn)URL的功能,可以避免重復(fù)下載。

def link_crawler(seed_url, link_regex):
    """從指定的種子網(wǎng)址按照link_regex匹配的鏈接進(jìn)行抓取"""
    crawal_queue = [seed_url] # 要下載的URL隊列
    seen = set(crawal_queue) # 跟蹤以前看過的URL
    while crawal_queue:
        url = crawal_queue.pop()
        html = download(url).decode('utf-8')
        for link in get_links(html):
            # 檢查鏈接是否匹配預(yù)期正則表達(dá)式
            if re.match(link_regex, link):
                # 形式絕對鏈接
                link = urllib.parse.urljoin(seed_url, link)
                # 檢查是否已經(jīng)看過該鏈接
                if link not in seen:
                    seen.add(link)
                    crawal_queue.append(link)

當(dāng)運行該腳本時,它會爬取所有地點,并且能夠如期停止。最終,我們得到了一個可用的爬蟲!

高級功能

現(xiàn)在,讓我們?yōu)殒溄优老x添加了一些功能,使其在爬取其它網(wǎng)站時更加有用。

解析robots.txt

首先,我們需要解析robots.txt文件,以避免下載禁止爬取的URL。使用Python自帶的robotparser模塊,就可以輕松完成這項工作,如下圖的代碼所示。

>>> import robotparser
>>> rp = robotparser.RobotFileParser()
>>> rp.set_url('http://example.webscraping.com/robots.txt')
>>> rp.read()
>>> url = 'http://example.webscraping.com'
>>> user_agent = 'BadCrawler'
>>> rp.can_fetch(user_agent, url)
False
>>> user_agent = 'GoodCrawler'
>>> rp.can_fetch(user_agent, url)
True

rotbotparser模塊首先加載robots.txt文件,然后通過can_fetch()函數(shù)確定指定的用戶代理是否允許訪問網(wǎng)頁。在本例中,當(dāng)用戶代理設(shè)置為‘BadCrawler’時,robotparser模塊會返回結(jié)果表明無法獲取網(wǎng)頁,這和示例網(wǎng)站robots.txt的定義一樣。
為了將該功能集成到爬蟲中,我們需要在crawl循環(huán)中添加該檢查。

while crawal_queue:
        url = crawal_queue.pop()
        # 檢查網(wǎng)址傳遞的robots.txt限制
        if rp.can_fetch(user_agent, url):
            ...
        else:
            print("Blocked by robots.txt", url)

支持代理

有時我們需要使用代理訪問某個網(wǎng)站。比如,Netflix屏蔽了美國以外的大多數(shù)國家。使用urllib.request支持代理并沒有想象中的那么容易(可以嘗試使用更友好的Python HTTP模塊 requests來實現(xiàn)該功能,點擊跳轉(zhuǎn)文檔地址。下面是使用urllib.request支持代理的代碼。

proxy = ...
opener = urllib.request.build_opener()
proxy_params = {urllib.parse.urlparse(url).scheme: proxy}
opener.add_handler(urllib.request.ProxyHandler(proxy_params))
response = opener.open(request)

下面是集成了該功能的新版本 download 函數(shù):

# -*- coding: utf-8 -*-
import urllib.request
import urllib.parse
def download5(url, user_agent='wswp',proxy=None, num_retries=2):
    """支持代理功能的下載函數(shù)"""
    print("Downloading:", url)
    headers = {'User-agent': user_agent}
    request = urllib.request.Request(url, headers=headers)
    opener = urllib.request.build_opener()
    if proxy:
        proxy_params = {urllib.parse.urlparse(url).scheme: proxy}
        opener.add_handler(urllib.request.ProxyHandler(proxy_params))
    try:
        html = opener.open(request).read()
    except urllib.request.URLError as e:
        print("Download error:", e.reason)
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code < 600:
                # 重試 5xx HTTP 錯誤
                html = download5(url, user_agent, proxy, num_retries-1)
    return html

下載限速

如果我們爬取網(wǎng)站的速度過快,就會面臨被封禁或是造成服務(wù)器過載的風(fēng)險。為了降低這些風(fēng)險,我們可以在兩次下載之間添加延時,從而對爬蟲限速。下面是實現(xiàn)了該功能的類的代碼。

class Throttle:
    def  __init__(self, delay):
        self.delay = delay
        self.domains = {}

    def wait(self, url):
        domain = url.parse.urlparse(url).netloc
        last_accessed = self.domains.get(domain)
        if self.delay > 0 and last_accessed is not None:
            sleep_secs = self.delay - (datetime.now() - last_accessed).seconds
            if sleep_secs > 0:
                time.sleep(sleep_secs)
        self.domains[domain] = datetime.now()

     def get_links(html):
        webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
        return webpage_regex.findall(html)

Throttle 類記錄了每個域名上次訪問的時間,如果當(dāng)前時間距離上次訪問時間小于指定延時,則執(zhí)行睡眠操作。我們可以在每次下載之前調(diào)用Throttle對爬蟲進(jìn)行限速。

throttles = Throttle(delay)
...
throttle.wait(url)
result = download(url, headers, proxy = proxy, num_retries=num_retries)

避免爬蟲陷阱

目前,我們的爬蟲會跟蹤所有之前沒有訪問過的鏈接。但是,一些網(wǎng)站會動態(tài)生成頁面內(nèi)容,這樣就會出現(xiàn)無限多的網(wǎng)頁。比如,網(wǎng)站有一個在線日歷功能,提供了可以訪問下個月和下一年的鏈接,那么下個月的頁面中同樣會包含訪問下個月的鏈接,這樣頁面就會無止境地鏈接下去。這種情況被稱為爬蟲陷阱。
想要避免陷入爬蟲陷阱,一個簡單的方法是記錄到達(dá)當(dāng)前網(wǎng)頁經(jīng)過了多少個鏈接,也就是深度。當(dāng)?shù)竭_(dá)最大深度時,爬蟲就不在向隊列中添加該網(wǎng)頁中的鏈接了。要實現(xiàn)這一功能,我們需要修改seen變量。該變量原先只記錄訪問過的網(wǎng)頁鏈接,現(xiàn)在修改為一個字典,增加了頁面深度的記錄。

def link_crawler(.... max_depth=2):
    max_depth = 2
    seen = {}
    ...
    depth = seen[url]
    if depth != max_depth:
            for link in links:
                if link not in seen:
                    seen[link] = depth + 1
                    crawl_queue.append(link)

現(xiàn)在有了這一功能,我們就有信心爬蟲最終一定能夠完成。如果想要禁用該功能,只需將max_depth設(shè)為一個負(fù)數(shù)即可,此時當(dāng)前深度永遠(yuǎn)不會與之相等。

最終版本

這個高級鏈接爬蟲的完整源代碼可以在下載。要測試這段代碼,我們可以將用戶代理設(shè)置為BadCrawler,也就是本章前文所述的被robots.txt屏蔽了的那個用戶代理。從下面的運行結(jié)果中可以看出,爬蟲果然被屏蔽了,代碼啟動后馬上就會結(jié)束。

>>> seed_url = 'http://example.webscraping.com/index'
>>> link_regex = '/(index|view)'
>>> link_crawler(seed_url, link_regex, user_agent='BadCrawlar')
Blocked by robots.txt: http://example.webscraping.com/

現(xiàn)在,讓我們使用默認(rèn)的用戶代理,并將最大深度設(shè)置為1,這樣只有主頁上的鏈接才會被下載。

>>> link_crawler(seed_url, link_regex, max_depth=1)

和預(yù)期一樣,爬蟲在下載完國家列表的第一頁之后就停止了。

最終鏈接爬蟲的代碼如下:

import re
import urllib.parse
import urllib.request
import urllib.robotparser
import time
from datetime import datetime
import queue


def link_crawler(seed_url, link_regex=None, delay=5, max_depth=-1, 
        max_urls=-1, headers=None, user_agent='wswp', proxy=None, num_retries=1):
    """從指定的種子網(wǎng)址按照link_regex匹配的鏈接進(jìn)行抓取"""
    crawal_queue = queue.deque([seed_url]) # 仍然需要抓取的網(wǎng)址隊列
    seen = {seed_url: 0} # 已經(jīng)看到的網(wǎng)址以及深度
    num_urls = 0 # 跟蹤已下載了多少個URL
    rp = get_robots(seed_url)
    throttle = Throttle(delay)
    headers = headers or {}
    if user_agent:
        headers['User-agent'] = user_agent

    while crawal_queue:
        url = crawal_queue.pop()
        # 檢查網(wǎng)址傳遞的robots.txt限制
        if rp.can_fetch(user_agent, url):
            throttle.wait(url)
            html = download(url, headers, proxy=proxy, num_retries=num_retries)
            links = []

            depth = seen[url]
            if depth != max_depth:
                # 仍然可以進(jìn)一步爬行
                if link_regex:
                    # 過濾符合我們的正則表達(dá)式的鏈接
                    links.extend(link for link in get_links(html) if re.match(link_regex, link))

                for link in links:
                    link = normalize(seed_url, link)
                    # 檢查是否已經(jīng)抓取這個鏈接
                    if link not in seen:
                        seen[link] = depth + 1
                        # 檢查鏈接在同一域內(nèi)
                        if same_domain(seed_url, link):
                            # 成功! 添加這個新鏈接到隊列里
                            crawal_queue.append(link)

            # 檢查是否已達(dá)到下載的最大值
            num_urls += 1
            if num_urls == max_urls:
                break
        else:
            print("Blocked by robots.txt:", url) # 鏈接已被robots.txt封鎖

class Throttle:
    """Throttle通過睡眠在請求之間下載同一個域"""
    def __init__(self, delay):
        """每個域的下載之間的延遲量"""
        self.delay = delay
        # 上次訪問域時的時間戳
        self.domains = {}

    def wait(self, url):
        domain = urllib.parse.urlparse(url).netloc
        last_accessed = self.domains.get(domain)

        if self.delay > 0 and last_accessed is not None:
            sleep_secs = self.delay - (datetime.now() - last_accessed).seconds
            if sleep_secs > 0:
                time.sleep(sleep_secs)
        self.domains[domain] = datetime.now()

def download(url, headers, proxy, num_retries, data=None):
    print("Downloading:", url)
    request = urllib.request.Request(url, data, headers)
    opener = urllib.request.build_opener()
    if proxy:
        proxy_params = {urllib.parse.urlparse(url).scheme: proxy}
        opener.add_handler(urllib.request.ProxyHandler(proxy_params))
    try:
        response = opener.open(request)
        html = response.read()
        code = response.code
    except urllib.request.URLError as e:
        print("Download error:", e.reason)
        html = ''
        if hasattr(e, 'code'):
            code = e.code
            if num_retries > 0 and 500 <= code < 600:
                # 重試 5xx HTTP 錯誤
                return download(url, headers, proxy, num_retries-1, data)
        else:
            code = None
    return html

def normalize(seed_url, link):
    """通過刪除散列和添加域來規(guī)范化此URL"""
    link, _ = urllib.parse.urldefrag(link) # 刪除散列以避免重復(fù)
    return urllib.parse.urljoin(seed_url, link)

def same_domain(url1, url2):
    """如果兩個網(wǎng)址屬于同一域,則返回True"""
    return urllib.parse.urlparse(url1).netloc == urllib.parse.urlparse(url2).netloc

def get_robots(url):
    """初始化此域的機器人解析器"""
    rp = urllib.robotparser.RobotFileParser()
    rp.set_url(urllib.parse.urljoin(url, '/robots.txt'))
    rp.read()
    return rp

def get_links(html):
    """從HTML返回一個鏈接列表"""
    # 從網(wǎng)頁提取所有鏈接的正則表達(dá)式
    webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
    html = html.decode('utf-8')
    # 來自網(wǎng)頁的所有鏈接的列表
    return webpage_regex.findall(html)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1 前言 作為一名合格的數(shù)據(jù)分析師,其完整的技術(shù)知識體系必須貫穿數(shù)據(jù)獲取、數(shù)據(jù)存儲、數(shù)據(jù)提取、數(shù)據(jù)分析、數(shù)據(jù)挖掘、...
    whenif閱讀 18,320評論 45 523
  • 書名:《用python寫網(wǎng)絡(luò)爬蟲》,通過閱讀并記錄去學(xué)習(xí),如果文章有什么錯誤的地方還希望指正本文參考了http:/...
    楓灬葉閱讀 3,009評論 2 2
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評論 19 139
  • 聲明:本文講解的實戰(zhàn)內(nèi)容,均僅用于學(xué)習(xí)交流,請勿用于任何商業(yè)用途! 一、前言 強烈建議:請在電腦的陪同下,閱讀本文...
    Bruce_Szh閱讀 13,025評論 6 28
  • 詞:董書利 今天愛讓彼此難堪 在愛與不愛里互換 誓言不見那愛已走遠(yuǎn) 雖有千頭卻如此凌亂 相守不再等待而是忍耐 那曾...
    星巢文化閱讀 252評論 0 1

友情鏈接更多精彩內(nèi)容