《网络爬虫开发实战案例》笔记

转自行云博客https://www.xy586.top/

爬虫基础

1.HTTP基本原理

URI和URL

URI: 统一资源标志符

URL: 统一资源定位符

HTTP和HTTPS

HTTP: 超文本传输协议,用于从网络传输超文本数据到本地浏览器的传输协议,它能够保证高效准确的传输文本文档

HTTPS: 以安全为目标的HTTP通道,简单来讲是HTTP的安全版,即HTTP下加入SSL,所简称为HTTPS

HTTPS的安全基础是SSL,因此通过它传输的内容都是经过SSL加密的,它的主要作用可以分为两种:

  1. 建立一个信息安全通道来保证数据传输的安全
  2. 确认网站的安全性,凡是使用HTTPS的网站,都可以通过点击浏览器地址栏的锁头标志来查看网站认证的真实信息

HTTP请求过程

  • 第一列name: 请求的名称,一般会将URL的最后一部分当作名称

  • 第二列status: 响应的状态码

  • 第三列type: 请求的文档信息

  • 第四列initiator: 请求源,用来标记请求是由哪个对象或者进程发起的

  • 第五列size: 从服务器下载的文件和请求的资源大小,如果是从缓存中取得的资源,则该列会显示 from cache

  • 第六列time: 发起请求到获取响应所用的总时间

  • 第七列waterfall: 网络请求的可视化瀑布流

请求

请求,由客户端向服务端发出,可以分为四个部分内容:请求方法、请求的网址,请求头,请求体

  1. 请求方法: 常见的请求方法有两种:GET 和 POST

    GET 和POST 请求方法有如下区别:

    • GET请求的参数包含在URL里面,数据可以在URL中看到,而POST请求的URL不会包含这些数据,数据都是通过表单形式传输的,会包含在请求体中
    • GET请求提交的数据最多只有1024字节,而POST方式没有限制
  2. 请求的网址: URL

  3. 请求头:

    • Accept: 请求报头域,用于指定客户端可接受哪些类型的信息

    • Accept-Language: 指定客户端可接受的语言类型

    • Accept-Encoding: 指定客户端可接受的语言类型

    • Host: 用于指定请求资源的主机 IP 和 端口号,其内容为请求URL 的原始服务器或者网关的位置

    • Cookie: 这是网站为了辨别用户进行会话跟踪而存储在用户本地的数据,他的主要功能就是维持当前访问会话,例如,我们输入用户名和密码成功登录某个网 站后,服务器会用会话保存登录状态信息,后面我们每次刷新或请求该站点的其他页面时, 会发现都是登录状态,这就是 Cookies 的功劳。Cookies 里有信息标识了我们所对应的服务器 的会话,每次浏览器在请求该站点的页面时,都会在请求头中加上 Cookies 并将其发送给服

      务器,服务器通过 Cookies 识别出是我们自己,并且查出当前状态是登录状态,所以返回结 果就是登录之后才能看到的网页内容

    • Referer: 此内容来标识这个请求是从哪个页面发过来的,服务器可以拿到这一信息并做相应的处理,如做来源统计、防盗链处理

    • User-Agent: 简称UA ,他是一个特殊的字符串,可以使服务器识别客户使用的操作系统及版本,浏览器即版本信息,在做爬虫时加上此信息,可以伪装为浏览器,如果不加,很可能会被识别为爬虫

    • Content-Type: 也叫互联网媒体类型,在HTTP协议消息头中,它用来表示具体请求的媒体类信息,例如text/html 代表HTML格式 、application/json 代表 JSON 类型

      因此,请求头是请求的重要组成部分,再写爬虫时,大部分情况下都需要设定请求头

  4. 请求体

    请求体一般承载的内容是POST请求中的表单数据,而对于GET请求,请求体为空

    在爬虫中,如果要构造POST请求需要使用正确的Content-Type。并了解各种请求库的各个参数设置时使用的是哪种Content-Type,不然可能会导致POSt提交后无法正常响应

响应

响应,由服务器端返回给客户端,可以分为但部分:响应状态码、响应头、响应体

  1. 响应状态码

    状态码 描述
    200 服务器正常响应
    404 页面未找到
    500 服务器内部发生错误
  2. 响应头

    响应头包含了服务器对请求的应答信息,如Content-Type、Server、Set-Cookie

    • Date: 标识响应产生的时间
    • Last-Modified: 指定资源的最后修改时间
    • Content-Encoding: 指定响应内容的编码
    • Server: 包含服务器的信息,比如名称、版本号
    • Content-Type: 文档类型,代表返回的数据类型是什么
    • Set-Cookie: 设置Cookies 响应头中的Set-Cookie 告诉浏览器需要将此内容放在Cookies中,下次请求携带Cookies
    • expires: 指定响应的过期时间,可以使代理服务器或浏览器将加载的内容跟新到缓存中,如果再次访问时,就可以直接从缓存中加载,降低服务器负载,缩短加载时间
  3. 响应体

    最重要的就是响应体,响应的正文数据都在响应体中

2.网页基础

网页的组成

  • HTML: 超文本标记语言

  • CSS: 层叠样式表,样式指网页中文字大小、颜色、元素间距、排列等格式

  • JavaScript: 脚本语言,时用户与信息之间不只是一种浏览与显示的关系,而是实现了一种实时、动态、交互的页面功能

    综上所述,HTML定义了网页的内容和结构,CSS描述了网页的布局,JS定义了网页的行为

网页的结构

  • 节点树及节点间的关系:

    在HTML中,所有标签定义的内容都是节点,他们构成了一个HTML DOM 树

    DOM: 文档对象模型,它允许程序和脚本动态的访问和更新文档的内容、结构、样式

  • 选择器:

爬虫的基本原理

​ 我们可以把互联网比作一张大网,而爬虫便是在网上爬行的蜘蛛,把网的节点比作一个个网页,爬虫爬到这就相当于访问了该页面,获取了其信息,可以把节点之间的连线比作网页与网页之间的连接关系,这样蜘蛛通过一个节点,可以顺着节点连线继续爬到下一个节点,网站的数据就可以被抓取下来了

  • 爬虫概述:

    1. 获取网页
    2. 提取信息
    3. 保存数据
    4. 自动化程序
  • JavaScript渲染页面:

    有时候我们在用urllib 或 requests 抓取网页时,得到的源代码实际和浏览器中看到的不一样,但是在用 lib request 等库请求当前页面时,我们得到的只是这个 HTML 码,它不会帮助 我们去继续加载这个 JavaScript 文件,这样也就看不到浏览器中的内容了,因此,使用基本 HTTP 请求库得到的源代码可能跟浏览器中的页面源代码不太一样 对于这样的情 况,我们可以分析其后台 Ajax 接口,也可使用 Se nium Splash 这样的库来实现模拟 JavaScript 渲染

会话的基本原理

  • 无状态HTTP

    HTTP协议对事务处理是没有记忆功能的,也就是说服务器不知道客户端是什么状态。

    这时两个用于保持 HTTP 接状态的技术就出现了,它 分别是会话和 Cookies 会话在服务端。

    也就是网站的服务器,用来保存用户的会话信息; Cookies 在客户端,也可以理解为浏览器端,有

    Cookies ,浏览器在下次访问网页时会自动附带上它发送给服务器,服务器通过识别 Cookjes 并鉴定出

    是哪个用户,然后再判断用户是否是登录状态,然后返回对应的响应。

    我们可以理解为 Cookies 里面保存了登录的凭证,有了它,只需要在下次请求携带 Cookies 发送

    请求而不必重新输入用户名、密码等信息重新登录了。

    因此在爬虫中,有 候处理需要登录才能访问的页面时,我们一般会直接将登录成功后获取的

    Cookies 放在请求头里面直接请求,而不必重新模拟登录。

  • 属性结构

    • name: Cookie的名称,一旦创建,该名称便不可更改
    • value: 改Cookie的值,如果值为Unicode字符,需要为字符编码。如果值为二进制数据,则需要使用BASE64 编码
    • domain: 可以访问改Cookie的域名,例如,如果设置为.zhihu.com ,则所有以 zhihu.com 结尾的域名都可以访问该Cookie
    • Max Age: 该Cookie失效的时间
    • path: 该Cookie 的使用路径,如果设置为 /,则本域名下的所有页面都可以访问该Cookie
  • 会话 Cookie 和持久 Cookie

    从表面意思来说,会话 Cookie 就是把 Cookie 放在浏览器内存里,浏览器在关闭之后该 Cookie 失效 持久 Cookie 会保存到客户端的硬盘中,下次还可以继续使用,用于长久保持用户登录状态 其实严格来说,没有会话 Cookie 和持久 Cookie 分,只是由 ookie Max Age Expires 字段 决定了过期的时间

代理的基本原理

我们在做爬虫的过程巾经常会遇到这样的情况 最初爬虫正常运行,正常抓取数据,一切看起来 都是那么美好,然 杯茶的功夫可能就 出现错误,比如 403 Forbidden 这时候打开网页一看 ,可 能会看到“您的 IP 访问频率太高”这样的提示 出现这种现象的原因是网站采取了一些反爬虫措施 比如,服务器会检测某个 IP 在单位时间内的请求次数,如果超过了这个阔值,就会直接拒绝服务,返 问一些错误信息,这种情况可以称为封 IP,既然服务 检测的是某个 IP 单位时间的请求次数,那么借助某种方式来伪装我们的 IP ,让服 器识别不出是由我们本机发起的请求

  • 代理的作用:

    1. 突破自身IP的访问限制,访问一些平时不能访问的站点
    2. 访问一些单位或团体内部资源:比如使用教育网内地址段免费代理服务器,就可以用于对教育网开放的各类FTP下载上传,以及各类资料查询共享服务
    3. 提高访问速度,通常代理服务器都设置一个较大的硬盘缓冲区,当外界的信息通过时,同时也将其保存到缓冲区中,当其他用户在访问相同的信息时,则直接由缓冲区取出信息,传给用户,以提高访问速度
    4. 隐藏真实的IP,上网者也可以通过这种方式隐藏自己的IP,免受攻击,对于爬虫来说,我们用代理就是为了隐藏自身IP,防止自身的IP被封锁
  • 爬虫代理

    对于爬虫来说,由于爬虫爬取速度过快,在爬取过程中可能遇到同 IP 访问过于频繁的问题,

    此时网站就会让我们输入验证码登录或者直接封锁 ,这样会给爬取带来极大的不便

    使用代理隐藏真实的 IP ,让服务器误以为是代理服务器在请求向自己 这样在爬取过程中通过不断

    更换代理,就不会被封锁,可以达到很好的爬取效果

  • 代理分类

    1. 根据协议区分
      • FTP代理服务器,主要用于访问FTP服务器,一般有上传、下载、缓存功能,端口一般为 21 、2121 等
      • HTTP 代理服务器:主要用于访问网页,一般有内容过滤和缓存功能,端口一般为 80 、8080、3128
      • SSL/TLS:主要用于访问加密网站,一般有SSL 或者 TLS 加密功能(最高支持128位加密强度),端口一般为443
      • RTSP代理:主要用于访问Real流媒体服务器,一般有缓存功能,一般端口为554
      • Telnet代理:主要用于telnet 远程控制(黑客入侵计算机时常用于隐藏身份),端口一般为23
      • POP3/SMTOP代理:主要用于POP3/SMTP方式收发邮件,一般有缓存功能,端口一般为110、25
      • SOCKS代理:只是单纯传递数据包。不关心具体协议和用法,所有速度很快,端口一般为1080
    2. 根据匿名程度区分
      • 高度匿名代理:会将数据包原封不动的转发,在服务器看来就好像是一个真正的普通客户端在访问,而记录的IP是代理服务器的IP
      • 普通匿名代理:会在数据包上做一些改动,服务端有可能发现这是个代理服务器,也有一定几率追查到客户端的真实IP
      • 透明代理:不但改动了数据包,还会告诉服务器客户端的真实IP,这种代理除了能用缓存技术提高浏览速度,能用内容过滤提高安全性能之外,并无其他显著作用,最常见的例子就是内网中的硬件防火墙
  • 常见的代理设置

    • 使用网上的免费代理: 最好使用高匿名代理,另外可用的代理不多,需要在使用前筛选一下可用代理,也可以进一步维护一个代理池
    • 使用付费代理服务: 质量比免费代理还很多
    • ADSL拨号: 播一次号换一次IP ,稳定性高,也是一种比较有效的解决方案

3.requests库的基本使用

基本用法

  • GET请求:

    import requests
    r = requests.get(’http://httpbin.org/get‘)
    
    """
    输出结果:
    {
      "args": {}, 
      "headers": {
        "Accept": "*/*", 
        "Accept-Encoding": "gzip, deflate", 
        "Host": "httpbin.org", 
        "User-Agent": "python-requests/2.22.0"
      }, 
      "origin": "220.202.133.149, 220.202.133.149", 
      "url": "https://httpbin.org/get"
    }
    """
    

    r . json()方法:将返回的结果是JSON格式的字符串转为一个字典格式

    一般要在headers上加上User-Agent信息

    headers = {
                  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36"
              }
    
  • POST请求:

    import requests
    data = {
        'name':'donmo',
        'age' : 20
    }
    r = requests.post('http://httpbin.org/post',data = data)
    print(r.text)
    
    """
    {
      "args": {}, 
      "data": "", 
      "files": {}, 
      "form": {
        "age": "20", 
        "name": "donmo"
      }, 
      "headers": {
        "Accept": "*/*", 
        "Accept-Encoding": "gzip, deflate", 
        "Content-Length": "17", 
        "Content-Type": "application/x-www-form-urlencoded", 
        "Host": "httpbin.org", 
        "User-Agent": "python-requests/2.22.0"
      }, 
      "json": null, 
      "origin": "220.202.133.149, 220.202.133.149", 
      "url": "https://httpbin.org/post"
    }
    我们可以成功获取返回结果,其中form部分就是提交的数据,这就证明了post请求成功发送了
    """
    
    
  • 响应

    import requests
    headers = {
                  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36"
              }
    r = requests.get('https://www.jianshu.com/',headers = headers)
    print(type(r.status_code))
    print(type(r.headers))
    print(type(r.cookies))
    print(type(r.url))
    print(type(r.history))
    
    """
    
    
    
    
    
    """
    

高级用法

  1. 文件上传:

    我们可以知道requests可以模拟提交一些数据,假如有的网站需要上传文件,我们也可以用它来实现

    import requests
    files = {
        'file':open('爬取的资源/favicon.ico','rb')
    }
    r = requests.post('http://httpbin.org/post',files = files)
    print(r.text)
    
    """
    {
      "args": {}, 
      "data": "", 
      "files": {
        "file": "data:application/octet-........
      }, 
      "form": {}, 
      "headers": {
        "Accept": "*/*", 
        "Accept-Encoding": "gzip, deflate", 
        "Content-Length": "6665", 
        "Content-Type": "multipart/form-data; boundary=bcf321b8c23ed3ffec5c8c2ea30f3c40", 
        "Host": "httpbin.org", 
        "User-Agent": "python-requests/2.22.0"
      }, 
      "json": null, 
      "origin": "220.202.133.149, 220.202.133.149", 
      "url": "https://httpbin.org/post"
    }
    里面包含 files 这个字段,而 form 字段是 的,这证明文件上传部分会单独有一个 files 字段来标识
    
    """
    
  2. Cookies

    import requests
    r = requests.get('https://www.baidu.com')
    print(r.cookies)
    for key,value in r.cookies.items():
        print(key + "=" + value)
        
    """
    ]>
    BDORZ=27315
    
    这里我们首先调用 Cookies 属性即可成功得到 Cookies 可以发现它 RequestCookiJar 类型
    然后 items()方法将其转为元组组成的列表  遍历输出每一个 Cookie 名称和值  实现Cookie
    的遍历解析
    """
    
  3. 会话维持

  4. 代理设置

正则表达式

模式 描述
\w 匹配字母,数字及下划线
\W 匹配不是字母,数字及下划线
\s 匹配任意空白字符,等价于【\t \n \r\ \f】
\S 匹配任意非空字符
\d 匹配任意数子 等价于【0-9】
\D 匹配任意非数字的字符
\A 匹配字符串开头
\Z 匹配字符串结尾,如果存在换行,只匹配换行前的结束字符
\z 匹配字符串结尾,如果存在换行,同时还会匹配换行符
\G 匹配最后完成匹配的位置
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配一行字符串的开头
$ 匹配一行字符串的结尾
. 匹配任意字符,除了换行
[…] 用来表示一组字符,单独列出,比如[amk] 匹配a、m、或 k
[^…] 不在[ ]中的字符
* 匹配0 个 或多个表达式
+ 匹配一个或多个表达式
匹配 0 个或 1 个前面的正则表达式定义的片段,非贪婪方式
{n} 精确匹配 n 个前面的表达式
{n,m} 匹配 n 到 m 次由前面正则表达式定义的片段,贪婪方式
a|b 匹配 a 或 b
( ) 匹配括号内的表达式,也表示一个组
  • match(): 向他传入要匹配的字符串以及正则表达式,从字符串的开头开始匹配
  • 贪婪匹配:尽可能匹配多得字符
  • 非贪婪匹配:尽可能匹配少的字符
  • search(): 扫描整个字符串,然后返回第一个成功匹配的结果
  • findall(): 该方法会搜索整个字符串,然后返回匹配正则表达式的所有内容,有结果就是列表类型
  • sub(): 可以借助他来修改文本,第一个参数传入匹配的内容,第二个参数为替换的字符串,第三参数是原字符串
  • compile(): 将正则字符串编译成正则表达式对象,以便在后面的匹配中复用

4.解析库的使用

使用XPath

XPath:全称XML Path Language ,即XML 路径语言,是一门在XML 文档中查找信息的语言

官网:[https://www.w3.org/TR/xpath/]( XPath 官方文档)

  1. XPath 概览: 几乎所有我们想要定位的节点,都可以用XPath 来选择

  2. XPath 常用规则:

    表达式 描述
    nodename 选取此节点的所有子节点
    / 从当前节点选取直接子节点
    // 从当前节点选取子孙节点
    . 选取当前节点
    选取当前节点的父节点
    @ 选取属性
  3. XPath 常用匹配规则:

    //title[@lang = ‘eng’] : 代表选择所有名称为 title,同时属性 lang 的值为 eng 的节点

  4. 实例引入:

    from lxml import etree
    text = ' html .....'
    html = etree.HTML(text)
    result = etree.tostring(html)  # 这结果是btyes 类型 可以利用decode('utf-8') 转成str 类型
    
    • # 也可以直接读取文本文件进行解析
      from lxml import etree
      html = etree.parse('./test.html',etree.HTMLParse())
      result = etree.tostring(html)
      
  5. 文本的获取: text()

  6. 属性值多匹配: contains() 函数

    from lxml import etree
    text = '
  7. ...
  8. '
    html = etree.HTML(text) result = html.xpath('//li[contains(@class,"li")]')
  9. 多属性匹配: 根据多个属性确定一个节点,这时就需要同时匹配多个属性,此时可以使用 and 来连接

    from lxml import etree
    text = '
  10. ...
  11. '
    html = etree.HTML(text) result = html.xpath('//li[contains(@class,"li") and @name = "item"]')
  12. 按序选择: 我们在选择的时候可能同时匹配了多个节点,但只想要其中的某个节点

    result1 = html.xpath('//li[1]')  #选取第一个li 节点    li[1]
    result2 = html.xpath('//li[last()]')  #选取最后一个 li 节点   last()
    result3 = html.xpath('//li[position()<3]')  #选取位置小于3 的li 节点 也就1 2 节点  position()<3
    result4 = html.xpath('//li[last() - 2]')  #选取倒数第三个 li 节点  last() -2
    
  13. 节点轴选择:

    轴名称 描述
    //li[1]/ancestor: 匹配第一个li 节点的所有祖先节点
    //li[1]/ancestor::div 只有div 这个祖先节点
    //li[1]/attribute: 第一个li 节点的所有属性值
    //li[1]/child::a[@href = “link.html”] 获取 href 属性为link.html 的所有直接a 子节点
    //li[1]/descendant::span 获取所有的子孙节点 所有的span节点
    //li[1]/following:[2] 获取当前节点之后的所有节点 这里只获取第二个后续节点
    //li[1]/following - sibling:: * 获取当前结点之后的所有同级节点

使用Beautiful Soup

Beautiful Soup : 借助网页的结构和属性等特性来解析网页

  1. 解析器:

    Beautiful Soup 在解析式 实际上依赖解析器,lxml 解析器有解析 HTML 和 XML 的功能, 而且速度快,容错能力强,所以推荐使用它

  2. 基本用法:

    import requests
    from bs4 import BeautifulSoup
    
    response = requests.get('https://zhidao.baidu.com/question/268587066.html')
    response.encoding = 'gbk'
    html = response.text
    soup = BeautifulSoup(html,'lxml')
    print(soup.prettify())
    print(soup.title.string)
    
    # prettify()方法: 把要解析的字符串以标准的缩进格式输出
    # soup.title.string:  输出 HTML 中 title 节点的文本内容
    
  3. 节点选择器: 直接调用节点的名称就可以选择节点元素,在调用string属性就可以得到节点的文本内容

    • 选择元素

      print(soup.title)
      print(type(soup.title))
      print(soup.title.string)
      
      '''
      输出:
      
      什么是子节点_百度知道
      
      什么是子节点_百度知道
      '''
      
    • 提取信息

      1. 获取名称:利用name 属性来获取节点的名称

        print(soup.title.name)    # title
        
      2. 获取属性 : 调用 attrs 获取所有属性

        print(soup.p.attrs)
        print(soup.p.attrs['name'])
        
        '''
        输出:
        {'class':['title'],name = 'dromouse'}
        dromouse
        '''
        
      3. 获取内容

        print(soup.p.string)
        
        #注意: 这里选择到的 p 节点是第一个 p 节点,获取文本也是第一个 p 节点里面的文本
        
    • 嵌套选择:

      html = '''
      			
      				The Dromouse's stroy
      			
      		 '''
      from bs4 import BeautifulSoup
      soup = BeautifulSoup(html,'lxml')
      print(soup.html.title)
      print(soup.html.title.string)
      
      '''
      输出:
      The Dromouse's stroy
      The Dromouse's stroy
      '''
      
    • 关联选择:

      1. 子节点和子孙节点 :

        • 选取节点元素之后,如果想要获取它的直接子节点,可以调用 contents 属性

        返回的结果是列表形式,包含文本 又包含节点,列表中的每个元素都是p 节点的直接子节点,所以说,contents 属性得到的结果是直接子节点的列表,

        • 同样的 可以调用 children 属性得到相应的结果, 但返回的结果是生成器类型
        • 如果要得到所有的子孙节点的话,可以调用 descendants 属性 返回结果是生成器,递归查询所有的子节点,得到所有的子孙节点
      2. 父节点和祖先节点

        如果获取某个节点元素的父节点,可以调用 parent 属性 ,注意: 这里得到的只是直接父节点,而没有再向外寻找父节点的祖先节点,如果想要获取所有的祖先节点, 可以调用 parents属性

      3. 兄弟节点

        • next_sibling ; 获取节点的下一个兄弟元素
        • previous_sibling : 获取节点的上一个兄弟元素
        • next_siblings : 获取节点的下面所有兄弟元素
        • previous_siblings : 获取节点的上面所有兄弟元素
      4. 提取信息 :string 、attrs 属性 获得 其文本和属性

      5. 方法选择器

        • find_all() :

          传入一些属性 或 文本 ,就可以得到符合条件的元素

          1. soup.find_all(name = ‘li’)

          2. soup.find_all( attrs = {‘id’: ‘list-1’ }) 参数为字典的格式

            一些常用的属性,可以不用 attrs 来传参 soup.find_all( id = ‘list1’) soup.find_all( class_ = ‘elemen’)

        • find(): 返回单个元素,也就是第一个匹配的元素

        • find_parents() 和 find_parent()

        • find_next_siblings() 和 find_next_sibling()

        • find_previous_siblings() 和 find_previous_sibling()

    • CSS 选择器

      只需要调用select()方法 ,传入相应的CSS 选择器即可

使用pyquery

  1. 初始化: 它的初始化有多种,传入字符串、URL、文件名、等等

  2. 基本CSS 选择器

  3. 查找节点:

    这些函数和jQuery 中函数的用法完全相同

    • 子节点: 用法哦find() 方法,传入的参数是 CSS 选择器 ,查找的是所有的子孙节点

      ​ 用 children()方法 只查找子节点 ,可以传入CSS 选择器

    • 父节点: 用parent()方法获取某个节点的父节点,直接fujiedian

      用parents() 方法可以获得节点的祖先节点

    • 兄弟节点: 使用siblings()方法 获取兄弟节点

  4. 遍历:

    pyquery 的选择结果可能是多个节点,也可能是单个节点,并没有返回像Beautiful Soup 那样的列表

    lis = doc('li').items()
    for li in lis:
        print(li)
        
    # 调用items()方法后,会得到一个生成器
    
  5. 获取信息:

    • 获取属性:提取到某个 pyquery 类型的节点后,就可以调用attr()方法来获取属性

      ​ print(a.attr.href) print(a…attr(‘href’))

      当返回结果包含多个节点时,调用 attr()方法,只会得到第一个节点的属性,那么这个情况,如果想要获取所有的节点的属性,那么就要遍历了

    • 获取文本: 调用 text()方法来实现节点内部的文本信息,返回纯文字内容

      ​ html()方法 会返回节点的所有HTML文本

      ​ 但如果返回多个节点的时候想要获取文本的时候,text()直接返回多个文本信息,中间用空格隔开,而html() 则需要遍历才能拿到所有的文本信息

  6. 节点操作:

    pyquery提供了一系列方法来对节点进行动态修改,比如为某个节点添加一个class ,移除某个节点等,这些操作回味提取信息带来极大的方便

    • addClass 和 removeClass : 动态改变节点的 class属性

    • attr、text 和 html:attr() 对属性进行操作,

      text():节点内的全部文本改为传入的字符串文本

      html():节点内的全部文本变成传入的HTML 文本

      所以说 ,attr(),text(),html()只传入一个参数 就是获取这个参数的属性值,传入两个参数,就可以来修改内容

    • remove :remove()就是移除

  7. 伪类选择器:

5.数据存储

文件存储

TXT文档存储

open():读一个参数为目标文件的名称,第二个参数为打开方式,第三个参数可以设置为字符编码

open(‘info.txt’,‘a’,‘utf-8’)

  1. 文件打开方式 :

    • r :以只读的方式打开文件,文件的指针会放在文件的开头,这是默认模式
    • rb : 以二进制只读方式打开一个文件
    • r+ :以读写方式打开一个文件,文件指针放在文件的开头
    • rb+ : 以二进制读写方式打开一个文件
    • w :以写入方式打开一个文件,如果该文件已存在,则将其覆盖,如不存在将创建其文件
    • wb :以二进制写入方式打开一个文件
    • w+ :以读写方式打开一个文件,如果该文件已存在,则将其覆盖,如不存在将创建其文件
    • wb+ :以二进制读写方式打开一个文件
    • a :以追加方式打开一个文件,文件指针放在文件的末尾,文件如果不存在将会创建新的文件来写入
    • ab :以二进制追加方式打开一个文件
    • a+ : 以读写方式打开一个文件,文件指针放在文件的末尾
    • ab+ :以二进制追加方式打开一个文件
  2. 简化写法

    with open('info','a',encoding = 'utf-8') as file:
        file.write('hello word')
    

JSON文件存储

​ JSON:JavaScript 对象标记

  • 对象:在JavaScript中是使用 {} 包裹起来的内容,数据结构为:{key1:value1,key2:value2,…} 的键值对结构,在面向对象的语言中,key 为对象的属性,value 为对应的值

  • 数组:数组在JavaScript 中是 [] 包裹起来的内容,数据结构 [“java”,“JavaScript”,…] 的索引结构

    import json
    str = '''
        [{
            "name":"Bob",
            "age":20,
            "sex":"男"
        },
        {
            "name":"Rose",
            "age":"18",
            "sex":"女"
        }
        ]
    '''
    data  = json.loads(str)
    print(data)
    
    # 输出
    [{'name': 'Bob', 'age': 20, 'sex': '男'}, {'name': 'Rose', 'age': '18', 'sex': '女'}]
    

注意: JSON字符串的表示需要用双引号,否则loads()方法会解析失败

从JSON文本中读取内容:

with open("resource/info.json",'r',encoding='utf-8') as file:
    str = file.read()
    data = json.loads(str)
    print(data)

输出JSON:

​ 调用dumps()方法将JSON对象转化为字符串

import json
str = [{
        "name":"Bob",
        "age":20,
        "sex":"男"
    },
    {
        "名字":"东魔",
        "年龄":"20",
        "性别":"男"
    }
    ]

with open('resource/info.json','w',encoding='utf-8') as file:
    file.write(json.dumps(str,ensure_ascii=False,indent = 2))
    print('写入成功')
    
# 注意:在文件打开 open() 时加上以 'utf-8'编码,在dump()时也要加上 ensure_ascii = False
# 不然会变成ascii 码写到json 文件中
# indent参数:  代表缩进字符的个数 

这里dumps()是将 dict 转换成 str 格式 , loads() 是将str 转换成 dict 格式

CSV文件存储

CSV:Comma-Separated Values ,中文叫做 逗号分隔值,其文件以纯文本形式存储数据

import csv
with open('resource/data.csv','w') as csvfile:
    writer = csv.writer(csvfile)
    writer.writerow(['id','name','age'])
    writer.writerow(['1001','Mike',20])
    writer.writerow(['1002','Bob',18])
    writer.writerow(['1003','rose',15])

如果想修改列与列之间的分隔符,可以传入delimiter参数 delimiter = ’ ’

字典的方式写入

import csv
with open('resource/data.csv','w') as csvfile:
    fieldnames = ['id','name','age']
    writer = csv.DictWriter(csvfile,fieldnames = fieldnames)
    writer.writeheader()
    writer.writerow({'id':'1001','name':'xiaoming','age':'18'})
    writer.writerow({'id':'1002','name':'xiaohua','age':'20'})
    writer.writerow({'id':'1003','name':'xiaowang','age':'22'})

读取:

import csv
with open('resource/data.csv','r',encoding='utf-8') as csvfile:
    reader = csv.reader(csvfile)
    for row in reader:
        print(row)
  • pandas 中的read_csv() 方法读取csv文件

    import pandas as pd
    
    df = pd.read_csv('resource/data.csv')
    print(df)
    

MySQL的存储

  1. 连接内网服务器上宝塔上的MySQL数据库

    import pymysql
    db = pymysql.connect(host='172.20.32.111',user='donmo',password='47AdmG3hke7PTkzc',port=3306)
    cursor = db.cursor()
    cursor.execute('SELECT VERSION()')
    data = cursor.fetchone()
    print(data)
    
  2. 创建表

    import pymysql
    db = pymysql.connect(host='172.20.32.111',user='donmo',password='47AdmG3hke7PTkzc',port=3306,db='donmo')
    cursor = db.cursor()
    # 创建表
    sql = '''create table if not exists students(
                id varchar(255) not null ,
                name varchar(255) not null ,
                age int not null ,
                primary key (id)
    )'''
    cursor.execute(sql)
    db.close()
    
  3. 插入数据

    # 插入数据
    id = '201706030120'
    name = '东魔'
    age = 20
    sql = 'insert into students(id,name ,age) values (%s,%s,%s)'
    try:
        cursor.execute(sql,(id,name,age))
        db.commit()
    except:
        db.rollback()
    
    db.close()
    
  4. 事务的4个属性

    属性 解释
    原子性(atomicity) 事务是不可分割的工作单位,事务包括的诸多操作要么都做,要么都不做
    一致性(consistency) 事务必须使用数据库从一个一致性状态变到另一个一致性状态,一致性与原子性是密切相关的
    隔离性(isolation) 一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间互不干扰
    持久性 一个事务一旦提交,他对数据库的改变应该是永久性的,接下来的其他操作或者故障不因该对其有任何影响
  5. 动态插入数据

    很多情况下,我们要达到的效果就是插入方法无需改动,做成一个通用的方法,只需要传入一个动态的字典就好了

    # 动态字典插入数据
    data = {
        'id':'201706030102',
        'name':'刘湘云',
        'age':20
    }
    table = 'students'
    keys = ','.join(data.keys())
    values = ','.join(['%s']*len(data))
    sql = f'insert into {table}({keys}) values ({values})'
    
    try:
        if cursor.execute(sql,tuple(data.values())):
            print("success")
            db.commit()
    except:
        print('failed')
        db.rollback()
    
    db.close()
    

    INSERT INTO students (id,name,age)VALUES (%s,%s,%s)

    因此我们就达到实现传入一个字典来插入数据的方法,不需要再去修改SQL语句和插入操作了

  6. 更新数据

    • # 更新数据
      sql = 'update students set age=%s where name =%s'
      try:
          if cursor.execute(sql,(18,'东魔')):
              print('sucess')
              db.commit()
      except:
          print('filed')
          db.rollback()
      
      
    • 实际情况下,我们关心的是会不会出现重复的数据,如果出现了,我们希望的是更新数据,而不是重复保存一次,所以可以实现一种去重的方法

      # 去重更新数据
      data = {
          'id':'201706030120',
          'name':'donmo',
          'age':20
      }
      table = 'students'
      keys = ','.join(data.keys())
      values = ','.join(['%s']*len(data))
      sql = f'insert into {table}({keys}) values ({values}) on duplicate key update '
      update = ','.join([f"{key} = %s" for key in data])
      sql += update
      
      try:
          if cursor.execute(sql,tuple(data.values())*2):
              print('success')
              db.commit()
      except:
          print('field')
          db.rollback()
      

      insert into students(id,name,age) values (%s,%s,%s) on duplicate key update id = %s,name = %s,age = %s

      ON DUPLICATE KEY UPDATE :如果主键已存在,就执行更新操作,如此一来,就可以实现主键不存在便插入数据,主键存在就更新数据

  7. 删除数据

    # 删除数据
    table = 'students'
    condition = 'age<20'
    sql = f'delete from {table} where {condition}'
    try:
        cursor.execute(sql)
        db.commit()
    except:
        db.rollback()
    
  8. 查询数据

    • sql = 'select * from students'
      try:
          cursor.execute(sql)
          print('count:',cursor.rowcount)  # 获取查询结果的条数
          one = cursor.fetchone()  #  返回结果的第一条数据
          print('one:',one)
          result = cursor.fetchall() #  得到结果的所有数据   结果为 二重元组
          print('all:',result)
          for row in result:
              print(row)
      except:
          print('error')
          
      # fetchall()  内部实现有一个偏移指针来指向查询结果,去一次后就往下移一位,因此用了一次fetchone()  再用fetchall() 数据会少一条
      
    • 如果数据量很大,那么fetchall()方法占用的开销会非常高,因此推荐如下方法逐条取数据

      # 逐条查询数据
      sql = 'select * from students'
      try:
          cursor.execute(sql)
          print('count:',cursor.rowcount)
          row = cursor.fetchone()
          while row:
              print('Row:',row)
              row = cursor.fetchone()
      except:
          print('error')
      

非关系型数据库存储

MongoDB存储:

Mongodb: 由C++ 语言编写的非关系型数据库,是一个基于分布式文件存储的开源数据库系统,其内容存储形式类似 JSON 对象,它的字段值可以包含其他文档,数组,及文档数组。非常灵活

  1. 连接MongoDB

    ​ 用到pyMongo 库里面的 MongoClient ,一般来说,传入MongoDB 的IP 及端口即可

    import pymongo
    # 连接MongoDB
    client = pymongo.MongoClient(host='172.20.32.111',port=27017)
    
    # 指定数据库
    db = client.mongo_donmo
    # db = client['donmo'] 方法二
    
    # 指定集合  类似于关系型数据库中的表
    collection = db.students
    # collection = db['students']   方法二
    
    
  2. 插入数据

    官方推荐使用inert_one 和 insert_many 方法来插入单条记录和多条记录

    • import pymongo
      # 连接MongoDB
      client = pymongo.MongoClient(host='172.20.32.111',port=27017)
      
      # 指定数据库
      db = client.mongo_donmo
      # db = client['donmo'] 方法二
      
      # 指定集合  类似于关系型数据库中的表
      collection = db.students
      # collection = db['students']   方法二
      
      # 插入单条数据
      student = {
          'id': '201706030120',
          'name':'东魔',
          'age':20,
          'gender':'male'
      }
      result = collection.insert_one(student)
      print(result)
      print(result.inserted_id)
      
    # 插入多条数据
    student = {
        'id': '201706030124',
        'name':'黄家伟',
        'age':20,
        'gender':'male'
    }
    student1 = {
        'id': '201706030105',
        'name':'老蔡',
        'age':20,
        'gender':'male'
    }
    result = collection.insert_many([student,student1])
    print(result)
    print(result.inserted_ids)
    
    # 
    # [ObjectId('5ddf48b7587b3e553dec7735'), ObjectId('5ddf48b7587b3e553dec7736')]
    
  3. 查询

    我们可以利用find_one 或者find 方法来进行查询,find_one 查询得到的是单个结果,find 方法返回的是一个生成器对象

    • # 查询数据
      result  = collection.find_one({'name':"赵波"})
      print(type(result))
      print(result)
      
      # 
      # {'_id': ObjectId('5ddf46993c11ac435633f3d2'), 'id': '201706030134', 'name': '赵波', 'age': 20, 'gender': 'male'}
      # 这里的 _id 属性,是MongoDB 在插入过程中自动添加的
      
    results = collection.find({'age':20})
    print(results)
    for result in results:
        print(result)
        
    <pymongo.cursor.Cursor object at 0x03E7C4B0>  # 返回的结果是 Cursor  相当于一个生成器
    # {'_id': ObjectId('5ddf46993c11ac435633f3d2'), 'id': '201706030134', 'name': '赵波', 'age': 20, 'gender': 'male'}
    # {'_id': ObjectId('5ddf47b6bf9cb333f31b4408'), 'id': '201706030120', 'name': '东魔', 'age': 20, 'gender': 'male'}
    # {'_id': ObjectId('5ddf48a86269c96348b19455'), 'id': '201706030124', 'name': '黄家伟', 'age': 20, 'gender': 'male'}
    
    
    • 比较符号

      符号 含义 示例
      $lt 小于 {‘age’:{’$lt’:20}}
      $gt 大于 {‘age’:{’$gt’:20}}
      $lte 小于等于 {‘age’:{’$lte’:20}}
      $gte 大于等于 {‘age’:{’$gte’:20}}
      $ne 不等于 {‘age’;{’$ne’:20}}
      $in 在范围内 {‘age’:{’$in’:[20,30]}}
      $nin 不在范围内 {‘age’:{’$nin’:[20,30]}}
    • 正则查询

      符号 含义 示例 示例含义
      $regex 匹配正则表达式 {‘name’:{’$regex’:’^M,*’}} name 以M开头
      $exists 属性是否存在 {‘name’:{’$exists’:True}} name 属性存在
      $type 类型判断 {‘age’:{’$type’:‘int’}} age 的类型为 int
      $mod 数字模操作 {‘age’:{’$mod’:[5,0]}} age 取模5 为 0
      $text 文本查询 {‘KaTeX parse error: Expected '}', got 'EOF' at end of input: text':{'search’:‘Mike’}} text 类型的属性包含Mike字符串
      $where 高级条件查询 {’$where’:‘obj.fans_count == obj.follows_count’} 自身粉丝数等于关注数

      MongoDB 官方文档:https://docs.mongodb.com/manual/reference/operator/query/

  4. 计数

    要统计查询结果有多少条数据,可以调用count() 方法

    count = collection.find().count()
    print(count)
    
  5. 排序

    直接调用sort() 方法,在其中传入排序的字段及升序的标志即可

    pymongo.ASCENDING: 指定升序排列

    pymongo.DESCENDING:指定降序排列

    # 排序
    results = collection.find().sort('id',pymongo.ASCENDING)
    print([result['name'] for result in results])
    
    # ['老蔡', '东魔', '黄家伟', '赵波']
    
  6. 偏移

    在某些情况下,我们可能只想取某几个元素,这时就可以利用 skip() 方法偏移几个位置

    results = collection.find().sort('id',pymongo.ASCENDING).skip(2)
    print([result['id'] for result in results])
    
    # ['黄家伟', '赵波']
    
    # limit() 方法指定要取结果的个数
    results = collection.find().sort('id',pymongo.ASCENDING).skip(2).limit(1)
    print([result['id'] for result in results])
    
    # ['黄家伟']
    

    注意: 在数据库数量非常庞大的时候,最好不要用偏移量来查询数据,因为这样可能导致内存溢出

    from bso.objectid import ObjectId
    results = collection.find({'_id':{'$gt':ObjectId('5ddf46993c11ac435633f3d2')}})
    
    # 这是需要记录好上次查询的Id 
    
  7. 更新

    对于数据更新,我们可以使用update()方法,指定更新的条件和更新后的数据即可

    matched_count: 获得匹配的条数

    modified_count:获得影响的条数

    • condition = {'age':{'$in':[20]}}
      result = collection.update_many(condition,{'$inc':{'age':18}})
      print(result)
      print(result.matched_count,result.modified_count)
      
      # 年龄为20的 每条数据都加18
      
    condition = {'name': '老蔡'}
    student = collection.find_one(condition)
    student['name'] = '戴徐坤'
    result = collection.update_one(condition,{'$set':student})
    print(result)
    print(result.matched_count,result.modified_count)
    
  8. 删除

    直接调用 remove() 方法指定删除条件即可,符合条件的所有数据均会被删除

    官方推荐:

    delete_one : 删除第一条符合条件的数据

    delete_many: 删除所有符合条件的数据

    result = collection.delete_one({'name':'huanxi'})
    print(result)
    
  9. 其他操作

    另外,PyMongo 还提供了一些组合方法,find_one_and_delete()、find_one_and_replace()、find_one_and_update()

    PyMongo 的详细用法文档

Redis存储:

Redis: 是一个基于内存的高效键值对型非关系型数据库,存取效率极高,而且支持多种存储数据结构,使用也非常简单

  1. Redis 和 StrictRedis

    redis-py 库提供这两个类来实现Redistribution 的命令操作

  2. 连接Redis

    from redis import StrictRedis
    redis = StrictRedis(host='170.20.32.111',port=6379)
    
  3. 键操作

    方法 作用 参数说明 结果
    exists(name) 判断一个键是否存在 name:键名 True
    delete(name) 删除一个键 name:键名
    type(name) 判断键类型 name:键名 b’string’
    keys(pattern) 获取所有符合规则的键 pattern:匹配规则 [b’name’]
    randomkey() 获取随机的一个键 b’name’
    rename(src,dst) 重命名键 src:原键名 dst:新键名 True
    dbsize() 获取当前数据库中键的数目
    expire(name,time) 设定键的过期时间 name:键名
    ttl(name) 获取键的过期时间 name:键名
    move(name,db) 将键移动到其他数据库 name:键名 db:数据库代号
    flushdb() 删除当前数据库所有的键
    flushall() 删除所有数据库中所有的键
  4. 字符串操作

    Redis 支持最基本的键值对形式存储

    方法 作用 参数说明 结果
    set(name,value) 给数据库中键为name的string赋予值value name:键名 value:值 True
    get(name) 返回数据库中键为name的string的value name:键名 b’Bob’
    getset(name,value) 给数据库中键为name的string赋予值value并返回上次的value name:键名 value:新值 b’Bob’
    mget(keys,*args) 返回多个键对应的value keys:键的列表
    setnx(name,value) 如果不存在这个键值对,则更新vallue,否则不变 name:键名 True/False
    setex(name,time,value) 设置可以对应的值为string类型的value,并指定此键对应的有效期 name:键名 time:有效期 value:值 True
    setrange(name,offset,value) 设置指定键的value值的子字符串 name:键名 offset:偏移量 value:值 字符串长度
    mset(mapping) 批量赋值 mapping:字典 True
    msetnx(mapping) 键均不存在时才批量赋值 mapping:字典 True
    incr(age,amount=1) 键为age的value增值操作,默认为1,键不存在则被创建并设为amount name:键名 amount:增长值 修改后的值
    decr(name,amount) 减值操作
    append(key,value) 键为name的string的值追加value key:键名 修改的字符串长度
    substr(name,start,end=-1) 返回键为name 的string子字符串 name:键名 start:起始所以 end:终止索引 b’ello’
    getrange(key,start,end) 获取键的value值从start到end的子字符串 key:键名 start:起始值 end:终止索引 b’ello’
  5. 列表操作

    Redis 还提供了列表存储,列表内的元素可以重复,而且可以从两端存储

    方法 作用 参数说明 结果
    rpush(name,*values) 在键为name的列表末尾添加值为value的元素 name:键名 value:值 列表的大小
    lpush(name,*values) 在键为name的列表头添加值为value的元素 name:键名 value:值 列表的大小
    llen(name) 返回键为name的列表的长度 name:键名 列表长度
    lrange(name,start,end) 返回键为name的列表中start至end之间的元素 name:键名 start:起始索引 end:终止索引 [ ]
    ltrim(name,start,end) 截取键为name的列表保留索引为start 到end的内容 name:键名 start:起始索引 end:终止索引 [ ]
    lindex(name,index) 返回键为name的列表中index位置元素 name:键名 index:索引 [ ]
    lset(name,index,value) 给键为name的列表中index位置元素赋值,越界则报错 name:键名 index:索引值 value:值 True
    lrem(name,count,value) 将键为name的列表中删除count个值为value的元素 name:键名 count:删除个数 value:值 删除的个数
    lpop(name) 返回并删除键为name的列表中的首元素 name:键名 删除的元素
    rpop(name) 返回并删除键为name的列表的尾元素 name:键名 删除的元素
  6. 集合操作

    方法 作用 参数说明 结果
    sadd(name,*values) 向键为name的集合中添加元素 插入数据的个数
    srem(name,*values) 从键为name的集合中删除元素 删除的个数
    spop(name) 随机返回并删除键为name的集合中的一个元素 删除元素
    smove(src,dst,value) 从src对应的集合中移除元素并将其添加到dst对应的集合中 src:源集合 dst:目标集合 value:值 True
    scatd(name) 返回键为name的集合的元素个数
    sismember(name,value) 测试value是否是键为name的集合的元素
    sinter(keys,*args) 返回所有给定键的集合的交集
  7. RedisDump

    RedisDump 提供了强大的Redis 数据的导入和导出功能

    RedisDump 提供了两个可执行命令:

    ​ redis-dump:导出数据

    ​ redis-load: 用于导入数据

6.Ajax数据爬取

对于一种情况,数据加载是一种异步加载方式,原始的页面最初不会包含某些数据,原始页面加载完毕后,会向服务器请求某个接口获取数据,然后数据才会被处理从而显示到网页上,这其实即使发送了一个Ajax

大多数网页的原始HTML 文档不会包含任何数据,数据都是通过Ajax统一加载后再呈现出来的,这样在web开发上可以做到前后端分离,而且降低服务器直接渲染页面带来的压力

所以如果遇到这样的页面,直接利用requests 等库来抓取页面,是无法获取有效的数据的,这时需要分析网页后台像接口发送Ajax 请求,如果可以利用requests 来模拟Ajax 请求,那么就i可以成功抓取了

  1. 什么是Ajax

    Ajax:全称 Asynchronous Javascript and XML ,异步的Javascript 和 XML,不是一门编程语言,而是利用Javascript 在保证页面不被刷新,页面链接不改变的情况下与服务器交换数据并更新部分网页的技术

    W3school教程:

    1. 创建XMLHttpRequest对象

      XMLHttpRequest 用于在后台与服务器交换数据,意味着可以在不重新加载整个网页的情况下,对网页的某些部分进行更新

      variable = new XMLHttpRequest();
      
    2. XHR 请求

      如需将请求发送到服务器,我们使用XMLHttpRequest 对象的open() 和 send()方法

      xmlhttp.open("GET","test1.txt",true);
      xmlhttp.send();
      
      方法 描述
      open(method,url,async) 规定请求的类型,URL,以及是否异步处理
      method:请求的类型;GET 或 POST
      url: 文件在服务器上的位置
      async: true(异步) 或 false (同步)
      send(string) 将请求发送到服务器
      string:仅限于POST 请求

      GET 还是 POST?

      在以下情况中,请使用POST 请求:

      • 无法使用缓存文件(更新服务器上的文件或数据)

      • 向服务器发送大量数据(POST 没有数据量限定)

      • 发送包含未知字符的用户输入( POST 比 GET 更稳定也更可靠)


    3. XHR 响应

      属性 描述
      responseText 获得字符串形式的响应数据
      document.getElementById(“myDiv”).innerHTML=xmlhttp.responseText’
      responseXML 获得XML形式的响应数据
    4. XHR readystatechange 事件

      当请求被发送到服务器时,我们需要执行一些基于响应的任务

      每当readystate 改变时,就会触发onreadystatechange 事件

      readystate 属性存有 XMLHttpRequest的状态信息

      属性 描述
      onreadystatechange 存储函数,每当readystate属性改变时,就会调用该函数
      readystate 存有XMLHttpRequest 的状态。从 0 到 4 发生变化
      0:请求未初始化
      1:服务器连接已建立
      2:请求已连接
      3:请求处理中
      4:请求已完成,且响应已就绪
      status 200: ok
      404: 未找到页面

      .

  2. 基本原理

    发送Ajax 请求到网页更新的这个过程可以简单的分为三步:

    1. 发送请求
    2. 解析内容
    3. 渲染页面
    • 发送请求

      function loadXMLDoc()
      {
      var xmlhttp;
      if (window.XMLHttpRequest)
        {// code for IE7+, Firefox, Chrome, Opera, Safari
        xmlhttp=new XMLHttpRequest();
        }
      else
        {// code for IE6, IE5
        xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
        }
      xmlhttp.onreadystatechange=function()
        {
        if (xmlhttp.readyState==4 && xmlhttp.status==200)
          {
          document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
          }
        }
      xmlhttp.open("GET","/ajax/test1.txt",true);
      xmlhttp.send();
      }
      
    • 解析内容

      返回的内容可能时HTML,可能是JSON ,接下来只需要在方法中用 JavaScript 进一步处理即可

    • 渲染网页

      我们知道,真实的数据其实都是一次次Ajax 请求得到的,如果想要抓取这些数据,需要知道这些请求是怎么样发送的,发往哪里,发了那些参数

  3. Ajax 分析方法

    1. 查看请求

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bAm3wz8Q-1597060868055)(C:\Users\23977\AppData\Roaming\Typora\typora-user-images\1575095638127.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y559b52C-1597060868060)(C:\Users\23977\AppData\Roaming\Typora\typora-user-images\1575096228712.png)]

    2. 过滤请求

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1kNCgKR7-1597060868064)(C:\Users\23977\AppData\Roaming\Typora\typora-user-images\1575096788093.png)]

  4. Ajax 结果提取

    base_url 来表示请求的URL 的前半部分,接下来构造参数字典

    urlencode() 方法将参数转化为 URL 的 GET 请求参数 ,类似于 type=uid&page=3

    直接调用 json() 方法将内容解析为 JSON 返回

7.动态渲染页面爬取

很多网站不知Ajax 动态渲染这一种,甚至Ajax接口含有很多加密参数,我们难以直接找出其规律

为了解决这些问题,我们可以直接使用模拟浏览器运行的方式来实现,这样就可以做到在浏览器中看到什么样,抓取的源码就是什么样,也就是可见即可爬

python 提供了许多模拟浏览器运行的库。Selenium、Splash、PyV8、Ghost等

Selenium 的使用

  1. 在Windows下,建议直接将chromdriver.exe 文件拖到 Python 的 Scripts 目录下

  2. 基本使用

    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
    
    browser = webdriver.Chrome()
    try:
        browser.get('https://www.baidu.com')
        input = browser.find_element_by_id('kw')
        input.send_keys('菜鸟教程')
        input.send_keys(Keys.ENTER)
        wait = WebDriverWait(browser,10)
        wait.until(EC.presence_of_element_located((By.ID,'content_left')))
        print(browser.current_url)
        print(browser.get_cookies())
        print(browser.page_source)
    finally:
        browser.close()
    

    这里我们得到的当前URL、Cookies 和源代码都是浏览器中的真实内容

    所以说,如果用Selenium 来驱动浏览器加载网页的话,就可以直接拿到 JavaScript渲染的结果了,不用担心使用的是什么加密系统

  3. 声明浏览器对象

    from selenium import webdriver
    
    browser = webdriver.Chrome()  # 谷歌浏览器
    browser = webdriver.Firefox()  # 火狐浏览器
    
    # 这样完成了浏览器对象的初始化并将其赋值为 browser 对象,我们接下来要做的就是调用 browser对象,让其执行各个动作来模拟浏览器操作
    
  4. 访问页面

    from selenium import webdriver
    
    browser = webdriver.Chrome()
    browser.get('https://www.taobao.com')
    print(browser.page_source)
    
    # 通过这几行代码 ,我们就可以实现浏览器的驱动并获取网页源码
    
  5. 查找结点

    Selenium 可以驱动浏览器完成各种操作,比如填充表单、模拟点击,因而我们要找出这些节点所在的位置

    • 单个节点 find_element()

      from selenium import webdriver
      
      browser = webdriver.Chrome()
      browser.get('https://www.taobao.com')
      # print(browser.page_source)
      
      input_first = browser.find_element_by_id('q')
      input_second = browser.find_element_by_css_selector('#q')
      input_third = browser.find_element_by_xpath('//*[@id="q"]')
      print(input_first)
      print(input_second)
      print(input_third)
      browser.close()
      
      # 这里我们使用了 3种方式获取输入框,分别是ID、CSS、XPath 获取  返回的都是 WebElement类型
      

      ​ 这里列出了所有获取单个节点的方法:

      • browser.find_element_by_id()
        browser.find_element_by_name()
        browser.find_element_by_xpath()
        browser.find_element_by_link_text()
        browser.find_element_by_partial_link_text()
        browser.find_element_by_tag_name()
        browser.find_element_by_class_name()
        browser.find_element_by_css_selector()
        

        Selenium 还提供了通用方法 find_element(),他需要传入两个函数,查找方式 By 和 值,实际上,它就是 find_element_by_id() 这种方式的通用函数版本

        find_element_by_id(id) 等价于 find_element(By.ID , id ) 两者得到的结果完全一致

        这样参数更加灵活了

    • 多个节点 find_elements()

      from selenium import webdriver
      
      browser = webdriver.Chrome()
      browser.get('https://www.taobao.com')
      # 查找多个节点
      lis = browser.find_elements_by_css_selector('.service-bd li')
      for li in lis:
          print(li)
          
      # lis = browser.find_elements(By.CSS_SELECTOR,'.service-bd li')  一样的结果
      
  6. 节点交互
    让浏览器模拟执行一些动作,输入文字时用 send——keys()方法、清空文字时用clear() 方法、点击按钮时用click() 方法

    import time
    from selenium import webdriver
    
    browser = webdriver.Chrome()
    browser.get('https://www.taobao.com')
    # 节点交互
    input = browser.find_element_by_id('q')
    input.send_keys('伞')
    time.sleep(3)
    input.clear()
    input.send_keys('帽子')
    button = browser.find_element_by_class_name('btn-search')
    button.click()
    

    交互式官方文档:

    https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.remote.webelement

  7. 动作链
    鼠标拖拽,键盘按键等,这些动作用一种方式来执行,这就是动作链

    from selenium import webdriver
    from selenium.webdriver import ActionChains
    
    # 动作链
    browser = webdriver.Chrome()
    url = 'https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable'
    browser.get(url)
    browser.switch_to.frame('iframeResult')
    source = browser.find_element_by_css_selector('#draggable')
    target = browser.find_element_by_css_selector('#droppable')
    actions = ActionChains(browser)
    actions.drag_and_drop(source,target)
    actions.perform()
    

    动作链文档:

    https://selenium-python.readthedocs.io/api.html#module-selenium.%20webdriver.common.action-chains

  8. 执行 JavaScript
    对于某些操作, Selenuim API 没有提供,比如下拉进度条,所以我们可以直接模拟运行 JavaScript

    使用 execute_script() 方法即可实现

  9. 获取节点信息

    • 获取属性 get_attribute()

    • 获取文本值 : 每个节点都有text属性

      相当于 BeautifulSoup 的 get_text() 方法 、 pyquery 的 text() 方法

    • 获取 id、位置、标签名和大小

      id:获取节点的id

      location:获取节点在页面中的相对位置

      tag_name: 获取标签名称

      size:获取节点的大小,也就是宽高

  10. 切换 Frame

    我们知道网页中有一种节点叫做 iframe ,也就是子Frame。相当于页面的子页面

    Selenium打开页面,默认是在父级 Frame里面操作,要想获得zi Frame 里面的节点

    这个时候就需要使用 switch_to_frame()方法来切换

  11. 延时等待
    Selenium 中,get() 方法会在网页加载后结束执行,可能某些页面还有额外的 Ajax 请求,所以,这里需要延迟等待一定的时间,确保节点已经加载出来

    等待方式有两种:隐式等待 和 显示等待

    • 隐式等待: implicitly_wait()

      如果Selenium 没有在DOM 中找到节点,将继续等待,超出设定时间后,则抛出找不到节点异常,默认时间是 0

    • 显示等待:

      隐式等待效果没那好,可能页面加载会受到网络条件的影响

      指定一个最长等待时间,如果在规定时间内加载出来了这个节点,就返回查找的节点,如果到了规定时间依然没有加载出该节点,则抛出异常

      wait = WebDriverWait(browser,10)
      input = wait.until(EC.presence_of_element_located(By.ID,'q'))
      
      # EC.presence_of_element_located 这个条件,代表节点出现的意思
      
      等待条件 含义
      title_is 标题是某内容
      title_contains 标题包含某内容
      presence_of_element_located 节点加载出来,传入定位元组
      visibility_of_element_located 节点可见,传入定位元组
      presence_of_all_elements_located 所有节点加载出来
      text_to_be_present_in_element 某个节点包含某文字
      text_to_be_present_in_element_value 某个节点值包含某文字
      element_to_be_clickable 节点可点击
      staleness_of 判断一个节点是否仍在DOM ,可判断页面是否刷新
      alert_is_present 是否出现警告

      等待条件参考文档:

      https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.support.expectedcoditions

  12. 前进和后退

    forward() 和 back()

  13. Cookies

    对 Cookies 进行操作,例如获取、添加、删除Cookoes

    get_cookies() : 获取所有的 Cookies

    add_cookie() : 添加一个 Cookie

    delete_all_cookies() : 删除所有的 Cookies

  14. 异常处理

    try except 来捕获各类异样

Splash 的使用

Splash 是一个 JavaScript 渲染服务,是一个带有 HTTP API 的轻量级浏览器,同时 对接了 Python 中的 Twisted 和 QT 库,利用他,我们同样可以实现动态页面的抓取

  1. 功能介绍

    • 异步方式处理多个网页渲染过程
    • 获取渲染后的页面的源代码或截图
    • 通过关闭图片渲染或者使用 Adblock 规则来加快页面渲染速度
    • 可执行特定的 JavaScript 脚本
    • 可通过 Lua 脚本来控制页面渲染过程
    • 获取渲染的详细过程并通过 HAR 格式呈现
  2. Splash 的安装

    在 docker 下安装:

    docker run -d -p 8050:8050 scraping/splash
    
  3. Splash Lua 脚本

    • 入口及返回值

      function main(splash,args)
          splash:go("http://www.baidu.com")
          splash:wait(0.5)
          local title = splash:evaljs("document.title")
          return {titile = title}
      end
      
      返回结果:
      Splash Response: Object
      titile: "百度一下,你就知道"
      
    • 异步处理

      function main(splash,args)
          local example_urls = {"www.baidu.com","www.taobao.com","www.jd.com"}
          local urls = args.urls or example_urls
          local results = {}
          for index ,url in ipairs(urls) do
              local ok,reason = splash:go("https//" .. url)
              if ok then
                  splash:wait(2)
                  results[url] = splash:png()
              end
          end
              return results
      end
      
  4. Splash 对象属性

    • args

      该属性可以获取加载时配置的参数,比如 URL,如果为 GET 请求,可以获得 GET 请求参数,为 POST 请求,它可以获取表单提交的数据,Splash 也支持使用第二个参数直接作为 args

      function main(splash,args)
          local url = args.url
      end    
      
    • js_enabled

      这个属性是 Splash 的 JavaScript 执行开关,可以将其配置为 true 或 false 来控制是否执行 JavaScript 代码,默认为true

    • resource_timeout

      此属性可以设置加载的超时时间,单位是秒,如果设置为 0 或 nil(类似于 Python 中的 Nome),代表不检测超时,此属性适合在网页加载速度较慢的情况下设置

    • image_enabled

      此属性可以设置图片是否加载,默认情况下是加载的。禁用该属性后,可以节省网络流量并提高网页的加载速度,但是需要注意的是,可能会影响 JavaScript 的渲染,因为禁用后,它外层 DOM 节点的高度会受到影响,进而影响 DOM 节点的位置,如果 JavaScript 对图片节点有操作的话,其执行就会受到影响

    • plugins_enabled

      此属性可以控制浏览器插件(如 flash 插件)是否开启,默认情况下是 false,不开启

    • scroll_position

      事务之此属性,我们可以控制页面上下滚动

      function main(splash,args)
          assert(splash:go("https://www.taobao.com"))
          splash.scroll_position = {y=400}
          return {png = splash.png()}
      end    
      
  5. Splash 对象的方法

    • go()

      该方法用来请求某个链接,可以模仿 GET 和 POST 请求,同时支持传入请求头,表单等数据

      ok,reason = splash:go{url,baseurl=nil,headers=nil,http_method="GET",body=nil,formdata=nil}
      
      • url: 请求的URL
      • baseurl : 可选参数,默认为空,表示资源加载相对路径
      • headers : 可选参数,默认为空,表示请求头
      • http_method : 可选参数,默认为GET,也支持 POST
      • body :可选参数,默认为空,发 POST请求时,使用的 content-type 为 application/json
      • formdata :可选参数,默认为空,POST 的时候的表单参数,使用 Content-type 为 application/x-www-form-urlencode
    • wait()

      此方法可以控制页面的等待时间

      ok,reason = splash:wait{time,cancel_on_redirect,cancel_on_error = true}
      
      • time:等待的秒数
      • cancel_on _redirect :可选参数,默认 false ,表示如果发生了重定向就停止等待,并返回重定向结果
      • cancel_on_error :可选参数,默认为false ,表示如果发生了加载错误,就停止等待
    • jsfun():此方法可以直接调用 Javascript 定义的方法,但是所调用的方法需要用双中括号包围,相当于实现了 Javascript方法 到 Lua 脚本的转换

    • evaljs() :此方法执行 Javascript 代码 并返回其结果

    • html() :此方法来获取网页的源代码

    • png() :此方法来获取 PNG 格式的网页截图

    • har() : 此方法来获取页面加载过程描述

    • get_cookies() :此方法可以获取当前页面的 Cookies

    • select_all():此方法可以选中所有符合条件的节点,其参数时 CSS 选择器

    • mouse_click() : 此方法可以模拟鼠标点击操作,传入的参数为坐标 x 和 y,也可以直接选中某个节点

  6. Splash API 调用

    • render.html

      此接口用于获取 JavaScript 渲染的页面的HML代码,接口地址就是 Splash 的运行地址 加此接口名称

      curl http://172.20.32.111:8050/render.html?url = https://www.baidu.com
      
      
      用Python 实现的话
      import requests
      url = 'http://172.20.32.111:8050/render.html?url = https://www.baidu.com'
      response = requests.get(url)
      print(response.text)
      
    • render.png

      此接口获取网页截图,width 和 height 来控制宽高,返回的时 PNG 格式的 二进制数据

      curl http://172.20.32.111:8050/render.png?url=https://www.taobao.com&wait=5&width=1000&height=700
      
    • render.json

      此接口包含了前面接口的所有功能,返回结果是 JSON 格式

      curl https://172.20.32.111:8050/render.json?url=https://httpbin.org
      

      我们可以传入不同的参数控制其返回结果。传入 html=1,返回结果即会得到源代码数据,png=1 ,即会得到页面 PNG截图数据

    • execute

      此接口才是最强大的接口,此接口可以实现与 Lua脚本的对接

      前面的render.html 和 render.rng 等接口对于一般的 JavaScript渲染页面足够了。但是如果要实现一些交互操作的话,他们还是无能为力,这里就需要使用 excute接口

      一个简单的 Lua 脚本
      function main(splash)
      	return ''hello
      end
      
      然后将此脚本转化为 URL 编码后的字符。拼接到execute接口后面
      curl http://localhost:8050/executelua_source=functio+main%28splash%29%0D%0A++return+%27hello%27%OD%0Aend
      

      用python来实现

      import requests
      from urllib.parse import quote
      
      lua = """
      function main(splash)
      	return 'hello'
      """
      
      url = 'https://172.20.32.111:8050/excute?lua_source=' + quote(lua)
      response = requests.get(url)
      print(response.text)
      
      # quote() 方法 将 脚本进行 URL 转码  ,然后将其作为 lua_source 参数传递
      
      # 返回的结果是 JSON 格式
      

      如此一来,我么之前所说的 Lua 脚本均可以用此方式与python 进行对接,所有的网页的动态渲染,模拟点击,表单提交,页面滑动,延时等待后的一些结果均可以自由控制,获取页面源码和截图也都不在话下

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