实战:爬取简书之多线程爬取(一)

在上上篇我们编写了一个简单的程序框架来爬取简书的文章信息,10分钟左右爬取了 1万 5千条数据。

现在,让我们先来做一个简单的算术题:

假设简书有活跃用户一千万人(不知道简书有多少活跃用户,我只能往小了算)

平均每人写了 15篇文章,那么一共有一亿五千万篇文章

我们10分钟爬取了 1万 5千篇,凑个整算 2万

那么爬取一亿五千万条数据需要

150000000 / 20000 = 10 * 7500 = 75000 min = 1250 h = 52 d

w(゚Д゚)w 52天!!!,如果按照前面的脚本来爬要爬整整 52天,那时候黄花菜都凉了呀。

这些数据的时间跨度如此大,如果要做数据分析的进行对比的话就会产生较大的误差。

所以,我们必须得提高爬取速度!!!

这时候就轮到今天得主角登场了,

噔 噔 噔 蹬------》多线程

一、多线程简介

简单来讲,多线程就相当于你原来开一个窗口爬取,现在开了10个窗口来爬取。

不计较数据的重复的话,现在的速度应该是之前的10倍,也就是说原来要52天才能爬完的数据现在只要5.2天了。

不过多线程和上面的例子还是有一些区别的

多线程是在一个窗口里同时运行十个线程,而上面的例子是同时打开十个窗口。

如果将数据比作货物的话,原来一个线程就相当于一个人在搬,十个线程就相当于十个人在搬

二、多线程的简单使用

threading是 python的标准库,可以直接导入使用

Thread类是 threading库的重点,我们使用要使用多线程都要通过这个类来使用

Thread一共有两种使用方法,第一种是直接传一个回调函数给 Thread类,这个回调函数可以有参数,但必须返回 None也就是不能有返回值。

第二种方法是继承 Thread类,然后重载 Thread类的 __init__()run()方法,第二种方法可以在实例初始化的时候向 run方法传递参数。

这两种方式可以互换,但是推荐使用第二种方法,因为这样更利于代码的组织,代码的可读性也更强。

下面我们就用代码来演示一下,Thread类的使用方法:
第一种:

Thread的原型是:

threading.Thread(group=None, target=None, name=None, args=(), kwargs={})

这里 group必须是 None,将回调函数传给 target,name是线程的名字默认是 ‘Thread-N’ N是一个数字。

args是要传递给回调函数的位置参数,kwargs是传递给回调函数的关键词参数。

第一种使用方法如下:

先定义一个函数,然后将函数和它所需的参数作为 Thread类的初始化参数得到一个 Thread实例,这个实例就是一个未开始的线程。

要启动这个线程,只需调用 start() 方法,然后调用 join()方法阻塞主线程。

为什么要调用 join()方法呢?

因为我们实例化的线程和主线程(也就是我们代码所在的线程)是分开的,那就有一个完成先后的问题。

如果我们实例化的线程先完成,那问题不大,但是要是主线程先完成了,那么正在运行的其他子线程会全部强行被停止

所以调用 join()方法阻塞主线程来保证所有的子线程全部完成,下面看代码示例:

在这个示例中我们用十个线程来访问我的首页 100次,并用 time库测试所用时间。

#-*- coding: utf-8 -*
import threading
import requests
import time


# 定义回调函数
def getPage(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36'
    }
    for i in range(10):
        r = requests.get(url, headers=headers)
        print(r)



url = '//www.greatytc.com/u/472a595d244c'

# 用来存放线程的列表
threads = []

# 记录开始时间
start = time.time()

# 添加十个线程到线程列表中
for i in range(10):
    # 向线程列表传递参数,通过 kwargs向回调函数传递参数
    t = threading.Thread(target=getPage, name=f'Thread {i}', kwargs={'url':url})
    # 通过 args向回调函数传递参数, 注意 url后的逗号,
    # args必须是一个元组
    # t = threading.Thread(target=getPage, name=f'Thread {i}', args=(url,))
    threads.append(t)

# 开启所有线程
for t in threads:
    t.start()

# 阻塞主线程,直到所有线程全部完成
for t in threads:
    t.join()

# 记录结束时间
end = time.time()

print(f'多线程共用时 {end - start} 秒')

十个线程访问一百次大概是 8.13秒

用普通的方法访问一百次的话,大概需要 60秒

前者平均每次访问耗时 0.08秒,而后者平均每次访问耗时 0.6秒,多线程差不多是普通方法的 8倍

第二种:

在使用之前我们得先定义一个 Thread类的子类:

# 定义一个子类并重载 __init__()方法和 run()方法
class testThread(threading.Thread):
    def __init__(self, thread_name):
        # 初始化时调用基类的初始化函数 初始化基类
        threading.Thread.__init__(self)
        # 将参数赋值作为 self的属性 这样就可以将参数通过 self传递给 run方法
        self.thread_name = thread_name


    # 要在多线程里运行的函数
    def run(self):
        url = '//www.greatytc.com/u/472a595d244c'
        headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36'
        }
        for i in range(10):
            r = requests.get(url, headers=headers)
            print(r, ' '+self.thread_name)

后面的使用方法就和第一种方法差不多了,只是传递的参数发生了一点变化:

# 主页链接
url = '//www.greatytc.com/u/472a595d244c'

# 用来存放线程的列表
threads = []

# 记录开始时间
start = time.time()

# 添加十个线程到线程列表中
for i in range(10):
    # 像使用普通类一样
    t = testThread(url)
    threads.append(t)

# 开启所有线程
for t in threads:
    t.start()

# 阻塞主线程,直到所有线程全部完成
for t in threads:
    t.join()

# 记录结束时间
end = time.time()

print(f'多线程共用时 {end - start} 秒')

这两种方法运行时间没有太大差别,但是第二种方可读性更强,所以推荐大家尽量使用第二种方法

三、使用多线程需要注意的问题

凡事都有两面性,虽然使用多线程速度更快,但是多线程也会带来一些问题,我们来看下面这个例子:

定义一个函数,这个函数会在控制台中打印 Hello World一次

现在我们用十个进程来同时执行它,看看输出的结果:

#-*- coding: utf-8 -*
import threading


def print_hello_world():
    print('Hello World')


threads = []
for i in range(10):
    t = threading.Thread(target=print_hello_world)
    threads.append(t)

for t in threads:
    t.start()

for t in threads:
    t.join()

现在 pycharm里运行一遍,和正常情况一样,每一句 hello world单独占一行,没什么问题。

但是,现在我们用 idel再运行一遍这段代码,你会发现:

原本应该分成十行输出的 hello world现在变成了一行

更加糟糕的是 hello world竟然变成了 world hello (#°Д°)

为什么会这样呢?

这是因为 pycharm的控制台是线程安全的,而 idel则没有做线程保护

所以当多个线程同时访问 idel的控制台时,就会出现争抢的现象

比如前一个线程刚打印完 hello,这时后面的线程就根本不管前面地线程还没打印完,直接上去就开始打印

这时就会出现单词顺序不对的问题,就像上面的例子一样

那什么时候会出现这样的问题呢?

其实问题的根源是多个线程同时访问了同一个对象,造成争抢,然后就会出问题

比如多个线程同时操作一个文件、变量、列表等等

为了防止这样的问题产生,我们会给每一个可能被多个线程访问的资源加一个资源锁,这样同时就只能有一个线程访问了。

不过这样速度就会变慢一些,而且整体速度与资源锁的数量成反比。

所以要适当的使用资源锁,不要滥用,不然速度会变得很慢。

这里的资源锁指的就是 threading.Lock() ,它的一个实例就是一个资源锁对象

使用方法如下:

lock.acquire()
print('Hello World')
lock.release()

lock.acquire()申请上锁,lock.release() 解锁,在两者之间的代码就是线程安全的

现在,我们已经能够简单地使用多线程了

那么,下一篇我们就把 v1.0 版的简书爬虫升级到 v2.0版的多线程版简书爬虫。

最后,觉得不错的话,记得关注、点赞、评论哦(❤ ω ❤)

上一篇:实战:爬取简书之搭建程序框架
下一篇:实战:简书爬取之多线程爬取(二)速度提升何止10倍!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容