美文网首页python大法攻略
Scrapy+redis分布式爬虫(三、具有模拟登陆功能爬虫的编

Scrapy+redis分布式爬虫(三、具有模拟登陆功能爬虫的编

作者: 眼君 | 来源:发表于2020-09-14 08:57 被阅读0次
    cookie与session

    cookie是浏览器支持的一种本地存储方式, 以键值对的方式存储到浏览器中。

    之所以需要cookie是因为http协议是一种无状态的协议, 无状态请求可以理解为两次http访问本身相互独立, 没有依赖关系, 两次访问的依赖关系记录在cookie中。

    session是记录在服务端的数据, 用来记录某个客户端的数据记录。

    实现模拟登录

    考虑复用之前的代码, 所以我们在之前的项目目录下新建一个spider脚本文件:

    cd ArticleSpider
    scrapy genspider zhihu(爬虫名)  www.zhihu.com(目标网站地址) 
    
    下载安装Selenium
    pip install selenium 
    

    同时, 我们需要下载对应浏览器的driver, 以chromedriver为例, 我们需要在http://npm.taobao.org/mirrors/chromedriver/下载对应版本的driver, 我们可在chrome中又上角点击“帮助”, “关于chrome”获得其版本号:

    Selenium的使用
        直接使用chromedriver打开chrome浏览器并模拟登录时, 很多网站都能识别出来,包括知乎, 这种方式启动的浏览器和平时手动打开的浏览器会有一些区别, 这些区别并不是因为http请求头参数不一致, 而是因为chromedriver有一些JS变量, 会暴露其真实身份, 从而被拒绝登陆。这时我们可以手动启动一个chrome, 然后用chromedriver去连接它, 我们用如下命令启动一个chrome浏览器:
    
    <打开chrome的可执行文件路径> --remote-debugging-port=9222 
    
        这样我们就用命令行打开了一个浏览器, 并且在9222号端口上进行监听。然后我们还需要确认这个开启的浏览器是有效的,具体方法是在浏览器中输入地址[http://127.0.0.1:9222/json](http://127.0.0.1:9222/json), 查看是否能打开一个json页面。确认有效后我们开始编写driver的配置代码:
    
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    import os
    import time
    
    chrome_option = Options()
    chrome_option.add_argument("--disable-extensions")
    chrome_option.add_experimental_option("debuggerAddress","127.0.0.1:9222")
    driver_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ,"drivers/chromedriver")
    browser = webdriver.Chrome(executable_path = driver_path,chrome_options=chrome_option)
    browser.get('https://www.zhihu.com/signin')
    browser.find_element_by_css_selector('form > div.SignFlow-tabs > div:nth-child(2)').click()
    browser.find_element_by_css_selector('.SignFlow-accountInput.Input-wrapper input').send_keys("")
    browser.find_element_by_css_selector('.SignFlow-password  input').send_keys("")
    browser.find_element_by_css_selector('form > button').click()
    time.sleep(60) 
    
    模拟登录优化

    以上方法基本能保证登录成功了, 但是selenium不能保证每一次操作都确定有效, 因此我们要对模拟登录进行一些优化, 首先我们在每次输入账号密码前, 模拟全选快捷键, 保证输入信息前, 对<input>中原本的信息进行清空:

    from selenium.webdriver.common.keys import Keys
    ......
    
    ......
    browser.find_element_by_css_selector('.SignFlow-accountInput.Input-wrapper input').send_keys(Keys.CONTROL + "a")
    browser.find_element_by_css_selector('.SignFlow-accountInput.Input-wrapper input').send_keys("")
    browser.find_element_by_css_selector('.SignFlow-password  input').send_keys(Keys.CONTROL + "a")
    browser.find_element_by_css_selector('.SignFlow-password  input').send_keys("")
    ...... 
    

    接下来, 我们对click()操作进行优化, 因为selenium控制的click()有时候会失效。这里我们可以下载一个工具, 模拟鼠标的操作:

    pip install mouse 
    

    接下来开始模拟鼠标移动和点击:

    from mouse import move,click
    ......
    
    #获得确定按钮的位置(也可以考虑下载一个屏幕坐标获取工具)
    browser.find_element_by_css_selector('form > button').location
    move(874,567)
    click()
    
    cookie保存

    当我们用driver登录过一次后, 再次打开登录页面, 发现用户名密码也已经自动填充了, 这说明driver是能保存我们的cookie的, 现在我们来想办法将driver中的cookie取出来, 我们在登录完成后执行以下代码, 并用debug的方式执行:

    ......
    cookies = browser.get_cookies() 
    

    debug后, 我们得到了drivers存储的cookies的值, 我们也可以使用python内置的pickle将cookies存储到文件中:

    import pickle
    ......
    cookies = browser.get_cookies()
    pickle.dump(cookies,open("<file_path>",'wb'))
    cookie_dict = {}
    for cookie in cookies:
        cookie_dict[[cookie["name"]] = cookie["value"]] 
    

    注意,如果我们要使用cookie,需要先去settings配置激活一下两行变量:

    ......
    #如果这里配置为True, scrapy将会自动将上一个request的cookies添加到下一个request中,实际上除了第一个request, 后续的request我们都不需要添加cookies的参数了.
    COOKIES_ENABLED = True
    #是否允许打印cookie信息
    COOKIES_DEBUG = True
    ...... 
    

    此外, 我们还需要在settings中设置一个user_agent, 否则scrapy将很容易别服务器识别为爬虫,同时,我们也需要配置一下相应的middleware,这个组件的用法, 后面会提到:

    ......
    USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"
    ......
    
    DOWNLOADER_MIDDLEWARES = {
        #'ZhihuSpider.middlewares.ZhihuspiderDownloaderMiddleware': 543,
        'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware':2
    } 
    
    知乎倒立文字验证识别

    我们还是不能有侥幸心理, 还是要想办法解决倒立验证码的问题, 这里我们可以使用git上的一个开源项目“zheye”

    git clone --depth=1 https://github.com/muchrooms/zheye.git
    cd zheye
    pip install -r requirements.txt 
    

    下载完以后, 我们将Zheye这个文件夹放到主目录下, 按如下代码就可以调用:

    from ZhihuSpider.zheye import zheye
    z = zheye()
    positions = z.Recognize('zhihu_images/a.gif')
    print(positions) 
    

    positions显示的就是倒立字的坐标了, 利用这个坐标加上前面的mouse就可以解决倒立字验证码的问题了。

    重写start_requests

    在我们已经从技术上实现了模拟登陆过程, 并成功拿到了cookies后, 我们开始将这个过程写到scrapy当中去, 这里我们需要用到scrapy的一个函数start_requests, 每一个spider在处理start_urls时, 首先会进入的就是start_requests, 其原码如下:

    def start_requests(self):
        cls = self.__class__
        if method_is_overridden(cls, Spider, 'make_requests_from_url'):
            warnings.warn(
                "Spider.make_requests_from_url method is deprecated; it "
                "won't be called in future Scrapy releases. Please "
                "override Spider.start_requests method instead (see %s.%s)." % (
                    cls.__module__, cls.__name__
                ),
            )
            for url in self.start_urls:
                yield self.make_requests_from_url(url)
        else:
            for url in self.start_urls:
                yield Request(url, dont_filter=True) 
    

    start_requests方法是spider处理start_urls的入口,即处理start_urls的第一个方法,它会将start_urls遍历, 依次传递给Request, 我们可以重写此方法, 在下载start_urls页面时先进行模拟登录:

    def start_requests(self):
        return [scrapy.Request(url=' headers= self.headers,callback = self.login]
    
    def login(self):
        chrome_option = Options()
        chrome_option.add_argument("--disable-extensions")
        chrome_option.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
        driver_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "drivers/chromedriver")
        browser = webdriver.Chrome(executable_path=driver_path, chrome_options=chrome_option)
        browser.get('https://www.zhihu.com/signin')
        browser.find_element_by_css_selector('form > div.SignFlow-tabs > div:nth-child(2)').click()
        browser.find_element_by_css_selector('.SignFlow-accountInput.Input-wrapper input').send_keys(Keys.CONTROL + "a")
        browser.find_element_by_css_selector('.SignFlow-accountInput.Input-wrapper input').send_keys("")
        browser.find_element_by_css_selector('.SignFlow-password  input').send_keys(Keys.CONTROL + "a")
        browser.find_element_by_css_selector('.SignFlow-password  input').send_keys("")
        browser.find_element_by_css_selector('form > button').click()
        time.sleep(10)
        return [scrapy.Request(url=self.start_urls[0], dont_filter=True)] 
    
    编写知乎解析逻辑

    start_requests在完成登陆后, 会通过request将第一个start_url, 也就是知乎登陆后进入的首页进行下载解析, 并将response返回给parse方法, 然后在parse方法中通过request从首页中解析出所有的question页面的URL, 回调给parse_question处理, 如果拿到的不是question页面的URL, 则回调给自身进一步解析question的URL:

    def parse(self, response):
        """
        提取出html页面中所有的url进行判断,如果提取的URL格式是/question/xxx进下载进行解析
        """
        all_urls = response.css("a::attr(href)").extract()
        all_urls = [urljoin(response.url,url) for url in all_urls]
        all_urls = filter(lambda x:True if x.startswith("https") else False,all_urls)
        for url in all_urls:
            match_obj = re.match("(.*zhihu.com/question/(\d+))(/|$).*",url)
            if match_obj:
                request_url = match_obj.group(1)
                print(request_url)
                yield scrapy.Request(request_url,headers=self.headers, dont_filter=True,callback=self.parse_question)
            else:
                yield scrapy.Request(url,headers=self.headers,callback=self.parse) 
    

    parse_question方法用于解析question页面, 获取每一个question的数据:

    def parse_question(self, response):
        """
        处理question页面, 提取出具体的items
        """
        match_obj = re.match("(.*zhihu.com/question/(\d+))(/|$).*", response.url)
        if match_obj:
            question_id = match_obj.group(2)
        item_loader = ItemLoader(item=ZhihuQuestionItem(),response=response)
        item_loader.add_css("title",".QuestionHeader-title::text")
        item_loader.add_css("content",".QuestionHeader-detail")
        item_loader.add_value("url",response.url)
        item_loader.add_value("zhihu_id",question_id)
        item_loader.add_css("answer_num",".List-headerText span::text")
        item_loader.add_css("comments_num",".QuestionHeader-Comment > button::text")
        item_loader.add_css("watch_user_num", ".NumberBoard-itemValue::text")
        item_loader.add_css("topics", ".QuestionHeader-tags .Popover::text")
    
        question_item = item_loader.load_item() 
    

    解析完question的数据后, 我们接下来开始解析answer的数据, answer的数据是通过访问接口动态加载的, 我们通过分析得知访问接口的参数都放在URL中:

    start_answer_url = 'https://www.zhihu.com/api/v4/questions/{0}/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_labeled%2Cis_recognized%2Cpaid_info%2Cpaid_info_content%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%2A%5D.topics&limit={1}&offset={2}&platform=desktop&sort_by=default' 
    

    该URL返回的数据是json形式了,可以使用json方式解析该数据:

    def parse_answer(self,response):
        ans_json = json.loads(response.text)
        is_end = ans_json.get("paging").get("is_end")
        totals_answer = ans_json.get("paging").get("totals")
        next_url = ans_json.get("paging").get("next") 
    
    配置pipelines

    pipelines的使用方式, 之前已经介绍过了, 基本用法不做过多介绍, 这里重点说明, 当我们通过pipelines的items有两个时, 如何在一个pipelines中实现不同的item去执行不同的sql语句呢, 这里有一个办法是将sql写入items中:

    class ZhihuQuestionItem(scrapy.Item):
    ......
        def get_sql(self):
            <insert_sql>
            <params>
            return <insert_sql>,<params>
    
    class MysqlTwistedPipeline(object)
    ......
        def do_insert(self,cursor,item):
            insert_sql,params = items.get_sql()
                ......
    

    相关文章

      网友评论

        本文标题:Scrapy+redis分布式爬虫(三、具有模拟登陆功能爬虫的编

        本文链接:https://www.haomeiwen.com/subject/yinuektx.html