使用了协程的简单改进
使用gevent非常简单,只需要将任务放进将要开启的协程列表中即可,会根据IO操作切换任务,速度明显变快,需要注意的是monkey.patch_all()需要放在request前面进行编译,否则会报错。通过观察图片可以发现,并没有按照歌单的顺序处理,而是随机的。也就是对于异步IO来说,IO操作谁先处理完,谁先返回。
#coding=utf-8
import gevent
from gevent import monkey
#标记IO非阻塞,针对标准库
monkey.patch_all()#这玩意得在request上面
import threading
import time
import csv
import requests
from urllib import request
from io import BytesIO
from PIL import Image
from queue import Queue
from threading import currentThread
from bs4 import BeautifulSoup
q = Queue() # 生成一个队列用来存取id,这种方式存取id不需要使用锁
lock = threading.Lock()
sig=1
# 消费者通过生产者类传过来的用户id提取歌单的详细信息
def Consumer():
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'
}
#lock.acquire()
with open("./music.csv","a",encoding="utf-8-sig") as file:
write=csv.writer(file)
ids=q.get()
if (ids is None) or (len(ids)==0):
print("queue is empty")
print("%s end"%(currentThread().name))
sig=0
return
for id in ids:
url = 'https://music.163.com/' + id['href'] # 生产者传递的id链接
print("get url: %s"%(url))
response = requests.get(url=url, headers=headers)
html = response.text
soup = BeautifulSoup(html, 'html.parser')
img = soup.select('img')[0]['data-src'] # 图片链接
title = soup.select('title')[0].get_text() # 标题
idd = soup.select('.s-fc7')[0]['href'].split('=')[-1]#创建者id
nickname = soup.select('.s-fc7')[0].get_text() # 昵称
description = soup.select('p')[1].get_text() # 简介
count = soup.select('strong')[0].get_text() # 播放次数
song_number = soup.select('span span')[0].get_text() # 歌的数目
add_lis = soup.select('a i')[1].get_text() # 添加进列表次数
share = soup.select('a i')[2].get_text() # 分享次数
comment = soup.select('a i')[4].get_text() # 评论次数
write.writerow([title,idd,nickname,description,count,song_number,add_lis,share,comment])
#写文件时,为了防止争抢时写串行,加锁,写完这部分即释放
#接下来是处理图片
res=requests.get(url=img,headers=headers)
image = Image.open(BytesIO(res.content))
try:
image.save("./pic/"+str(time.time())+'.jpg')
#处理报错 OSError: cannot write mode RGBA as JPEG
except:
image.save("./pic/"+str(time.time()) + '.png')
#lock.release()
#生产者类产生用户id,将用户id传给消费者
def Producer(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'
}
response = requests.get(url=url, headers=headers)
html = response.text
soup = BeautifulSoup(html, 'html.parser')
ids = soup.select('.dec a') # 获取包含歌单详情页网址的标签
q.put(ids)#将id放入队列中
def main():
url=[]
#保存url网址
for i in range(0,1505,35):
url_item=f'https://music.163.com/discover/playlist/?order=hot&cat=%E6%AC%A7%E7%BE%8E&limit=35&offset={i}'
#网易云非常阴险的在com/和discover之间加了一个奇怪的字符#/,导致我之前爬虫的时候死活爬不出内容
url.append(url_item)
#采用线程池的方式
#采用协程的方式 gevent
url_list=[gevent.spawn(Producer,url_i) for url_i in url]#生成一个任务列表
gevent.joinall(url_list)#使用协程进行IO操作,并且按顺序返回结果
print("producer done")
print("consumer begin")
print(q.qsize())
#下面先固定csv的格式
with open("./music.csv","a",encoding="utf-8-sig") as csvfile:#之前用utf-8不行,用utf-8-sig行
writer = csv.writer(csvfile)
writer.writerow(['歌单标题', '创建者id', '创建者昵称','介绍','播放量','歌曲数量','添加到播放列表的次数','分享次数','评论数'])
#一开始设置了10个协程,导致只爬了350个歌单,刚好等于10*35,说明并没有执行循环后的操作
url_list_C=[gevent.spawn(Consumer) for i in range(q.qsize())]#生成多少个页面个协程的列表,来抓取,类似于n个线程,否则需要做循环爬取的任务
gevent.joinall(url_list_C)#使用协程进行IO操作,并且按顺序返回结果
print("mission finished")
if __name__ == "__main__":
main()
功能改进
1、以筛选播放量超过500为例
好像也用不上yield
count = soup.select('strong')[0].get_text() # 播放次数
if int(count)<500:
continue
2、根据csv文件信息,筛选url
这个功能可以用到yield,仿照示例,可以做个consumer和producer进行url传输和访问
可以添加如下功能,判断完一个IO后,传给consumer函数让该函数慢慢进行IO的写入内核端操作
# -*- coding: utf-8 -*-
import csv
#读是一个IO操作
def producer(conn):#conn为传入
with open("D:\经管大三\现代程序设计\week14\music.csv","r",encoding="utf-8") as f:
print("开始筛选")
conn.send(None)
reader = csv.reader(f)#相当于生成一个迭代,每次读是一个IO操作
lis=next(reader)#先把第一行的名称部分删除,注意每行记录之间有空行
while True:#我们希望先读取一个,然后进行解析判断,跳过读取部分
try:
lis=next(reader)#一个读的IO操作
lis=next(reader)#真正读取了一个数据行,当读完最后一个空行,再执行时,引发报错
if int(lis[4])>100000:#播放量大于100000,则将id传给consumer
conn.send(int(lis[1]))#将值传给迭代器运行
except StopIteration:
print("ending")
conn.send("nothing")
#写是一个IO操作
def consumer():
with open("./筛选的url.txt","a") as f:
while True:
print(f'开始写入')
id=yield 1
if isinstance(id,str):#说明已经筛选完了
break
url='https://music.163.com/' + str(id)
f.write(url+'\n')
def main_1():
conn=consumer()
producer(conn)
if __name__ == '__main__':
main_1()
# main_2()
pass
str(id)
f.write(url+‘\n’)
def main_1():
conn=consumer()
producer(conn)
if name == ‘main’:
main_1()
# main_2()
pass