python爬虫的学习总结

背景

基于django框架完成jira网页数据的爬取。由于对爬虫知识知道的太少,我开始了新的学习之旅。本文前半部分都是记录这一周主要的错误,如果想直接看最终成果,可以跳到本文“成功爬取”部分浏览。

学习爬虫知识

在知道了本项目可能需要爬虫后,我开始学习爬虫相关知识,通过对爬虫教程的阅读实践修改,我初步理解了一些相关的语法:

  1. 什么时候可以使用爬虫:但且仅当符合robots.txt时可以。
  2. 以get方式爬取:request.get(url),返回response,使用response.text输出html文件
  3. 以post方式爬取:调用开发者模式,选取network,查找来往文件,取出url和formdata中的内容组成新的字典,request.post(url,formdata),并用json解析,即可获得数据。
  4. 问题:原文中的链接已经不让爬取了,get的我是用baidu,post可以参考其他文章,将url中的_o去掉或者实时生成参数。

至此爬虫学习部分就暂时告一段落,截图就不放了,真正的困难还在后面

了解jira

jira,Atlassian公司出品的项目与事务跟踪工具,被广泛应用于缺陷跟踪、客户服务、需求收集、流程审批、任务跟踪、项目跟踪和敏捷管理等工作领域。我司主要用于测试提单的提交工作,但提单的数量十分庞大,一个一个查询十分不方便,因此我的任务就是爬取网站数据,并进行分析,存入数据库,并在我们自己的网页上更清晰明了的完成显示。

对API的尝试

和同事探讨了解jira后,我知道了jira有自己的API,只需要替换成我司的网站就可以爬取数据了,但很可惜,经过两天的尝试,除了几个API可以返回结果,其余的都是返回404或者json解析错误。没法得到数据。基于之前学习的一些爬虫知识,我开始转向打算使用正常原始的爬虫方法爬取数据,也就是本文的重点,也是一切问题的开始。。。
注:另一位成员则继续学习相关API,并发现python有更好封装的jira库,这个库已经证实可以爬取该网站,并且效率方面更优(因为正常爬取需要一条数据一条数据的爬取,自然不如直接查询数据库获得全部数据来的快,但基于本文方法更为通用,易于后人爬取其他网站,因此我们两个决定同步完成爬取代码的编写)

原始方法爬取

最开始的代码由于失败,已经找不到了,这也是一个教训吧,以后写代码的时候会注意按版本顺序编写,而不是失败后直接就覆盖掉。现在凭借记忆完成描述吧。
首先遇到的一大困难就是jira网站需要登录,这也是后续诸多问题的三大核心来源之一。直接爬取会出现直接跳转到登录界面,而无法获取信息。由于原始爬取的方法记忆性较差,因此在学习了cookie之后,我开始尝试使用cookie进行登录,参考模拟登录这篇文章,在真实登录过网站后,在开发者模式下选择network,查找来往文件,提取头部含有cookie的文件,复制cookie和user_agent,并将他们加入头部,很顺利的让我进入网站,获取了相关信息。但是好景不长,在第二天关机重启后,我再次运行,却发现cookie已经发生了改变,这种方式无法实现长久的登录,最终也只好放弃。(我后来也想过每次模拟登录一下获取cookie然后就可以了,但当时我已经知道了selenium,就没有费事搞了)
另一个核心问题就是jira网站的加载方式问题,访问首页时浏览器可以看到提单列表和提单内容,但爬虫爬不到这些数据,经过后期诸多尝试猜测,我认为这是由于加载顺序问题造成的,实际上,jira是分许多个文件依次加载各个模块,而一般原始爬虫代码在接受到一个response后就不会继续接受response了,也不会去拼接这些网站,所以无法爬到完整的数据,因为虽然以提单的id爬虫可以获取到对应的数据,但我却无法很好的获取我要爬的链表。也是因为如此,我开始新的一轮搜索,尝试找到如何应对这种多个response的情况,知道我在知乎上意外看到了selenium这个单词。

学习selenium

selenium是一个完全仿真用户真实运行的工具,可以完成链接的点击,文本框输入,读取数据,甚至拖拽等动作。如此岂不美哉嘛。那我还何必担心会出现登录问题,我直接代码帮我登录就好了嘛。
说干就干,我用两分钟扫了一眼别人的博客,复制运行一下代码完成简单的学习使用。这里我主要参考的是selenium实例,同时根据错误方法进行修改,符合我的环境的语法。有一个不太好的地方就是网上资源大都是针对Chrome或者火狐的,当然Edge经过实践也是完全可以用的,只不过参数需要自己调一下。比如它的options是要用

from selenium.webdriver.edge.options import Options
options = Options()
browser=webdriver.Edge(options = options)

很快我的第一个登录程序就写完了,这个代码还能找回来,就隐去一些信息放出来吧。

try:
        browser=webdriver.Edge(options=options)
        browser.get("要访问的网站")
        input1=browser.find_element(By.ID,value="login-form-username")
        input2=browser.find_element(By.ID,value="login-form-password")
        input1.send_keys("your_user_name")
        input2.send_keys("your_password")
        button=browser.find_element(By.ID,value="login-form-submit")
        button.click()
        browser.implicitly_wait(10)
        print(browser.find_element(By.XPATH,'要爬取数据的XPATH路径').text)
        lis = browser.find_elements(By.CLASS_NAME,'splitview-issue-link')
    finally:
        browser.close()

就这样我分别通过id,class,xpath完成登录并精准获取第一个提单的提交信息和创建时间,但与此同时,我一直不敢面对的第三大核心问题也随之出现了。。。怎么访问近两个月的数据? 相信有人肯定会说直接爬嘛,都知道提单列表了。但别忘了,两个月的提单,就单纯我们组就有数十个,一个一个爬取起码要一两分钟,绝对不可取的。另一个同学用jira API可以直接一次获得,我真实爬取却要依次进行,当然啦,其实也就第一次爬要这么久,以后都爬变化的就好了。但我还是抱有一丝侥幸,觉得多线程可以解决我的问题,在加上我还没有写过python的多线程,所以何不就此学学呢?哪知一入佛门深似海,让我仿佛一切都回到了原点。

多线程加速

最开始我决定参考这篇selenium多线程加速,于是改动代码先开一个线程去访问链表中的一个元素试试,但随后发现一个让我很不爽的问题,由于我要多线程操作多个界面,所以肯定是要开多个新的窗口去访问,可谁知开新的窗口居然不能保存登录状态,直接跳转到登录链接,这可不好啊,如果每次都要登录岂不是太过麻烦(可以开几个线程登录上,然后这几个线程分布访问一部分数据,当然这是后话,我当时想的首先就是保存登录信息)。那这个简单,认证我是不是登录过只需要我的cookie对就可以了嘛,因此我就在网上搜索添加cookie的方法。但不知道是版本原因还是什么情况,加上cookie依然不可以自动登录。我又尝试勾选浏览器自带的记住本电脑,但依然无果。我猜想可能是因为记住本电脑也只是去记录cookie,并不是记录电脑的信息,所以开新的网页时依然会发生要输入密码的问题。

保存浏览器信息

首先搜索的是这篇博客保存浏览器信息,亲测有效果但对于多线程很不友好,每次只能开一个窗口新打开的窗口覆盖原来的,如果开多线程的话会发生多个线程访问的是一个数据,但对于开多个端口的想法测试过,但目前还没有尝试完整,但只是在代码中开多线程启用cmd运行是不对的,最后会发生链接不上,用Telnet测试也是如此,只有某一个端口可以找到。
注:这里补充一些python运行cmd的知识,使用

import os
os.system(your_cmd)

这里要注意的是如果出现路径带有空格,cmd解析会出现问题,必须要把路径用引号引起来才可以。

成功爬取

经过一周的试错,我最终选择了一个目前来看最优的结果。首先创建若干个线程,让每个线程登录,并最终获取列表,在列表中给每个线程分配相同数量的链接任务,并各自打开对应链接的标签(如果是窗口会需要再次登录)。最后获取需要的数据。话不多说,上代码(本代码为安全保密起见,增加了一些数据的隐藏(所有中文部分),无法运行,主要是为了看思路,关键步骤已经增加了注释)

# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.edge.options import Options
from selenium.webdriver.edge.service import Service
from threading import Thread
import subprocess,threading,time
import time,sys,os,copy

thread_list = []
process_list = []
thread_num = 2
process_num = 2
last_herf = "最后要爬取的数据(链接),也可以改变stop去停止程序运行"
stop = 0
create_list = []
lock = threading.RLock() #互斥锁

def create(href,browser):
    href = 'window.open("' + href + '")' 
    print(href)
    browser.execute_script(href) #打开新标签
    browser.implicitly_wait(10)
    browser.switch_to.window(browser.window_handles[-1])
    temp = browser.find_element(By.XPATH,'要获取信息的路径').text
    print(temp)
    lock.acquire()
    create_list.append(temp) #保存
    lock.release()
    browser.switch_to.window(browser.window_handles[0])

def get_jira(num):
    try:
        options = Options()
        # 处理SSL证书错误问题
        options.add_argument('--ignore-certificate-errors')
        options.add_argument('--ignore-ssl-errors')
         
        # 忽略无用的日志
        options.add_experimental_option("excludeSwitches", ['enable-automation', 'enable-logging'])
        options.add_argument('--no-sandbox')
        options.add_argument('--disable-dev-shm-usage')
        options.add_argument('--headless') #无窗口模式运行
        options.add_argument('blink-settings=imagesEnabled=false')
        options.add_argument('--disable-gpu')
        browser=webdriver.Edge(options=options)
        browser.get("你首先要访问的网站(登录网站)")
        browser.implicitly_wait(10)
        input1=browser.find_element(By.ID,value="用户名标签")
        input2=browser.find_element(By.ID,value="密码标签")
        input1.send_keys("你的用户名")
        input2.send_keys("你的密码")
        button=browser.find_element(By.ID,value="登录标签")
        button.click()
        browser.implicitly_wait(10)
        button=browser.find_element(By.XPATH,value='获取所有信息链接的路径')
        button.click() #跳转以进入根网页
        browser.implicitly_wait(10)
        # print_log = open("printlog.html","w")
        # sys.stdout = print_log
        # print(browser.page_source)
        lis = browser.find_elements(By.CLASS_NAME,'获取所有信息形成的链表') #链表用elements
        for i in range(len(lis)):
            # print(lis[i].get_attribute("href"),num)
            if i%thread_num == num: #等额分配任务
                create(lis[i].get_attribute("href"),browser)
            if lis[i].get_attribute("href") == last_herf or stop:
                break
    finally:
        browser.close()

def start_prog():
    for i in range(thread_num): #创建线程
        t = Thread(target=get_jira,args=(i,))
        t.start()
        thread_list.append(t)
    for i in thread_list:
        i.join()

if __name__ == '__main__':
    time_start = time.time()
    start_process()
    for i in create_list:
        print(i)
    time_end = time.time()
    time_c = time_end - time_start
    print('time_cost', time_c, 's')

实际运行

经过我司网站实际爬取的成果显示,爬取32条数据时,使用2-4线程(进程)效果较好,这是因为虽然开多线程(进程)对IO密集型程序来说可以很好的提高速度,但是由于开多线程(进程)时,每次都需要登录(这是我最想取消的一步,但很可惜,始终没有找到一个完美的解决方案)会浪费大量时间。而且根据测试结果得出,jira服务器可能对多线程(进程)实现的不是很优良,以至于我在开多线程时,访问时间并不会明显缩短,相比于单线程,测试表现中,双线程可以节约1/3的时间,四线程可以节约接近1/2时间,8线程可以节约略多于1/2的时间。但开4线程以上的线程时,登录跳转时间已经可以占总运行时间一半以上。因此2-4线程时加速效果比较好,可以加速30%左右。当然,在爬取数据更多时,比如爬取100个数据,得到的结果是:相比于单线程,测试表现中,相比于单线程,双线程可以节约1/2时间,4线程可以节约2/3时间,6线程可以节约5/7时间,8线程可以节约约5/7时间。再计算进入登录过程的时间,可以得出6线程的性能略好于4线程,但性能差异不大。因此使用4-6线程对于爬取100数据来说效果较好。
综上所述,jira对于多线程(进程)有一定的实现,但效果不佳,存在瓶颈上限,当浏览线程数超过2时,性能提升度逐渐下降,4线程并不能达到4倍的提升速率,超过8时,浏览器无法正常运行。在我打开任务管理器进行监控时发现,2线程时的CPU占有率已经达到80%左右。之所以后期有所继续提升,应该是因为本程序介于IO密集型和CPU密集型程序吧。

进一步提速

由于爬虫最后是为了获取数据的,那么加载图片、css和JavaScript等用处就不是很大了,可以参考博客selenium加速爬取来完成加速,实测加速效果可以提速约1.2倍的样子。
当然这还没有结束,由于网页时分层加载的,所以不需要像上面代码所写的使用隐式等待,等到所有html都加载完毕才获取数据,可以使用显式等待。至此,爬虫的爬取速率已经得到了尽可能大的提升。

反思

本次项目至此已经完成了爬取核心部分,做一个简单的移植即可在已有框架中实现了。整个过程充满了跌宕起伏,但也学到了许多东西。对后来人也是一个很好的模板(例如多线程,多进程的使用,以后爬取时可以直接更换网页和对应的查找元素即可)。

怕什么真理无穷,进一寸有一寸欢喜。

你可能感兴趣的:(python,爬虫,学习,selenium)