本课程共五个章节,课程地址:
【Python爬虫教程】花9888买的Python爬虫全套教程2021完整版现分享给大家!(已更新项目)——附赠课程与资料_哔哩哔哩_bilibili
第四章
- 本章内容梗概
- 多线程
- 多进程
- 线程池和进程池
- 抓取广州江南果菜批发市场菜价
- 协程
- aiohttp模块 多任务异步协程
- 扒光一本电子书
- 综合训练:抓取一部电影
目录
(一)本章内容梗概
到目前为止,我们可以解决爬虫的基本抓取流程了,但是抓取效率还是不够高。如何提高抓取效率呢?我们可以选择多线程、多进程、协程等操作完成异步爬虫(多车道同时进行爬取)
在这里要特殊说明一下,多线程异步爬虫中每一步都可以设立成多线程的,具体操作得实际去分析。当然,也可以像上图这样,每一个url一个单独线程
想抓取一万条数据, 每一次发送请求的时候都要经历这几个过程:
- 请求到url
- 得到响应
- 从响应中提取内容
- 存储到数据库或本地
一万条数据就要执行一万次请求,线性过程
异步操作(如一次性跑四个),有很多方法可以实现异步操作:
- 多线程
- 多进程
- 协程
(二)多线程
操作系统每次运行一个程序的时候,都会给这个程序准备一块内存,用来存放这个程序执行过程中产生的变量等。这个内存区域可以叫进程(资源单位)。在这个进程里面会有若干个线程(执行单位)进行工作(每一个进程至少要有一个线程,CPU执行时是找线程)
# 启动每一个程序默认都会有一个主线程
if __name__ == '__main__':
print("你好啊")
快速敲出下面这条语句的方法:输入main,然后敲回车
if __name__ == '__main__':
先看单线程效果
# 单线程程序,线性过程,func()执行完了才开始执行main
def func():
for i in range(1000):
print("func", i)
if __name__ == '__main__':
func()
for i in range(1000):
print("main",i)
执行过程:程序启动 ——> 加载func() ——> 执行main ——> 调用func() ——> func执行完毕,继续执行main中的内容 整个过程是一条线跑下来的,这就是单线程
误区:单线程是一条线跑下来的,那如果写个if是不是就是两条线了? 非也
我们要注意一个细节,不论程序真还是假,它只能选择一条路走,所以还是单线程,并没有异步的效果
那么在python里如何使用多线程呢?
借助 Thread类 来完成
第一套写法:
# 多线程程序(写法1,适用于小脚本)
from threading import Thread # 线程类
def func():
for i in range(1000):
print("func", i)
if __name__ == '__main__':
# 创建一个线程类的对象,并给线程安排任务
t = Thread(target=func) # func后面不能加(),因为加了()就是调用它
# 多线程状态为可以开始工作状态, 具体的执行时间由CPU决定
t.start()
for i in range(1000):
print("main",i)
第二套写法:
# 多线程程序(写法2,适用于复杂的情况)
from threading import Thread # 线程类
class MyThread(Thread): # 写一个类MyThread,继承Thread类
# 重写run()方法
def run(self): # 固定的 -> 当线程被执行的时候, 被执行的就是run()
for i in range(1000):
print("子线程", i)
if __name__ == '__main__':
t = MyThread() # 造对象
# t.run() # 这么写不行,是方法的调用 -> 单线程
t.start() # 开启线程
for i in range(1000):
print("主线程", i)
程序效果:main和func交替执行(如果速度够快,给我们的感觉就是一起执行)
执行过程:加载fun() ——> 执行main ——> 创建子线程t ——> 子线程t启动 ——> 执行func中的内容 |——> 继续执行main
我们成功的让两件事同时发生了,那么想一下,如果我有1000个url准备去下载,那么交给每个func单独去执行就好,主函数该干嘛还干嘛
如何创建两个子线程且能区分?(传参)
from threading import Thread
def func(name): # 函数传参
for i in range(1000):
print(name, i)
if __name__ == '__main__':
t1 = Thread(target=func, args=("周杰伦",)) # 传递参数必须是元组,所以如果只有一个参数后面必须要有,
t1.start()
t2 = Thread(target=func, args=("王力宏",))
t2.start()
对于写法2,即创建类的形式,可以通过定义构造函数的方式往里面传参
class MyThread(Thread): # 写一个类MyThread,继承Thread类 def __init__(self): # 定义构造函数
(三)多进程
多进程:通过主程序去创建多个进程来完成并行的效果
相对于多线程而言,多进程更耗资源(要开内存),所以不常用
python里怎么创建多进程?
(跟创建多线程代码的逻辑是一样的)
注意,python的作者其实做了一件大好事:本质上多线程和多进程的执行过程是不一样的,python的作者为了让开学人员更舒服,采用了几乎完全相同的API,所以写起来几乎一样
第一套写法:
from multiprocessing import Process
def func():
for i in range(100000):
print("子进程",i)
if __name__ == '__main__':
p = Process(target=func) # 创建一个子进程
p.start()
for i in range(100000):
print("主进程",i)
第二套写法:
from multiprocessing import 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)
(四)线程池和进程池入门
广州江南果菜批发市场 批发市场 最大的水果批发市场 蔬菜批发市场 江南市场
当我们对某些网站内容进行抓取的时候非常容易遇到这样一种情况:看这个网站,我们发现这网站的数据太多了,有一万多页,也就对应着一万多个url,那我们设计多线程的时候,如果每个url对应一个线程就会产生新问题 —— 创建线程本身也是要消耗计算机资源的,那这时我们就可以考虑能不能重复的使用线程呢?答案当然可以,线程池来搞定
若想爬取所有的数据,且想提高效率,可以把每一个url都放在一个线程里,来完成数据的爬取。但是,对应要使用一万多个线程,此时CPU的资源完全被浪费掉,因为开辟一个线程也是要耗资源的。有没有什么机制能够创建50个线程并反复利用这50个线程来爬取这一万多条的数据?
线程池工作原理:创建一个大池子,存放固定数量的线程/一次性开辟一些线程,用户直接给线程池提交任务,线程任务的调度交给线程池来完成
在python里如何使用线程池?
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# ThreadPoolExecutor 线程池 ProcessPoolExecutor 进程池
def fn(name):
for i in range(1000):
print(name, i)
if __name__ == '__main__':
# 创建一个有50个线程的线程池
with ThreadPoolExecutor(50) as t:
for i in range(100): # 有100个任务
t.submit(fn, name=f"线程{i}") # f是format 格式化字符串
# 等待线程池中的任务全部执行完毕. 才继续执行 ——> 守护
print("123")
进程池
进程池同理,把 ThreadPoolExecutor 更换为 ProcessPoolExecutor 就可以了
(五)抓取广州江南果菜批发市场菜价
思路:
- 如何提取单个页面的数据(xpath)
- 上线程池,多个页面同时抓取
右键 ——> 检查
# xpath的延伸用法
# 场景:table里连同表头和数据都是在<tr>里,现只想要数据,不想要表头
# 两种写法
trs = table.xpath("./tr")[1:] # 写法1
trs = table.xpath("./tr[position()>1]") # 写法2
第一步:提取单个页面的数据
import requests
from lxml import etree
def download_one_page(url):
# 拿到页面源代码
resp = requests.get(url)
html = etree.HTML(resp.text)
# print(resp.text)
# 从页面源代码里提取我们想要的内容
table = html.xpath("/html/body/div/div[4]/div/div[2]/div[2]/table")[0]
# print(table)
trs = table.xpath("./tbody/tr") # 相对查找 剔除表头
# print(len(trs))
for tr in trs: # 拿到每个tr
txt = tr.xpath("./td/text()")
print(txt)
if __name__ == '__main__':
download_one_page("http://www.jnmarket.net/import/list-1_1.html")
我们想去掉如 “江苏/河南/山东/云南” 中的 “/”,对代码稍作修改:
# 对数据做简单的处理:去掉/
txt = (item.replace("/","") for item in txt) # 生成器
print(list(txt))
将数据写进csv文件:
import csv
f = open("data_new.csv",mode="w",newline="",encoding="utf-8")
csvwriter = csv.writer(f)
# 把数据存放在文件中
csvwriter.writerow(txt)
print(url,"提取完毕!")
提取单页面数据的完整代码:
import requests
from lxml import etree
import csv
f = open("data_new.csv",mode="w",newline="",encoding="utf-8")
csvwriter = csv.writer(f)
def download_one_page(url):
# 拿到页面源代码
resp = requests.get(url)
html = etree.HTML(resp.text)
# print(resp.text)
# 从页面源代码里提取我们想要的内容
table = html.xpath("/html/body/div/div[4]/div/div[2]/div[2]/table")[0]
# print(table)
trs = table.xpath("./tbody/tr") # 相对查找 剔除表头
# print(len(trs))
for tr in trs: # 拿到每个tr
txt = tr.xpath("./td/text()")
# 对数据做简单的处理:去掉/
txt = (item.replace("/","") for item in txt) # 生成器
# print(list(txt))
# 把数据存放在文件中
csvwriter.writerow(txt)
print(url,"提取完毕!")
if __name__ == '__main__':
download_one_page("http://www.jnmarket.net/import/list-1_1.html")
第二步:上线程池,多页面同时抓取
from concurrent.futures import ThreadPoolExecutor
if __name__ == '__main__':
# for i in range(1,10548): # 效率极其低下
# download_one_page(f"http://www.jnmarket.net/import/list-1_{i}.html")
# 创建线程池
with ThreadPoolExecutor(50) as t: # 准备一个50个线程的线程池
for i in range(1,10548):
# 把下载任务提交给线程池
t.submit(download_one_page, f"http://www.jnmarket.net/import/list-1_{i}.html")
print("全部下载完毕!")
全部代码:
import requests
from lxml import etree
import csv
from concurrent.futures import ThreadPoolExecutor
f = open("data_new.csv",mode="w",newline="",encoding="utf-8")
csvwriter = csv.writer(f)
def download_one_page(url):
# 拿到页面源代码
resp = requests.get(url)
html = etree.HTML(resp.text)
# print(resp.text)
# 从页面源代码里提取我们想要的内容
table = html.xpath("/html/body/div/div[4]/div/div[2]/div[2]/table")[0]
# print(table)
trs = table.xpath("./tbody/tr") # 相对查找 剔除表头
# print(len(trs))
for tr in trs: # 拿到每个tr
txt = tr.xpath("./td/text()")
# 对数据做简单的处理:去掉/
txt = (item.replace("/","") for item in txt) # 生成器
# print(list(txt))
# 把数据存放在文件中
csvwriter.writerow(txt)
print(url,"提取完毕!")
if __name__ == '__main__':
# for i in range(1,10548): # 效率极其低下
# download_one_page(f"http://www.jnmarket.net/import/list-1_{i}.html")
# 创建线程池
with ThreadPoolExecutor(50) as t: # 准备一个50个线程的线程池
for i in range(1,10548):
# 把下载任务提交给线程池
t.submit(download_one_page, f"http://www.jnmarket.net/import/list-1_{i}.html")
print("全部下载完毕!")