在写爬虫时,多线程或多进程有时候是必要的,我们爬取的数据量有时过于庞大,使用单一线程可能会使我们的时间成本显著增加。

线程与进程

  • 何谓进程

“进程”在计算机科学中是指正在执行的程序的实例。它是程序代码与程序运行时所需的资源(如内存、CPU 时间等)结合的产物。

进程是资源单位。

  • 何谓线程

“线程”是操作系统中执行程序的基本单元,是比进程更小的执行单位。一个进程可以包含多个线程,这些线程共享进程的资源(如内存空间、文件句柄等),但每个线程有自己的栈空间和程序计数器。

线程是执行单位。

如果把进程比作公司,那么线程就是公司里的员工,一个公司里可以有多个员工,也必须至少有一个员工。

一般而言,我们更倾向于用多线程去解决任务。

案例

下面是一些有关多线程,多进程,线程池的程序代码以及一些 Python 的细节说明。

案例 1

1
2
3
4
5
6
7
8
9
10
11
from threading import Thread

def func():
for i in range(1000):
print("func", i);

if __name__ == '__main__':
t = Thread(target=func); # 创建一个线程并安排任务。
t.start(); # 多线程状态为可以开始工作,具体执行执行时间由 CPU 决定。
for i in range(1000):
print("main", i);
  • Thread 是一个线程对象,利用 target 参数可以把我们需要进行的多线程任务 (在案例中 func)传入,通过 start() 方法,来使该线程的执行状态为可以开始,至于具体什么时候开始,则与 CPU 有关。
  • __name__ 是一个 Python 自带的参数,它的功能是用于区分程序是直接被编译还是在 import 中被调用,如果是直接被编译,那么 __name__ 的值就是 ‘__main’,如果是被 import,那么它的值就是 import 后面的字符串。

案例 2

第二种实现多线程的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
class MyThread(Thread):
def run(self):# 固定的。当线程可以开始之后,默认执行的就是 run 方法。
for i in range(10):
print("zixiancheng",i);


if __name__=='__main__':
t = MyThread();
t.start();#开始线程。
# t.join();
for i in range(10):
print("zhuxiancheng",i);

这段代码工作的原理是:在默认情况下,Thread 类的实例在 start 时会运行 run() 方法,上述代码通过继承类 Thread 并重定义 run() 方法,实现了目标结果。

案例 3

1
2
3
4
5
6
7
8
9
10
11
12
from multiprocessing import Process
from threading import Thread

def func(name):
for i in range(1000):
print(name, i);

if __name__=='__main__':
p = Process(target=func);
p.start();
for i in range(1000):
print("zhuxincheng", i);

可以看到多进程和多线程的代码非常类似,只是换了模块而已。这要感谢 Python 提供的如此良好的 API。

案例 4

线程池的需求是必要的。试想如果我们有 1000 个网页要爬取,难道要开 1000 个线程吗?还是只开 50 个线程来回使用?显然后者是更优的一种做法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def fn(name):
for i in range(1000):
print(name, i);

if __name__ == '__main__':
# 创建线程池。
with ThreadPoolExecutor(50) as t:
# 开辟了一个 50 个线程组成的线程池.
for i in range(100):
t.submit(fn, name=f"线程{i}");
# 等待线程池中的任务全部执行完毕,才继续执行(守护)。
print(123);

这里 Python 自带的 concurrent.futures 模块里自带了线程池和进程池。本代码使用线程池做的演示,进程池完全类似。

注解 1:上面的所有示例代码中多线程的任务有些是不带参数,有些只带了 1 个参数。事实上,对于 Thread 实例来说,如果采取第一种实现方式,则可以通过 Thread(target=func, args=("t1",)); 这样的方式进行传参,注意到 args 后面的 , 是必须的,表示 args 是一个元组。而对于线程池来说,就可以像案例 4 的代码一样,在 submit() 方法中直接传入参数即可,也可以按照顺序排列,不用名言是哪个参数。

最后是一个线程池与爬虫结合起来的部分代码

案例 5

1
2
3
4
5
6
7
8
9
10
11
12
13
def download_one_page(url):
# 拿到页面源代码
resp = requests.get(url);
print(resp.text);
html = etree.HTML(resp.text);
table = html.xpath("");
trs = table.xpath("tr[position()>1]");
# 拿到每个 tr
for tr in trs:
txt = tr.xpath("./td/text()");
txt = (item.replace("\\","").replace("/","") for item in txt);
print(list(txt));
resp.close();
  • 要点 1:xpath 路径中 position()>1 表示的是所有顺位比 1 大的,在代码中就代表 tr[2], tr[3], tr[4]... 等等。
  • 要点 2:(item.replace("\\","").replace("/","") for item in txt) 代表的是数据生成器(可以粗略的理解为迭代器),而非元组。可以用 for 循环或者 next 进行遍历。