本文主要采用requests库和正则表达式爬取猫眼电影的top100排名的电影的信息,包括电影、评分、主要演员等信息,并保存到本地文件;同时,下载电影的图片到本地目录。

主要内容:

一、目标站点分析;

二、数据爬取的流程;

三、讨论与扩展

一、目标站点分析:

进入目标站点猫眼电影https://maoyan.com/board/4

点击下一页,观察第二页的url:

可以发现不同的分页由offset取不同的整数10倍数来区分,每个页面显示10条电影信息,所以offset以10为偏移量,第一页的offset是10,第二页就是20,依此类推。其中每部电影的信息采用标签dd来描述:

主要思路:爬取第一页数据,然后采用正则表达式提取前10部电影的信息,要爬取前100的电影,后续的只要重复第一页的操作即可。

二、数据爬取流程:

1、爬取首页:

定义一个get_one_page方法,参数url给出要爬取的页面,源码如下:

通过判断response返回消息的状态码是否为200,判断是否访问成功,将代码包括在try…except…语句中,捕捉可能发生的异常。

出现的问题:

将猫眼top100首页的url:https://maoyan.com/board/4? 传入到get_one_page()方法后,消息状态码是200,但打印出来的信息是乱码,说明网站采取反爬措施。如何破?通过给request设置必要的headers信息,就可以解决这一问题,具体参照浏览器访问猫眼top100时所显示的header信息。

尝试使用Accept和User-Agent这两个字段,构建headers,并修改代码request.get(url)为requests.get(url, headers=headers),重新调用方法后,打印的信息与浏览器中的网页信息一致,说明headers的设置起作用。

2、使用正则表达式解析网页:

引入re模块,创建正则表达式,因为我们要匹配:排名,电影封面,片名,主演姓名,上映时间,评分等6个信息,都写在一个正则表达式中,整个代码会比较长,需要耐心处理:

1
pattern = re.compile('<dd>.\*?board-index.\*?>(\\d+)</i>.\*?data-src="(.\*?)".\*?class="name">.\*?>(.\*?)</a>.\*?'+'class="star">(.\*?)</p>.\*?class="releasetime">(.\*?)</p>.\*?class="integer">(.\*?)</i>'+'.\*?class="fraction">(.\*?)</i></p>.\*?</dd>', re.S)

该代码的几个注意点:

  • a. 目标数据点,即要提取的信息,需要使用”()”包裹起来;比如第一个是关于排名的数字,其正则表达式”\d+”采用“()”包裹;

  • b. 无关紧要的中间字符,采用”.*?”过滤掉;

  • c. 因为正则表达式过程,中间必需切分换行,换行相当于对一个长字符串切分;

  • d.”re.S”需要加上,表示跨\n符进行匹配;

完整的代码如下:

1
2
3
4
def parse_one_page(html):
         pattern = re.compile('<dd>.\*?board-index.\*?>(\\d+)</i>.\*?data-src="(.\*?)".\*?class="name">.\*?>(.\*?)</a>.\*?'+'class="star">(.\*?)</p>.\*?class="releasetime">(.\*?)</p>.\*?class="integer">(.\*?)</i>'+'.\*?class="fraction">(.\*?)</i></p>.\*?</dd>', re.S)
        items = re.findall(pattern, html)
        print(items)

返回的结果是list,信息比较杂乱,使用yield方法,对其进行字典格式化:

1
2
3
4
5
6
7
8
9
10
items = re.findall(pattern, html)
for item in items:
yield{
     'index': item[0],
     'image': item[1],
      'title': item[2],
      'actor': item[3].strip()[3:],
     'time': item[4].strip()[5:],
     'score':item[5]+item[6]
}

上述代码索引actor的strip()方法主要用于除去换行符,而[3:]主要截取字符串,删除”主演:”这三个字符。

3、存储数据到文件:

定义一write_to_file()方法,参数为步骤2中的字典格式化内容,代码如下:

1
2
3
4
def write_to_file(content):
      with open("result.txt", 'a') as f:
          f.write(json.dumps(content)+'\n')
           f.close()

因为是字典序,使用json.dumps将dict类型数据转化为string类型数据,File文件的打开类型选择’a’,表示追加写的方式,运行代码后,当前的PyCharm目录下自动增加result.txt文件,但是里面的中文以乱码显示,所以设定编码方式为utf-8,修改后的代码:

1
2
3
4
def write_to_file(content):
         with open("result.txt", 'a', encoding='utf-8') as f:
                f.write(json.dumps(content, ensure_ascii=False)+'\n')
                f.close()
4、开启循环,抓取前100电影信息:

根据每个分页以offset偏移量为10,构建循环处理的代码,每次循环即处理新的offset url值的过程,代码如下:

1
2
3
4
5
6
7
8
9
for i in range(10):
         main(i*10)

def main(offset):
    url="https://maoyan.com/board/4?offset=" + str(offset)
       html=get_one_page(url)
        for item in parse_one_page(html):
            print(item)
            write_to_file(item)
5、下载电影封面图到本地:

图片属于二进制文件,给定url路径使用request的get方法即可获得数据,定义save_image_file(url, path)方法,其中path指定图片的保存路径。

1
2
3
4
5
6
7
def save_image_file(url, path):
        res = requests.get(url)

if res.status_code == 200:
        with open(path, 'wb') as f:
               f.write(res.content)
               f.close()

上述代码的调用示例:

1
save_image_file(item['image'], 'cover/' + item['title'] + '.jpg')

三、讨论与扩展:

1、为加快数据爬取速度,可实现多线程或者多进程抓取,比如可使用多进程包multiprocessing:

示例代码如下:

1
2
3
from multiprocessing import pool
pool = Pool()
pool.map(main, [i*10 for i in range(10)]

但使用多进程,容易被网站侦测,导致封ip,所以需同时配合ip代理池使用。

2、短时间爬取次数过多后,会发生程序出错,原因可能是被网站侦测到爬虫,导致封ip行为,所以必须配合ip代理池;