NO.36——qq音乐全站分布式爬虫(二)

三、数据存储

         在爬虫逻辑功能实现的过程中发现数据入库的函数 insert_data(),该函数存放在music_db.py中,本节使用SQLAlchemy实现数据入库。

         从爬虫规则分析,入库的数据有歌名、所属专辑、时长、歌曲mid(下载歌曲文件以歌曲mid命名)和歌手姓名。根据所爬取的数据及性质,数据库命名如下:

song

  • song_id  :int(11)
  • song_name  :varchar(50)
  • song_album  :varchar(50)
  • song_interval  :varchar(50)
  • song_songmid  :varchar(50)
  • song_songer  :varchar(50)

        根据数据库的命名,SQLAlchemy映射数据库代码如下:

from sqlalchemy import Column, String,Integer, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
#链接数据库
engine=create_engine("mysql+pymysql://root:****@localhost:3306/music_db?charset=utf_8",echo=False)
#创建会话对象,用于数据表的操作
DBSession=sessionmaker(bind=engine)
#创建DBSession()对象
SQLsession=DBSession()
#创建对象的基类
Base=declarative_base()
#映射数据表
class song(Base):
    #表名
    __tablename__='song'
    #字段,属性
    song_id=Column(Integer,primary_key=True)
    song_name=Column(String(50))
    song_album=Column(String(50))
    song_interval=Column(String(50))
    song_songmid=Column(String(50))
    song_singer=Column(String(50))
#创建数据表
Base.metadata.create_all(engine)
#数据入库
def insert_data(song_dict):
    #连接数据库
    engine=create_engine("mysql+pymysql://root:1992@localhost:3306/music_db?charset=utf_8",echo=False)
    #创建会话对象,用于数据表的操作
    DBSession=sessionmaker(bind=engine)
    SQLsession=DBSession()
    data=song(
        song_name=song_dict['song_name'],
        song_album=song_dict['song_album'],
        song_interval=song_dict['song_interval'],
        song_songmid=song_dict['song_songmid'],
        song_singer=song_dict['song_singer'],)
    SQLsession.add(data)
    SQLsession.commit()

        函数insert_data()主要对传递的参数song_dict进行入库处理,参数song_dict为字典格式。函数运行会创建新的数据库连接,创建新数据库连接主要是为异步编程做准备。

四、concurrent.futures

        Pathon标准库为我们提供了threading和multiprossing来实现多线程,自从Python3.2之后,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutor和ProcessPollExecutor两个类,实现了对threading和multiprocessing更高级的抽象,对编写线程、进程提供了直接的支持。

        下面简单讲述一下concurrent.futures的属性和方法:

        Executor:Executor是一个抽象类,不能被直接使用。为具体的异步执行定义了基本方法:ThreadingPoolExecutor和ProcessingPoolExecutor继承了Executor,分别被用来创建线程和进程。

        创建线程和进程后,Executor提供了submit()和map()方法对其操作。submit()和map()方法最大的区别是参数类型,map()的参数必须是列表、元组和迭代器的数据类型。

        Future:可以理解为在未来完成的操作,这是异步编程的基础。通常情况下,我们执行IO操作和访问URL时,在等待结果返回之前会产生阻塞,CPU不能做其他事情,而Future的引入帮助我们在等待的这段时间完成其他操作。

五、分布式爬虫

         现在我们知道,爬取全站歌曲信息是按照字母A-Z的顺序循环爬取的,这是在单进程单线程的情况下运行的。如果将这26次循环分为26个进程同时执行,每个进程只需执行对应的字母分类,假设执行一个分类的时间相同,那么多进程并发的效率是单进程的26倍。

         除了运行多进程外,项目代码大部分是IO密集型的,那么在每个进程下使用多线程可以提高每个进程的运行效率。我们知道歌手列表页是通过两层循环实现的,第一层是循环每个分类字母,现将每个分类字母当做一个单独的进程处理。第二层是循环每个分类的歌手总页数,可将这个循环使用多线程处理。假设每个进程使用10条线程(线程数可自行设定),那么每个进程的效率也相对提高10倍。

        分布式策略考虑的因素有网站服务器负载量、网速快慢、硬件配置和数据库最大连接量。举个例子,爬取某个网站1000万数据,从数据量分析,当然进程数和线程越多,爬取的速度越快。但往往忽略了网站服务器的的并发量。假设设定10个进程,每个进程200条线程,每秒并发量为200*10=2000,若网站服务器并发量远远低于该并发量,在请求网站的时候,就会出现卡死的情况,导致请求超时,无形之中增加等待时间。除此之外,进程和线程越多,对运行程序的系统压力越大,若涉及数据入库,还要考虑并发数是否超出数据库连接数。

        根据上述分布式策略,在music.py中分别添加函数myThread和myProcess,分别代表多线程和多进程:

# 多线程
# 修改了请求地址URL以及数据获取
def myThread(index, cookie_dict):
    cookie_dict = getCookies()
    # 每个字母分类的歌手列表页数
    url = 'https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI771604139451213&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":'+str(index)+',"sin":0,"cur_page":1}}}'
    r = session.get(url, headers=headers)
    total = r.json()['singerList']['data']['total']
    pagecount = math.ceil(int(total) / 80)
    page_list = [x for x in range(1, pagecount+1)]
    thread_number = 10
    # 将每个分类总页数平均分给线程数
    list_interval = math.ceil(len(page_list) / thread_number)

    # 设置线程对象
    Thread = ThreadPoolExecutor(max_workers=thread_number)
    for i in range(thread_number):
        # 计算每条线程应执行的页数
        start_num = list_interval * i
        if list_interval * (i + 1) <= len(page_list):
            end_num = list_interval * (i + 1)
        else:
            end_num = len(page_list)
        # 每个线程各自执行不同的歌手列表页数
        Thread.submit(get_genre_singer, index, page_list[start_num: end_num],cookie_dict)
# 多进程
# 添加Cookies获取和循环方式
def myProcess():
    with ProcessPoolExecutor(max_workers=27) as executor:
        cookie_dict = getCookies()
        for index in range(1, 28):
            # 创建27个进程,分别执行A-Z分类
            executor.submit(myThread, index, cookie_dict)
  • 多进程myProcess()函数:主要循环字母A-Z和#,将每个字母独立创建一个进程,每个进程执行的方法函数是myThread()。
  • 多线程myThread()函数:首先根据传入函数参数获取当前分类的歌手总页数,然后根据得到的总页数和设定的线程数计算每条线程应执行的页数,最后遍历设定线程数,让每条线程执行相应页数。例如总页数100页、10条线程,每条线程应执行10页,第一条线程执行0-10页,第二条线程执行10-20页,以此类推。线程调用的方法函数是get_genre_singer()。

五、总结

       以QQ音乐为爬取对象,爬取范围是全站的歌曲信息,爬取方式在歌手列表获取每一位歌手的全部歌曲。如果爬取的数量较大,就使用异步编程实现分布式爬虫开发,可提高爬虫效率。

  1. 歌曲下载 download(guid ,songmid,cookie_dict):爬虫最底层最核心的功能
  2. 歌手和歌曲信息  get_singer_songs(singermid,cookie_dict):将歌手的歌曲信息入库和歌曲下载
  3. 分类歌手列表  get_genre_singer(index,page_list,cookie_dict)  :获取单一字母分类的所有歌手和歌曲信息
  4. 全站歌手列表  get_all_singer()  : 获取全站歌曲和歌手信息
  5. 用户cookies信息   getCookies()  :  使用selenium获取用户的cookies
  6. 数据存储 insert_data(song_dict)   :  将爬取的歌手和歌曲信息入库处理
  7. 多进程  myProcess()  : 每个字母分类创建一个单独进程运行
  8. 多线程  myThread(genre)  :  每个进程使用多线程爬取数据

        

        

     

猜你喜欢

转载自blog.csdn.net/ghl1390490928/article/details/83477590