分享

python学习(二)爬虫

 二楼202 2019-10-17

python上一篇写了怎样获取整本小说并写入文件,但是速度实在太慢,这一篇我们使用多线程来提高爬取速度

一:多线程模块 threading

  1. 创建线程

    创建线程有两种方法
    第一种是继承threading.Thread类,并重写它的init和run函数
    代码如下:

import threading

class gettext(threading.Thread):
    def __init__(self,threadName):
        #调用父类的构造函数
        threading.Thread.__init__(self)
        self.threadName = threadName

    def run(self):
        print(self.threadName)

这里的self是一个指向当前对象的指针,用它来进行初始化和调用成员变量

将类定义好后,就可以创建线程了,代码如下:

#保存所有线程的列表
threads = []

#创建5个线程
for i in range(5):
    #创建一个线程
    thread = gettext('Thread-'+str(i))
    #将创建好的线程添加到线程列表
    threads.append(thread)
    #启动线程
    thread.start()

#等待所有线程结束
for t in threads:
    t.join()

print('线程结束')

运行结果:
示例1


这和预期的输出有点不同:
示例2


这是由于线程不同步造成的,在程序中输出流只有一个,但是线程却有多个,而且线程又是同时运行的,所以就会出现多个线程同时访问同一个资源的情况。
所以在使用多线程的时候,我们要对共有资源的访问加以限制,使得同时只能有一个线程访问。就像一把锁一样,在有线程访问资源的时候把资源锁起来不让其他线程访问,等到当前线程结束对共有资源的操作后再把锁打开,让其他线程可以访问。

如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。

使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release
方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。如下:

多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。

考虑这样一种情况:一个列表里所有元素都是0,线程”set”从后向前把所有元素改成1,而线程”print”负责从前往后读取列表并打印。

那么,可能线程”set”开始改的时候,线程”print”便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。

锁有两种状态——锁定和未锁定。每当一个线程比如”set”要访问共享数据时,必须先获得锁定;如果已经有别的线程比如”print”获得锁定了,那么就让线程”set”暂停,也就是同步阻塞;等到线程”print”访问完毕,释放锁以后,再让线程”set”继续。

经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。

这里的锁就是threading的Lock方法,代码如下:

import threading

class gettext(threading.Thread):
    def __init__(self,threadName,lock):
        threading.Thread.__init__(self)
        self.threadName = threadName
        self.lock = lock
    def run(self):
        #把共有资源锁起来
        self.lock.acquire()
        print(self.threadName)
        #操作完成后打开锁
        self.lock.release()


#保存所有线程的列表
threads = []

#创建锁
lock = threading.RLock()

#创建5个线程
for i in range(5):
    #创建一个线程
    thread = gettext('Thread-'+str(i),lock)
    #将创建好的线程添加到线程列表
    threads.append(thread)
    #启动线程
    thread.start()

#等待所有线程结束
for t in threads:
    t.join()

print('线程结束')

这样输出就和预期的一样了

第二种创建线程的方法称为函数式,
这个非常简单,详情请查看:菜鸟教程 | | python3 多线程

接下来开始敲代码

二:多线程获取单本小说

首先我们要将每个章节进行编号(因为多线程并不会按照列表的顺序来获取章节,所以我们将每个章节进行编号,写入文件的时候就不会乱序了)

将获取章节列表部分的代码修改为:

'''***************************获取章节列表**********************************'''
chapter_list = []
i = 1
while 1:
    #获取网页
    #更改编码
    #获得BeautifulSoup对象
    #获取章节列表
    #url + str(i)第i页的url
    r = requests.get(url + str(i),params=re_header)
    r.encoding = 'gbk'
    soup = BeautifulSoup(r.text,"html.parser")

    i+=1
    print(url + str(i))

    temp_list = soup.select('.last9 a')
    for t in range(len(temp_list)):
        temp_list[t] = temp_list[t]['href']
    del temp_list[0]
    if(len(temp_list)==0):
        break
    chapter_list.extend(temp_list)

for i in range(len(chapter_list)):
    chapter_list[i] = 'http://m.' + chapter_list[i]
    #对每个章节编号
    chapter_list[i] = [i,chapter_list[i]]
    print(chapter_list[i])

后面的代码也要进行相应的修改,现在章节链接是chapter_list[ i ][1]

线程类的代码,这里本来需要用queue队列的,但是那样会使得程序运行变慢,所以我直接用了列表的pop方法,先把chapter_list列表用reverse方法反向,然后再用pop方法逐个取出,效果和queue队列一样,还省去了put的过程,代码如下:

线程类和储存类的定义:

class gettext(threading.Thread):
    def __init__(self,chapter_list,book,lock,folock,re_header):
        threading.Thread.__init__(self)
        self.chapter_list = chapter_list
        self.book = book
        self.lock = lock
        self.folock = folock
        self.re_header = re_header
        self.exitflag = False

    def run(self):
        while not self.exitflag:
            #把共有资源锁起来
            self.lock.acquire()
            if len(self.chapter_list) != 0:
                data = self.chapter_list.pop()
                #获得链接后打开锁
                self.lock.release()

                #获取第一页页面
                r = requests.get(data[1],params=re_header)
                r.encoding = 'gbk'
                soup = BeautifulSoup(r.text,"html.parser")

                #这个网站把每个章节分为两页,要分两次获取
                #获取章节名和第一页的内容
                title = soup.select('.nr_title')[0].text
                content_1 = soup.select('#nr1')[0].text

                #获取第二页页面
                r = requests.get(data[1].replace('.html','_2.html'),params=re_header)
                r.encoding = 'gbk'
                soup = BeautifulSoup(r.text,"html.parser")

                #第二部分章节内容
                content_2 = soup.select('#nr1')[0].text

                #拼接两部分内容 详细请搜索字符串join方法
                str1 = ''
                chapter_content = str1.join([content_1,content_2])

                self.lock.acquire()
                #将章节内容放进储存对象中
                self.book.put(data[0],title,chapter_content)
                print(title)
                self.lock.release()

            else:
                #chapter_list长度为0时退出线程
                self.exitflag = True
                self.lock.release()

#定义一个storage类用来暂时储存小说内容
class storage:
    def __init__(self):
        self.content = []

    def put(self,index,title,content):
        self.content.extend([[index,title,content]])

线程的创建和启动:

#反向列表
chapter_list.reverse()

'''*************************************线程*******************************'''
#保存小说内容的对象
book = storage()
#保存所有线程的列表
threads = []

#创建锁
lock = threading.RLock()
folock = threading.RLock()

#创建10个线程
for i in range(10):
    #创建一个线程
    thread = gettext(chapter_list,book,lock,folock,re_header)
    #将创建好的线程添加到线程列表
    threads.append(thread)
    #启动线程
    thread.start()

#等待所有线程结束
for t in threads:
    t.join()

print('线程结束')

当线程全部结束时,小说的内容就保存在book对象中了,不过book对象中的章节全是乱序的,所以我们要根据每个章节的编号对book内的内容重新进行一次排序,这里我们用到的是桶排序:

#定义一个桶数组
novel = [['a','a']]*len(book.content)

for t in book.content:
    index = t[0]

    #因为python中列表是按引用传递的,所以这里我们传递的只是地址
    novel[index][0] = t

排序好后写入文件

#打开/创建文件
fo = open('1.txt','wb')

for t in novel:
    title = t[1]
    chapter_content = t[2]

    #写入章节名和内容
    fo.write((title).encode('utf-8'))
    fo.write((chapter_content).encode('utf-8'))

    #打印提示
    print(title + '已下载')


#关闭文件
fo.close()

print('下载成功')

因为网站服务器的缘故,访问的速度太快的话,有少量的请求会失败,所以我们还要在获取请求的时候对请求失败的情况做出处理。这里用到状态响应码r.status_code,当成功访问时状态响应码为200。这里只修改了gettext类的部分代码:

class gettext(threading.Thread):
    def __init__(self,chapter_list,book,lock,folock,re_header):
        threading.Thread.__init__(self)
        self.chapter_list = chapter_list
        self.book = book
        self.lock = lock
        self.folock = folock
        self.re_header = re_header
        self.exitflag = False

    def run(self):
        while not self.exitflag:
            #把共有资源锁起来
            self.lock.acquire()
            if len(self.chapter_list) != 0:
                data = self.chapter_list.pop()
                #获得链接后打开锁
                self.lock.release()

                #获取第一页页面
                while 1:
                    r = requests.get(data[1],params=re_header)
                    #成功获得页面跳出循环,否则继续
                    if r.status_code == 200:
                        break
                r.encoding = 'gbk'
                soup = BeautifulSoup(r.text,"html.parser")

                #这个网站把每个章节分为两页,要分两次获取
                #获取章节名和第一页的内容
                title = soup.select('.nr_title')[0].text
                content_1 = soup.select('#nr1')[0].text

                time.sleep(0.1)
                #获取第二页页面
                while 1:
                    r = requests.get(data[1].replace('.html','_2.html'),params=re_header)
                    #成功获得页面跳出循环,否则继续
                    if r.status_code == 200:
                        break
                r.encoding = 'gbk'
                soup = BeautifulSoup(r.text,"html.parser")

                #第二部分章节内容
                content_2 = soup.select('#nr1')[0].text

                #拼接两部分内容 详细请搜索字符串join方法
                str1 = ''
                chapter_content = str1.join([content_1,content_2])

                self.folock.acquire()
                #将章节内容放进储存对象中
                self.book.put(data[0],title,chapter_content)
                print(title)
                self.folock.release()

            else:
                #chapter_list长度为0时退出线程
                self.exitflag = True
                self.lock.release()

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多