1、访问链接,获得链接网页源码
难点:反爬虫
可以用headers和proxy解决
2、识别网页中的链接
链接形式不统一,有绝对链接和相对链接,urljoin()
3、链接储存
分为两部分,第一部分是进行识别链接是否已经被访问,第二部分进行储存所有链接
(1): 运用crawl_queue堆栈,将未被访问的链接进栈,在crawl_queue非空时,出栈一个url,访问此url并获取新的url,以此进行循环
(2):将所有url储存到link_save字典中

测试CPU:i5-6300hq;RAM: 8GB
测试链接:https://zhuanlan.zhihu.com/p/35324806
测试结果:爬取183个url
测试时间:386.641009527s

代码:

from urllib.request import urlopen, URLError, HTTPError, Request, build_opener, ProxyHandler

from urllib.parse import urlparse,quote,urljoin

import re

from datetime import datetime

import time

 

#第一个参数是要爬取的url,第二个参数是代理,第三个参数是重试次数

def download(url, proxy=None, num_retries=2):

    print('Downloading', url)

    headers = {

        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'}

    request = Request(url, headers=headers)        #加入headers进行浏览器模拟

    opener = build_opener()

    if proxy:

        opener.add_handler(ProxyHandler({urlparse(url).scheme : proxy}))    #urlparse(url).scheme是url的scheme,即协议,如http;proxy是代理服务器IP+Port
                                #ProxyHandler({urlparse(url).scheme : proxy}) 是建一个Handler处理器对象,参数是一个字典类型,包括代理类型和代理服务器IP+Port
                                #add_handler(ProxyHandler({urlparse(url).scheme : proxy}))是使用代理

    try:

        html = opener.open(request).read().decode()                #访问、读取、解码url

    except (URLError, HTTPError, UnicodeDecodeError, UnicodeEncodeError) as e:            #出现访问url错误时,捕获括号里的错误储存到e里

        print('Download error:', e.reason)

        html = None

        if num_retries > 0:

            if hasattr(e, 'code') and 500 <= e.code < 600:        #hasattr() 函数用于判断对象是否包含对应的属性。
                                                                                                                #HTTPError实例产生后会有一个code属性,这就是是服务器发送的相关错误号。
                                                                                                                #x and y 的值只可能是x或y. x为真就是y, x为假就是x; x and y 的值只可能是x或y. x为真就是y, x为假就是x
                return download(url, num_retries - 1)            #递归

    return html

 

 

def get_link(html):

    webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)    #'<a[^>]+href=["\'](.*?)["\']'表示匹配模式

    try:

        return webpage_regex.findall(html)

    except TypeError:

        return []

 

 

# 限速

class Throttle:

    def __init__(self, delay):

        self.delay = delay

        self.domains = {}

 

    def wait(self, url):

        domain = urlparse(url).netloc                         #urlparse(url).netloc,url域名

        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)                           # time sleep() 函数推迟调用线程的运行,可通过参数secs指秒数,表示进程挂起的时间。 

        self.domains[domain] = datetime.now()

 

# 爬取链接,保存到link_save

#第一个参数是要爬取的url,第二个参数是匹配链接的正则表达式,第三个参数是爬取的深度

def link_crawler(seed_url, link_regex, max_depth=2):

    crawl_queue = [seed_url]

    link_save = {seed_url:0}

    throttle = Throttle(2)

    while crawl_queue:

        url = crawl_queue.pop()        #弹出没有访问过的网站,进行访问

        depth = link_save[url]

        throttle.wait(url)

        html = download(url)

        if depth <= max_depth:

            for link in get_link(html):

                # 相对链接使用这行代码合并绝对链接

                 #link = urljoin(seed_url, link)

                if re.match(link_regex, link) and link not in link_save.keys():        #判断爬取的link的开头是否为link_regex,并且判断爬取的link是否已经保存

                    crawl_queue.append(link)

                    link_save[link] = depth + 1

    return link_save

 

if __name__ == "__main__":

    result = link_crawler("http://www.xxxx.com/", "http://.*?")

    with open('xxxx_com.txt', 'w') as f:

        for i in result.keys():

            f.writelines(f'{i}\n')

 

 

正则表达式模式:
.    匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符
[...]    用来匹配一组字符,如[amk]将匹配对象中存在的‘a’ 'm'  'k'都匹配出来
[^...]    匹配不在[]中的字符
a|b    匹配a或b
[a-z]    匹配任何小写字母
[A-Z]    匹配任何大写字母
\d    匹配一个数字字符
["\']    匹配单双引号