13

作者:xiaoyu
微信公众号:Python数据科学
知乎:Python数据分析师


前情回顾:
Python从零学爬虫
Python爬虫之urllib库—进阶篇

1. 撩妹起源

俗话说的好:少壮不撩妹,长大徒伤悲啊!

图片描述

说的很对,但是在这个撩妹的时代,要想成功把到妹,还真得花点心思才行啊。每次和妹子约会,妹子有时就会问:最近有啥好看的电影没?对于妹子的提问,回答要么就是不知道,要么就是自己去查app了,觉得这样有时候就缺少了一些互动的乐趣了。

于是就在想,如果有个能爬取电影咨询并能自动回复的东东是不是觉得有点小惊喜呢?

效果如如下:
图片描述

(后面有完整的效果)

也是前一阵闲来无事,想到这就搞了个简单的小工具,实现了在微信上实时爬取网站电影咨询的功能。这样子,以后就可以偶尔给给妹子来个小惊喜,没准儿就投怀送抱了呢。哈哈,博主也是开玩笑,其实就是觉得好玩,撸起袖子就是一顿敲。

2. 功能分析

好了,让我们看看是怎么个一回事。

网站电影的信息来源于豆瓣,都说豆瓣评分很公正客观,所以就选了这个作为目标源了。

  • 目标功能:

    • 用户输入任何带有电影字样的话(如:看电影),自动跳转到页面,提供 所有电影分类供用户选择。
    • 用户选择任意一个类型后,分别反馈给用户按热度、时间、评论顺序排列的三份前十电影表单(电影名+评分)。
    • 用户根据提供的电影,输入任意一个电影名后,将反馈给用户关于该电影的相关详细信息表单。
    • 要求用户可以再次输入任意电影类型去搜其它电影或者此类型的任意其它电影。
  • 网站页面分析:

clipboard.png

博主看到,这个页面的这些电影类型都是动态的信息(红色框),因此不能使用常规的request方法舒服的爬取了,这里将使用Selenium自动化测试工具来解决动态页面的爬取(之后会开一篇分享Selenium如何使用)。

clipboard.png

这是点击电影进去后看到的详细信息,这些信息是静态的,在源码中有很好的体现,因此详细信息的爬取使用前几篇分享过的request方法解析,request方法详见下面:

Python从零学爬虫
Python爬虫之urllib库—进阶篇

  • 页面抓取分析:

抓取信息博主使用了Selenium中的Xpath定位动态数据,以及BeautifulSoup的方法定位静态数据,方法很多种不唯一,只供参考(后续马上开一篇归纳所有爬取信息的方法)。

  • 微信对话:

与微信互动的方面,就使用简单的接口模块itchat实现,链接里面有详细的api介绍,http://itchat.readthedocs.io/...

好了,到此基本的功能实现方法有了一个概况,下面看看源码。由于篇幅问题,博主这里贴上部分主要源码,完整源码可以在以下链接下载:https://github.com/xiaoyusmd/...

3. 源码分析

电影概况信息(电影名+评分):

def browser_action_general_info(self, type_command):
    """
    chrome browser acts to crawl the general info to users (movie name, score)
    :param type_command:
    :return:
    """
    self.driver.get(self.url_category)
    sleep(1)
    # 点击选择的电影类型
    for num in range(0, len(movie_category)):
        if type_command == movie_category[num]:
            self.driver.find_element_by_xpath('//*[@id="content"]/div/div[1]/div/div[2]/div[1]'
                                              '/form/div[1]/div[1]/label[{}]'.format(num+1)).click()
    sleep(1)
    # 进行电影概况信息爬取
    self.browser_crawl_general_info()
    # 返回一个包括所有顺序排列的电影名及评分,供后边查电影具体信息查找用
    return movie_info_hot + movie_info_time + movie_info_comment

def browser_crawl_general_info(self):
    """
    crawl the general info from douban webstie
    :return:
    """
    # 清空顺列排列的列表,为用户下一次操作准备
    del movie_info_hot[:]
    del movie_info_time[:]
    del movie_info_comment[:]
    for num in range(1, 4):
        # 分别点击hot, time, comment顺序排列
        self.driver.find_element_by_xpath('//*[@id="content"]/div/div[1]/div/div[2]/div'
                                          '[1]/form/div[3]/div[1]/label[{}]/input'.format(num)).click()
        sleep(1)
        # 分别获取三组顺序排列的前十个电影名和评分
        for counter in range(1, 11):
            if num == 1:
                movie_info_hot.append(self.get_movie_general_info(counter))
            elif num == 2:
                movie_info_time.append(self.get_movie_general_info(counter))
            elif num == 3:
                movie_info_comment.append(self.get_movie_general_info(counter))
            else:
                pass
    # 对数据进行清洗整理
    self.clean_general_info()
  • 这里使用了Chrome浏览器作为模拟对象进行爬取,由于浏览器有点慢,操作间隙加了一些延迟,不然反应不过来。如果追求速度,也可以使用PhantomJS来代替Chrome。
  • 使用Selenium的xpath定位对象,利用鼠标点击事件完成动态操作。

电影详细信息操作:

    def browser_action_detail_info(self, counter, movie_name):
    """
    chrome browser acts to crawl the detail info for users
    :param counter:
    :param movie_name:
    :return:
    """
    movie_click_num = 0
    # click the type of movie
    # 点击上次用户选择的电影类型
    for num in range(0, len(movie_category)):
        if command_cache[0] == movie_category[num]:
            self.driver.find_element_by_xpath('//*[@id="content"]/div/div[1]/div/div[2]/div[1]'
                                              '/form/div[1]/div[1]/label[{}]'.format(num+1)).click()
    sleep(1)
    # 点击选择用户选择的电影所在顺序排列
    self.driver.find_element_by_xpath('//*[@id="content"]/div/div[1]/div/div[2]/div'
                                      '[1]/form/div[3]/div[1]/label[{}]/input'.format(counter)).click()
    sleep(1)
    if counter == 1:
        for x in range(0, len(movie_info_hot)):
            if movie_name in movie_info_hot[x]:
                movie_click_num = x + 1
    elif counter == 2:
        for x in range(0, len(movie_info_time)):
            if movie_name in movie_info_time[x]:
                movie_click_num = x + 1
    else:
        for x in range(0, len(movie_info_comment)):
            if movie_name in movie_info_comment[x]:
                movie_click_num = x + 1
    # 点击电影名称进入详细页面,记录详细页面的url
    movie_detail_url = self.driver.find_element_by_xpath('//*[@id="content"]/div/div[1]/div/div[4]/div/a[{}]'
                                                         .format(movie_click_num)).get_attribute('href')
    return movie_detail_url
    
    
  • 根据用户输入的电影名,查找其在详细列信息列表movie_info_all(三个顺序排列列表hot, time,
    comment的顺序extend总和)的位置
  • 进而定位电影名在哪个排列列表中里面,然后点击进去获得该电影的url

电影详细信息页面下载:

    @staticmethod
    def download_detail_info_html(url_target):
    """
    download douban target html
    :param url_target:
    :return:
    """
    try:
        response = urllib.request.Request(url_target, headers=headers)
        result = urllib.request.urlopen(response)
        html = result.read().decode('utf-8')
        # 返回下载的网页html,以供下一步进行数据提取
        return html
    except urllib.error.HTTPError as e:
        if hasattr(e, 'code'):
            print(e.code)
    except urllib.error.URLError as e:
        if hasattr(e, 'reason'):
            print(e.reason)
            
  • 根据上面返回的电影名url进行request下载,并返回下载的html。
  • 由于不是海量数据爬取,也没加入代理IP池等反爬技术。

电影详细信息解析(字段):

    @staticmethod
    def parse_detail_info(html_result):
    """
    parse the html downloaded
    :param html_result:
    :return:
    """
    # 清空详细信息列表
    del movie_detail_info[:]
    # 定义详细信息字段 
    movie_name = ''
    actor_name_list = '主演: '
    director_name = '导演: '
    movie_type = '类型: '
    movie_date = '上映日期: '
    movie_runtime = '片长: '
    soup = BeautifulSoup(html_result, 'lxml')
    # 用BeautifulSoup方法从下载页面中提取字段信息
    movie_name = movie_name + soup.find('span', property='v:itemreviewed').string.strip()\
    + soup.find('span', class_='year').string.strip()
    director_name = director_name + soup.find('a', rel='v:directedBy').string.strip()
    for x in soup.find_all('a', rel='v:starring'):
        actor_name_list = actor_name_list + x.string.strip() + '/'
    for x in soup.find_all('span', property='v:genre'):
        movie_type = movie_type + x.string.strip() + '/'
    for x in soup.find_all('span', property='v:initialReleaseDate'):
        movie_date = movie_date + x.string.strip() + '/'
        movie_runtime = movie_runtime + soup.find('span', property='v:runtime').string.strip()
    # 将所有字段信息字符串放入详细信息列表中
    movie_detail_info.append(movie_name)
    movie_detail_info.append(director_name)
    movie_detail_info.append(actor_name_list)
    movie_detail_info.append(movie_type)
    movie_detail_info.append(movie_date)
    movie_detail_info.append(movie_runtime)
    
  • 在函数体开头清空movie_detail_info,以准备用户下次的操作。
  • 根据提供的html进行详细信息字段的解析,将各个字段字符串放进movie_detail_info大列表中。

Wehchat微信数据交换接口:

@itchat.msg_register(itchat.content.TEXT, itchat.content.PICTURE)
def simple_reply(msg):
global movie_info_all
# 接受用户任意包含“电影”的字样,跳转到指定页面等待
if u'电影' in msg['Text']:
    douban_object.browser_hotopen()
    douban_object.cvt_cmd_to_ctgy_url(msg['Text'])
    movie_category_option = ' '.join(douban_crawl.movie_category)
    itchat.send_msg('----请选择一种类型----\n' + movie_category_option, msg['FromUserName'])
# 接受用户的电影类型输入,并执行概况信息爬取,然后反馈给用户
elif msg['Text'] in douban_crawl.movie_category:
        itchat.send_msg('正在查找' + msg['Text'] + '电影...', msg['FromUserName'])
        del douban_crawl.command_cache[:]
        douban_crawl.command_cache.append(msg['Text'])
        # 进行概况信息爬取,并将所有排列列表扩展到一起
        movie_info_all = douban_object.browser_action_general_info(msg['Text'])
        itchat.send_msg('----按热度排序----\n' + '\n' + '\n'.join(douban_crawl.movie_info_hot),
                        msg['FromUserName'])
        itchat.send_msg('----按时间排序----\n' + '\n' + '\n'.join(douban_crawl.movie_info_time),
                        msg['FromUserName'])
        itchat.send_msg('----按评论排序----\n' + '\n' + '\n'.join(douban_crawl.movie_info_comment),
                        msg['FromUserName'])
# 接受用户的电影名的选择,并进行指定电影的详细字段爬取,然后返回给用户
else:
    search_num = 0
    for x in movie_info_all:
        if msg['Text'] in x:
            itchat.send_msg('正在查找' + msg['Text'] + '...', msg['FromUserName'])
            loc = movie_info_all.index(x)
            if 0 <= loc < 10:
                search_num = 1
            elif 10 <= loc < 20:
                search_num = 2
            else:
                search_num = 3
            break
    url_result = douban_object.browser_action_detail_info(search_num, msg['Text'])
    html_result = douban_object.download_detail_info_html(url_result)
    douban_object.parse_detail_info(html_result)
    itchat.send_msg('\n\n'.join(douban_crawl.movie_detail_info), msg['FromUserName'])
  • movie_detail_info等的一些列表都是用了join方法字符串化了,并用n隔开。

以上就是大部分的核心源码了,写的有点糙,欢迎大家一起讨论和指正。

4. 功能效果图

clipboard.png
clipboard.png

clipboard.png
clipboard.png

5.总结归纳

模块使用

  • 使用Selenium工具进行动态操作
  • 使用request进行相应静态请求下载
  • 使用Selenium的xpath进行数据定位和提取
  • 使用BeautifulSoup进行数据提取
  • 使用itchat完成微信对话数据交互

改进和完善

  • 用户完成操作后一定时间内无反应浏览器自动关闭
  • 多人同时发信息的并发问题
  • 发生网络等中断错误时提示给用户 将电影的图片也一起返回给用户(现在下载的图片格式webp)
  • 还有很多地方需要改进和完善,在此与大家先分享,仅供参考,更多精彩内容后续分享。

关注微信公众号Python数据科学,获取 120G 人工智能 学习资料。
图片描述
图片描述


东哥起飞
3.8k 声望9.3k 粉丝