计算机想要运行一个程序. 必须要单独的创建一个进程.

进程是一个资源单位…. 它不是执行单位…

进行中的程序…

进程与进程之间默认情况下是隔离开的…

除了病毒, 杀毒软件/ 破坏性的程序..

进程中, 有一个执行单位叫线程.

进程是资源单位, 线程是执行单位…

每一个进程在被创建的时候. 默认会有一个线程…

想让爬虫效率高一些….

  1. 多创建一些线程.. 让更多的线程帮你工作

  2. 多创建一些进程. 让更多个进程帮你干活, 本质上, 其实还是类似多线程的逻辑…

当单纯的只是想要提升效率. 多线程

如果多个任务之间, 相互隔离…. 多进程

一, 什么是进程, 什么是线程?

​ 进程: 运行中的程序. 每次我们执行一个程序, 咱们的操作系统对自动的为这个程序准备一些必要的资源(例如, 分配内存, 创建一个能够执行的线程. )

​ 线程: 程序内, 可以直接被CPU调度的执行过程. 是操作系统能够进行运算调度的最小单位. 它被包含在进程之中, 是进程中的实际运作单位.

​ 进程与线程之间的关系:

​ 进程是资源单位. 线程是执行单位. 就好比是一家公司. 一家公司的资源就是桌椅板凳, 电脑饮水机这些资源, 但是, 我们如果说一家公司正在运转着, 运行着. 那里面必须要有能为这家公司工作的人. 程序里面也一样, 进程就是为了程序运行而需要的各种资源. 但是程序想要运行, 就必须由线程来被CPU调度执行.

​ 我们运行的每一个程序默认都会有一个线程. 哪怕是只有helloworld级别的程序. 想要执行. 也会有一个线程产生.

​ 并发, 并行: 让程序可以多个任务同时执行

二, 多线程

​ 顾名思义, 多线程就是让程序产生多个线程一起去执行. 还拿公司举例子. 一家公司里如果只有一个员工, 工作效率肯定不会高到哪里去. 怎么提高效率? 多招点儿人就OK了.

​ 如何实现多线程, 在python中, 有两种方案实现多线程.

1. 直接用Thread创建线程

我们先看看单线程的效果

def func():

for i in range(1000):

print("func", i)




if __name__ == '__main__':

func()

for i in range(1000):

print("main", i)

再看多线程

from threading import Thread




def func():

for i in range(1000):

print("func", i)




if __name__ == '__main__':

t = Thread(target=func)

t.start()

for i in range(1000):

print("main", i)

2. 继承Thread类

from threading import Thread




class MyThread(Thread):

def run(self):

for i in range(1000):

print("func", i)




if __name__ == '__main__':

t = MyThread()

t.start()

for i in range(1000):

print("main", i)

以上两种是最基本的python创建多线程的方案. python还提供了线程池

3. 线程池

python还提供了线程池功能. 可以一次性的创建多个线程, 并且, 不需要我们程序员手动去维护. 一切都交给线程池来自动管理.

# 线程池

def fn(name):

for i in range(1000):

print(name, i)




if __name__ == '__main__':

with ThreadPoolExecutor(10) as t:

for i in range(100):

t.submit(fn, name=f"线程{i}")

如果任务有返回值怎么办?

def func(name):

time.sleep(2)

return name




def do_callback(res):

print(res.result())




if __name__ == '__main__':

with ThreadPoolExecutor(10) as t:

names = ["线程1", "线程2", "线程3"]

for name in names:

# 方案一, 添加回调

t.submit(func, name).add_done_callback(do_callback)



if __name__ == '__main__':

start = time.time()

with ThreadPoolExecutor(10) as t:

names = [5, 2, 3]

# 方案二, 直接用map进行任务分发. 最后统一返回结果

results = t.map(func, names, ) # 结果是按照你传递的顺序来执行的, 代价就是如果第一个没结束. 后面就都没结果

for r in results:

print("result", r)

print(time.time() - start)

4. 多线程在爬虫中的应用

http://www.boxofficecn.com/boxofficecn

我们抓取从1994年到2021年的电影票房.

import requests

from lxml import etree

from concurrent.futures import ThreadPoolExecutor




def get_page_source(url):

resp = requests.get(url)

resp.encoding = 'utf-8'

return resp.text




def parse_html(html):

try:

tree = etree.HTML(html)

trs = tree.xpath("//table/tbody/tr")[1:]

result = []

for tr in trs:

year = tr.xpath("./td[2]//text()")

year = year[0] if year else ""

name = tr.xpath("./td[3]//text()")

name = name[0] if name else ""

money = tr.xpath("./td[4]//text()")

money = money[0] if money else ""

d = (year, name, money)

if any(d):

result.append(d)

return result

except Exception as e:

print(e) # 调bug专用




def download_one(url, f):

page_source = get_page_source(url)

data = parse_html(page_source)

for item in data:

f.write(",".join(item))

f.write("\n")




def main():

f = open("movie.csv", mode="w", encoding='utf-8')

lst = [str(i) for i in range(1994, 2022)]

with ThreadPoolExecutor(10) as t:

# 方案一

# for year in lst:

# url = f"http://www.boxofficecn.com/boxoffice{year}"

# # download_one(url, f)

# t.submit(download_one, url, f)



# 方案二

t.map(download_one, (f"http://www.boxofficecn.com/boxoffice{year}" for year in lst), (f for i in range(len(lst))))




if __name__ == '__main__':

main()

三, 多进程

一个公司能创造的价值毕竟是有限的. 怎么办? 开分公司啊. 此所谓多进程. python实现多进程的方案和多线程几乎一样. 非常的简单

1. 直接用Process创建进程

def func():

for i in range(1000):

print("func", i)




if __name__ == '__main__':

p = Process(target=func)

p.start()



for i in range(1000):

print("main", i)

2. 继承Process类

class MyProcess(Process):

def run(self):

for i in range(1000):

print("MyProcess", i)




if __name__ == '__main__':

t = MyProcess()

t.start()

for i in range(1000):

print("main", i)

3.多进程在爬虫中的应用

​ 我们一般很少直接使用多进程. 最适合使用多进程的情况是: 多个任务需要一起执行. 并且互相之间数据可能有交汇但功能相对独立.比如, 我们自己做一个代理IP池, 就需要从网络上进行抓取, 抓取得到的IP要进行校验才可以进行使用. 此时, 抓取任务和校验任务就相当于完全独立的两个功能. 此时就可以启动多个进程来实现. 再比如, 如果遇到图片抓取的时候, 我们知道图片在一般都在网页的img标签中src属性存放的是图片的下载地址. 此时我们可以采用多进程的方案来实现, 一个负责疯狂扫图片下载地址. 另一个进程只负责下载图片.

​ 综上, 多个任务需要并行执行, 但是任务之间相对独立(不一定完全独立). 可以考虑用多进程.

# 进程1. 从图片网站中提取到图片的下载路径

def get_pic_src(q):

print("start main page spider")

url = "http://www.591mm.com/mntt/"

resp = requests.get(url)

tree = etree.HTML(resp.text)

child_hrefs = tree.xpath("//div[@class='MeinvTuPianBox']/ul/li/a/@href")

print("get hrefs from main page", child_hrefs)

for href in child_hrefs:

href = parse.urljoin(url, href)

print("handle href", href)

resp_child = requests.get(href)

tree = etree.HTML(resp_child.text)

pic_src = tree.xpath("//div[@id='picBody']//img/@src")[0]

print(f"put {pic_src} to the queue")

q.put(pic_src)

# 考虑一下, 图片详情页还有分页呢? 分页图片怎么抓?

# print("ready to another!")

# others = tree.xpath('//ul[@class="articleV2Page"]')

# if others:




# 进程2. 从图片网站中提取到图片的下载路径

def download(url):

print("start download", url)

name = url.split("/")[-1]

resp = requests.get(url)

with open(name, mode="wb") as f:

f.write(resp.content)

resp.close()

print("downloaded", url)




def start_download(q):

with ThreadPoolExecutor(20) as t:

while True:

t.submit(download, q.get()) # 启动



def main():

q = Queue()

p1 = Process(target=start_download, args=(q,))

p2 = Process(target=get_pic_src, args=(q,))

p1.start()

p2.start()




if __name__ == '__main__':

main()