scrapy爬取知名问答网站(解决登录+保存cookies值+爬取问答数据)--完整版完美解决登录问题

菜鸟写Python:scrapy爬取知名问答网站 实战(3)

一、文章开始:

可能看到这篇文章的朋友,大多数都是受慕课网bobby讲师课程的影响,本人也有幸在朋友处了解过这个项目,但是似乎他代码中登录方式因为知乎的改版而不再适用,因为知乎的登录变得越来越复杂,但是要爬取知乎首页问答,不登录是无法爬取的。

这篇文章将分享我在启发下,利用Scrapy结合Selenium实现知乎登录+保存登录cookies+读取保存cookies爬取首页问答内容+处理API请求的Json数据+处理多个Item数据保存到MySql,一整套的完整实现,利用selenium神器得到cookies估计中简答粗暴的方式是永久不过期的,据说,bobby老师后面也是结合selenium登录的。

二、必须铭记于心的原理

scrapy爬取知乎问答网站这个项目的关键在于最开始登录,而登录无论我们采用何种方式,其关键在于cookies和session来保存我们的请求对话,毕竟我们爬虫模拟的就是浏览器请求数据。

所以我们要采用一种登录方式,然后拿到登录之后的cookies值,在后面的请求带上登录之后的cookies就可以在之后的请求中以登录状态请求了,加上scrapy请求会记录cookies,所以我们在只要在请求知乎首页带上一次cookies即可。

知道我们登录任务的关键在于记录登录后的cookies,那就我们想尽一切招来做这件时好了:

我目前使用过的保存cookies的方法:

1.urllib库request+cookielib(登录,保存cookies,读取cookies请求网页);

可以参照:Cookie登录爬取实战:Python 利用urllib库的cookie实现网站登录并抓取

2.直接复制浏览器的cookies,在代码中构建cookies值;

3.利用selenium模拟登录+保存cookies(本文采用第三中)

三、实现过程

3.1 利用selenium模拟浏览器登录(扫码方式)

3.2 将登录之后的cookies写入(保存成json文件)

3.3 在请求知乎首页前从json文件读取cookies构建dict类型的cookies

3.4 带上dict类型的cookies和headers请求知乎首页

3.5 爬取知乎首页问题和问题的回答

3.6 数据保存到MySql数据库中

当然,3.1-3.4 的实现过程也可以参照:菜鸟写Python实战:Scrapy完成知乎登录并保存cookies文件用于请求他页面(by Selenium),当然,本文将是一整个知乎问答的爬取过程代码

四、实现代码

环境:python3.6 + scrapy

3.1 登录和保存cookies

利用selenium模拟浏览器登录(扫码方式):selenium这个神器的真的太强大了,这里只要知道它是用来模拟浏览器即可。如何你想进一步了解,可以看这篇:菜鸟写Python-Selenium操作:Selenium登录豆瓣并获取cookies

    def loginZhihu(self):
        loginurl='https://www.zhihu.com/signin'
        # 加载webdriver驱动,用于获取登录页面标签属性
        driver=webdriver.Chrome()
        driver.get(loginurl)

        # 扫描二维码前,让程序休眠10s
        time.sleep(10)
        input("请点击并扫描页面二维码,手机确认登录后,回编辑器点击回车:")
        # 获取登录后的cookies
        cookies = driver.get_cookies()
        driver.close()
        # 保存cookies,之后请求从文件中读取cookies就可以省去每次都要登录一次的,也可以通过return返回,每次执行先运行登录方法
        # 保存成本地json文件
        jsonCookies=json.dumps(cookies)
        with open('zhihuCookies.json','w') as f:
            f.write(jsonCookies)

3.2 读取cookies和请求知乎首页

    def start_requests(self):
        # 首次要执行登录知乎,用来保存cookies,
        # 之后的请求可以注释该方法,每次请求都做登录那真的很烦
        self.loginZhihu()
        # self.loginZhihu()
        # 读取login保存的cookies值
        with open('zhihuCookies.json','r',encoding='utf-8') as f:
            listcookies=json.loads(f.read())
        # 通过构建字典类型的cookies
        cookies_dict = dict()
        for cookie in listcookies:
            cookies_dict[cookie['name']] = cookie['value']
        # Tips 我们在提取cookies时,其实只要其中name和value即可,其他的像domain等可以不用
        # yield发起知乎首页请求,带上cookies哦-->cookies=cookies_dict
        yield scrapy.Request(url=self.start_urls[0],cookies=cookies_dict,headers=self.headers,dont_filter=True)

Tips: 我们scrapy框架初始请求时start_request()方式,然后在yield请求提交scrapy.Request()中,不写callback=会默认调用parse()方法,所以:

yield scrapy.Request(url=self.start_urls[0],cookies=cookies_dict,headers=self.headers) 
等同于   yield scrapy.Request(url=self.start_urls[0],cookies=cookies_dict,headers=self.headers,callback=parse)

3.3 爬取知乎首页问答url,以便爬取每一问答内容

    # 爬取知乎首页,这里我们要做的是讲所以是知乎问答的请求url筛选出来
    def parse(self, response):
        # 爬取知乎首页->内容TopstoryMain中的->所有urls
        content_urls=response.css(".TopstoryMain a::attr(href)").extract()
        url_list=[] # 保存全部url,并用来进一步提取是question的URl
        for url in content_urls:
            # 大部分的url都是不带主域名的,所以通过parse函数拼接一个完整的url
            urls=parse.urljoin(response.url,url)
            url_list.append(urls)
        # 筛选所有urls中是知乎问答的urls(分析问答urls格式发现是:/question/xxxxxxxxx)
        for url in url_list:
            url=re.findall("(.*question/([\s\S]*?))(/|$)",url)  # 正则表达式匹配问答url
            if url:
                # 如果提取到question的url则通过yield请求question解析方法进一步解析question
                request_url=url[0][0]  # 问答url
                request_id=url[0][1]   # 问答id
                # 请求该问答详情页
                yield scrapy.Request(url=request_url,headers=self.headers,callback=self.parse_detail_question,meta={"zhihu_id":request_id})
            else: # 否则说明当前知乎页面中没有question,那么在此请求知乎首页,相当于刷新,直到出现question
                yield scrapy.Request(url=response.url,headers=self.headers,callback=self.parse)
    # 这两次的scrapy.Request()并没有带上cookies了,但实际它依然是登录后的状态,scrapy的作用

3.4 爬取一个具体的知乎问答内容

    # 爬取并解析问答详细页面,同时请求该问答知乎用户的回答
    def parse_detail_question(self,response):
        start_answe_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%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics%3Bdci_info&limit={1}&offset={2}&sort_by=default'

        # 以下是知乎问答的每一个字段提取,通过css方法,这里就不详细注释了
        time.sleep(1) # 进程休眠1s 防止请求太快,知乎给你掐了ip
        zhihu_id=response.meta.get("zhihu_id","")
        topics=response.css(".TopicLink>div>div::text").extract()
        topics=",".join(topics)
        url=response.url
        title=response.css('.QuestionHeader-title::text').extract_first('')
        # 只获取标签,不提取标签的文本,以便下一步做去标签处理
        content=response.css('.QuestionHeader-detail span').extract_first("")  
        # from w3lib.html import remove_tags 去除标签,留下文本
        content=remove_tags(content).strip()
        answer_num=response.css(".List-headerText>span::text").extract_first("").strip()
        # 匹配所有的数字,然后拼接成一起,因为大于三位数的回答数中有,隔开
        answer_nums=re.findall('([\d])',answer_num)   
        answer_num=''
        for answer in answer_nums:
            answer_num+=answer
        comment_num=response.css(".QuestionHeader-Comment").extract_first('')
        if "评论" in comment_num: # 判断是否有评论,可能一些问答没有评论
            comment_num=response.css(".QuestionHeader-Comment>button::text").extract_first('')
            comment_num=re.findall('([\s\S]*?)条评论',comment_num)[0].strip()
        watch_click=response.css(".NumberBoard-itemValue::text").extract()
        if len(watch_click[0])>3: # 去除大于3位数多出来的逗号'1,234'
            watch_user_num = watch_click[0]
            watch_user_nums = re.findall('([\d])', watch_user_num)
            watch_user_num = ''
            for i in watch_user_nums:
                watch_user_num += i
        else :
            watch_user_num=watch_click[0]
        if len(watch_click[1]) > 3:  # 去除大于3位数多出来的逗号'1,234'
            click_num = watch_click[0]
            click_nums = re.findall('([\d])', click_num)
            click_num = ''
            for i in click_nums:
                click_num += i
        else:
            click_num = watch_click[1]
        # 获取当前的时间,并把datetime转换成字符
        crawl_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 
        # 传给Item
        questionItem=ZhihuQuestionItem()
        questionItem['zhihu_id']=zhihu_id
        questionItem['topics'] = topics
        questionItem['url'] = url
        questionItem['title'] = title
        questionItem['content'] = content
        questionItem['answer_num'] = answer_num
        questionItem['comment_num'] = comment_num
        questionItem['watch_user_num'] = watch_user_num
        questionItem['click_num'] = click_num
        questionItem['crawl_time'] = crawl_time
        yield questionItem
        # 请求知乎相应回答数据,根据分析出的api接口发起请求,得到是json数据
        # start_answe_url.format(zhihu_id,20,0),请求url中有三个要传参数通过format()给
        yield scrapy.Request(url=start_answe_url.format(zhihu_id,20,0),headers=self.headers,callback=self.parse_answer)

3.5 爬取相应知乎问答的用户回答(这是一个api接口,可以根据页面加载更多分析得到)

    # 请求知乎问答的用户回答,api接口,返回是一个json数据
    def parse_answer(self,response):
        answer_data=json.loads(response.text)
        is_end=answer_data['paging']['is_end'] # 判断是否还有下一页
        for data in answer_data['data']: # 解析json中相应想要爬取的数据
            zhihu_id=data['id']
            url=data['url']
            question_id=data['question']['id']
            author_id=data['author']['id'] if 'id' in data['author'] else None
            content = data['content']
            praise_num = data['voteup_count']
            comments_num = data['comment_count']
            # 将获取时间格式为1530878188数字串转换成时间格式
            create_time = datetime.datetime.fromtimestamp(data['created_time'])  
            update_time = datetime.datetime.fromtimestamp(data['updated_time'])
            crawl_time= datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') #同样转成str
            # 保存Item
            answerItem = ZhihuAnswerItem()
            answerItem['zhihu_id'] = zhihu_id
            answerItem['url'] = url
            answerItem['question_id'] = question_id
            answerItem['author_id'] = author_id
            answerItem['content'] = content
            answerItem['praise_num'] = praise_num
            answerItem['comments_num'] = comments_num
            answerItem['update_time'] = update_time
            answerItem['create_time'] = create_time
            answerItem['crawl_time'] = crawl_time
            yield answerItem
        if not is_end: # 如果不是下一页,则带上next_url回调该方法
            next_url=answer_data['paging']['next']
            yield scrapy.Request(url=next_url,headers=self.headers,callback=self.parse_answer)

根据api请求返回的json数据:

3.6 将爬取的数据库保存到MySQL数据库中

scrapy库的执行过程是,将数据提交给itme,然后传给pipeline进行其他操作,如保存文件,转存到mysql数据库。

所以,这一步的核心在于在pipeline保存数据到mysql数据库中,但是这一步的关键在于我们有两个非同时yield出去的item,在pipeline中,我们无法同时处理这个两个item,即使你写两个pipeline分别处理,虽然可以入库,但是依然完美(虽然我干过,体验很不好),其实bobby老师在课程中给了一种非常好的方法,即将tem不同的处理操作,在各自的item中处理,在pipeline中只做相同的事。

讲了一堆自己啰嗦不清晰的后,还是直接来说下转存mysql的操作,可能会好理解一些。

第一步:在Item中,新增初入数据库的方法,这里只是构建sql语句和准备sql要插入的参数,然后return回pipeline执行数据库操作(其实这个便是每个item的特殊操作,通过调用这个pipeline就可以根据相应请求对应的插入操作),下面只是问答item,回答item其实结构是一样的。

# 知乎问答的item
class ZhihuQuestionItem(scrapy.Item):
    zhihu_id=scrapy.Field()
    topics=scrapy.Field()
    url=scrapy.Field()
    title=scrapy.Field()
    content=scrapy.Field()
    answer_num=scrapy.Field()
    comment_num=scrapy.Field()
    watch_user_num=scrapy.Field()
    click_num=scrapy.Field()
    crawl_time=scrapy.Field()

    # 这个是插入语句,1 构建insert语句, 2 准备insert要的参数
    def get_sql(self):
        sql="""
            insert into question (zhihu_id,topics,url,title,content,answer_num,comment_num,watch_user_num,click_num,crawl_time)
            values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
            on DUPLICATE key update content=values(content),answer_num=values(answer_num),comment_num=values(comment_num),
                                      watch_user_num=values(watch_user_num),click_num=values(click_num)
        """
        params=(self['zhihu_id'],self['topics'],self['url'],self['title'],self['content'],self['answer_num'],self['comment_num'],self['watch_user_num'],self['click_num'],self['crawl_time'])
        # 并将insert语句和参数返回(即返回到了pipeline中) 
        return sql,params

第二步,在pipeline中新建连接mysql数据执行数据库操作的pipeline

# 处理不同Item进行异步执行数据库操作
class ProcessMysqlPipeline(object):
    # 数据连接
    def __init__(self):
        params=dict(
            host='127.0.0.1',
            db='zhihu',
            user='root',
            passwd='upassword',
            charset='utf8mb4',
            cursorclass=MySQLdb.cursors.DictCursor,
            use_unicode=True
        )
        self.dbpool=adbapi.ConnectionPool("MySQLdb",**params)

    #sql操作
    def do_sql(self,cursor,item):
        # 获取sql语句和参数,这个就是调用相应item的get_sql(),这样就能实现不同item各自的插入
        sql,params=item.get_sql()
        # 执行数据库语句
        cursor.execute(sql,params)

    #处理错误
    def handle_error(self,failure,item,spider):
        print(failure)

    # Pipeline发起执行语句
    def process_item(self,item,spider):
        # 使用twisted框架,将mysql操作变成异步
        query=self.dbpool.runInteraction(self.do_sql,item)
        query.addErrback(self.handle_error,spider,item)
        return item

通过,上面处理就可以实现各自item执行数据插入,pipeline写完记得要到setting中配置一下,不然就等于白写了,不执行。

转载请注明出处哦。

上面的过程就可以实现知乎问答爬取了,代码基本如上,欢迎一起交流和学习,如果有错误欢迎指正呀,如果需要源码,欢迎加微信:第一行Python代码,一起学习python。

  • 4
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值