本篇爬虫我们将爬取猫眼电影的TOP100排行榜并优化程序,使用多线程/多进程进行数据爬取。
如果你是小白可以先查看较为基础的静态爬虫文章:Bilibili每日排行榜爬虫这篇文章中包含静态网页的爬取,利用正则进行的数据提取,数据的写入等,使用的方法也为比较基础

网页解析

首先到猫眼Top100榜单页面,可以看到总共有十页数据,每页数量为10部电影。点击第二页,可以看到URL为https://maoyan.com/board/4?offset=20,后续点击其他页数也有相同的规律,基本可以推断出offset参数为偏移量,只需要构造URL并改变偏移量即可。

确定URL后我们右键查看网页源代码,发现源代码中已包含所有我们需要的数据,基本可以确定这是一个静态网页,因此只需要请求网页内容并解析即可。

在这里插入图片描述

爬取代码

请求网址

利用request获取html信息,并进行异常处理.

def get_html_text(url, header):
    try:
        response = requests.get(url, headers=header, timeout=30)
        response.encoding = 'utf-8'
        if response.status_code == 200:
            print("请求成功")
            return response.text
    except requests.exceptions.RequestException:
        print("请求失败,请检查网络条件或重新运行")

解析HTML

之后使用正则将上一步返回的html文本进行解析并返回提取的结果

def get_info_from_htmltext(html):
#使用正则提取数据
    pattern = re.compile('.*?board-index.*?>(\d+)</i>'
                         + '.*?<p class="name"><.*?>(.*?)</a></p>'
                         + '.*?<p class="star">(.*?)</p>'
                         + '.*?<p class="releasetime">(.*?)</p>'
                         + '.*?<p class="score"><i class="integer">(.*?)</i><i class="fraction">(.*?)</i></p>', re.S)
    infomation = re.findall(pattern, html)
    for info in infomation:
        # 构造生成器函数,生成迭代对象
        yield {
            '排名': info[0],
            '电影名称': info[1],
            '主演': info[2].strip()[3:],#去除空格与换行符
            '上映时间': info[3].strip()[5:],
            '评分': info[4] + info[5]
        }

这一段中我们使用 strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列,同时使用yield函数创建一个可迭代的字典对象。
关于yield函数的使用可以查看这一篇文章:http://smilecoc.vip/2020/10/10/python%E4%B8%ADyield%E7%9A%84%E7%94%A8%E6%B3%95%E8%AF%A6%E8%A7%A3/

数据保存

这一步骤中将上一步骤中的返回的数据保存到csv中

def save_data(data):
    with open('miaoyan_top_100_film.csv', 'a', newline='', encoding='utf-8-sig', errors='ignore') as f:
        csv_file = csv.writer(f)
        csv_file.writerow([data['排名'], data['电影名称'], data['主演'], data['上映时间'], data['评分']])

主程序

最后完成主程序:

 def main(page):
    url="https://maoyan.com/board/4?offset=" + str(page * 10)
    header = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36",
        "cookie": "__mta=217946409.1602755224603.1602820125740.1602820410838.8; uuid_n_v=v1; uuid=37FEFDA00ECB11EBB74D8BF542AFB5B820214EE9447685C70B6F27096D57; _csrf=a69cdd1c13dad15868d1d821fac56d68691a1af9466e3534930fadceea199; Hm_lvt_703e94591e87be68cc8da0da7cbd0be2=1602755166; _lxsdk_cuid=17356de2c8-0c5a381c554641-4353760-100200-17356de2b62c8; _lxsdk=37FEFDA00ECB1EBB74D8BF51EFE42AFB5B820214EE9447685C70B6F27096D57; SL_GWPT_Show_Hide_tmp=1; SL_wptGlobTipTmp=1; Hm_lpvt_703e94591e87be68cc8da0da7cbd0be2=1602820411; _lxsdk_s=1752f852f16-684-f32-ead%7C%7C11"
    }
    html=get_html_text(url,header)
    for one_page_data in get_info_from_htmltext(html):
        print(one_page_data)
        save_data(one_page_data)


if __name__ == '__main__':
    for i in range(10):
        main(i)

这里有一个注意的地方,如果在header只有User-Agent运行代码,返回了200,看似很成功,但即将引来第二个障碍——我们发现它虽然返回了html信息,但是仔细一看有一个大大的验证中心,并不是我们需要的html页面
在这里插入图片描述
针对这种情况,我们加上自己的cookies就可以破解反扒了。

同时为了增加爬取速度,我们可以使用多线程或者多进程爬取

多线程与多进程

关于多线程与多进程的概念请参阅:http://smilecoc.vip/2020/10/10/%E5%A4%9A%E8%BF%9B%E7%A8%8B%E5%92%8C%E5%A4%9A%E7%BA%BF%E7%A8%8B%E7%9A%84%E6%A6%82%E5%BF%B5/

多进程爬取

Python中使用多进程需要multiprocessing,以及提供了process、queue、pipe、lock、pool等组件,这里主要使用的是pool,可以叫做进程池。

使用pool前需要先了解:

  • pool = Pool(n): 建立进程池,n就是代表了建立几个进程,这个n的设定一般与cpu的核数一样
  • pool.map(def,list):把列表list里面的每一项映射到你所定义的def函数内,有点通过这句话做list各项循环的意味
  • pool.close():关闭进程池,不再接受新的进程
  • pool.join():主进程阻塞等待子进程安全退出,父子进程同步
    多进程爬取代码如下:
    if __name__ == '__main__':
      pool=Pool(4)#根据电脑情况设置
      pool.map(main,(i for i in range(10)))

多线程爬取

同样多进程爬取需要用到threading模块。启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行,基本语法为:

  • *threading.Thread(target=程序名,args=(传入参数)): *创建线程
  • *Thread.run(): * 用以表示线程活动的方法。
  • *Thread.start(): *启动线程活动。
  • *Thread.join([time]): * 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。

本例中实现多线程需要对主函数进行修改,主要代码为:

#多线程    
if __name__ == '__main__':
    t1 = Thread(target=main, args=(0, 5))
    t2 = Thread(target=main, args=(5, 10))
    t1.start()
    t2.start()

多线程和多进程编程,模型复杂,容易发生冲突。例如在本例中使用多线程,多进程写入数据的话会造成一些不可预知的错误,所以必须用锁加以隔离,同时,又要小心死锁的发生,或者需要将各个线程/进程的数据保存在内存中再写入才可以避免错误。