python之爬虫三


20xpath入门

在编写爬虫程序的过程中提取信息是非常重要的环节,但是有时使用正则表达式无法匹配到想要的信息,或者书写起来非常麻烦,此时就需要用另外一种数据解析方法,也就是本节要介绍的 Xpath 表达式。

Xpath表达式

XPath(全称:XML Path Language)即 XML 路径语言,它是一门在 XML 文档中查找信息的语言,最初被用来搜寻 XML 文档,同时它也适用于搜索 HTML 文档。因此,在爬虫过程中可以使用 XPath 来提取相应的数据。

提示:XML 是一种遵守 W3C 标椎的标记语言,类似于 HTML,但两者的设计目的是不同,XML 通常被用来传输和存储数据,而 HTML 常用来显示数据。

您可以将 Xpath 理解为在XML/HTML文档中检索、匹配元素节点的工具。

Xpath 使用路径表达式来选取XML/HTML文档中的节点或者节点集。Xpath 的功能十分强大,它除了提供了简洁的路径表达式外,还提供了100 多个内建函数,包括了处理字符串、数值、日期以及时间的函数。因此 Xpath 路径表达式几乎可以匹配所有的元素节点。

Python 第三方解析库 lxml 对 Xpath 路径表达式提供了良好的支持,能够解析 XML 与 HTML 文档。

Xpath节点

XPath 提供了多种类型的节点,常用的节点有:元素、属性、文本、注释以及文档节点。如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2.  lang="zh-CN">website name
  3. 编程帮
  4. 2010
  5. www.biancheng.net

上面的 XML 文档中的节点例子:

(文档节点)

(元素节点)

lang="zh-CN" (属性节点)

节点关系

XML 文档的节点关系和 HTML 文档相似,同样有父、子、同代、先辈、后代节点。如下所示:

  1. <?xml version="1.0" encoding="utf-8"?>
  2.  lang="zh-CN">website name
  3. 编程帮
  4. 2010
  5. www.biancheng.net

上述示例分析后,会得到如下结果:

title name year address 都是 site 的子节点

site 是 title name year address  父节点

title name year address  属于同代节点

title 元素的先辈节点是 site website

website 的后代节点是 site title name year address

Xpath基本语法

1) 基本语法使用

Xpath 使用路径表达式在文档中选取节点,下表列出了常用的表达式规则:

Xpath路径表达式

表达式

描述

node_name

选取此节点的所有子节点。

绝对路径匹配,从根节点选取。

//

相对路径匹配,从所有节点中查找当前选择的节点,包括子节点和后代节点,其第一个 / 表示根节点。

选取当前节点。

..

选取当前节点的父节点。

选取属性值,通过属性值选取数据。常用元素属性有 @id 、@name、@type、@class、@tittle、@href。


下面以下述代码为例讲解 Xpath 表达式的基本应用,代码如下所示:

  1.  class="BookList">
  2.  class="book1" id="book_01" href="http://www.biancheng.net/">
  3.  class="name">c语言小白变怪兽

  4.  class="model">纸质书

  5.  class="price">80

  6.  class="color">红蓝色封装

  7.  class="book2" id="book_02" href="http://www.biancheng.net/">
  8.  class="name">Python入门到精通

  9.  class="model">电子书

  10.  class="price">45

  11.  class="color">蓝绿色封装

路径表达式以及相应的匹配内容如下:

xpath表达式://li

 

匹配内容:

c语言小白变怪兽

纸质书

80元

红蓝色封装

   

 

Python入门到精通

电子书

45元

蓝绿色封装

 

xpath表达式://li/p[@class="name"]

匹配内容:

c语言小白变怪兽

Python入门到精通

 

 

xpath表达式://li/p[@class="model"]

匹配内容:

纸质书

电子书

 

xpath表达式://ul/li/@href

匹配内容:

http://www.biancheng.net/

http://www.biancheng.net/

 

xpath表达式://ul/li

匹配内容:

c语言小白变怪兽

纸质书

80元

红蓝色封装

  

Python入门到精通

电子书

45元

蓝绿色封装

注意:当需要查找某个特定的节点或者选取节点中包含的指定值时需要使用[]方括号。如下所示:

xpath表达式://ul/li[@class="book2"]/p[@class="price"]

匹配结果:45元

2) xpath通配符

Xpath 表达式的通配符可以用来选取未知的节点元素,基本语法如下:

xpath通配符

通配符

描述说明

匹配任意元素节点

@*

匹配任意属性节点

node()

匹配任意类型的节点

示例如下:

xpath表达式://li/*

 

匹配内容:

c语言小白变怪兽

纸质书

80元

红蓝色封装

Python入门到精通

电子书

45元

蓝绿色封装

3) 多路径匹配

多个 Xpath 路径表达式可以同时使用,其语法如下:

xpath表达式1 | xpath表达式2 | xpath表达式3

示例应用:

表达式://ul/li[@class="book2"]/p[@class="price"]|//ul/li/@href

 

匹配内容:

45元

http://www.biancheng.net/

http://www.biancheng.net/

Xpath内建函数

Xpath 提供 100 多个内建函数,这些函数给我们提供了很多便利,比如实现文本匹配、模糊匹配、以及位置匹配等,下面介绍几个常用的内建函数。

Xpath内建函数

函数名称

xpath表达式示例

示例说明

text()

./text()

文本匹配,表示值取当前节点中的文本内容。

contains()

//div[contains(@id,'stu')] 

模糊匹配,表示选择 id 中包含“stu”的所有 div 节点。

last()

//*[@class='web'][last()]  

位置匹配,表示选择@class='web'的最后一个节点。

position()

//*[@class='site'][position()<=2]

位置匹配,表示选择@class='site'的前两个节点。

start-with()

"//input[start-with(@id,'st')]"

匹配 id 以 st 开头的元素。

ends-with()

"//input[ends-with(@id,'st')]"

匹配 id 以 st 结尾的元素。

concat(string1,string2)

concat('C语言中文网',.//*[@class='stie']/@href)

C语言中文与标签类别属性为"stie"的 href 地址做拼接。

21Xpath Helper的安装和使用

 

为了帮助大家快速掌握 Xpath 表达式的使用,这里给大家推荐一款 Xpath 表达式匹配助软件,它就是 Xpath Helper。

Xpath Helper介绍

Xpath Helper 是一款谷歌应用商店推出的免费工具,因此您需要在谷歌商店进行下载。下载完毕后,谷歌浏览器会将其作为插件自动安装在扩展程序中,如下所示:


1:Xpath Helper助手
 

点击扩展程序入口,进入管理扩展程序界面,如下图所示:


2:Chrome浏览器插件管理界面
 

您也可以通过以下步骤进入上述管理界面:浏览器设置 -> 更多工具 ->扩展程序 ->开发者模式。注意:此时右上角的开发者模式应处于开启状态。

Xpath Helper使用

安装完毕后,在需要匹配数据的页面处,使用快捷键打开助手工具(快捷键:ctrl+shift+x),使用示意图如下:

3:Xpath使用示意图(点击看高清图
 

将鼠标悬停在需要选取数据的文本上,并按下shift按键就会自动出现 Xpath 表达式,然后再根据您自己的需求对表达式稍微修改即可。

如果您没有谷歌应用商店账号,您也可以在网上搜索免费的下载资源。为了节省您的时间,下面提供了资源下载链接:

云盘链接:https://pan.baidu.com/s/18LcxOCLqALlob33UybTATA提取码:eo1m

下载解压后,将文件夹直接拖入 Chrome 扩展程序即可完成安装。

浏览器Xpath匹配助手

谷歌开发者调试工具也内置了 Xpath 表达式匹配功能,首先打开调试工具,在下方的调试工作区内使用快捷键ctrl+F打开 Xpath 匹配功能,如下图所示:

4:Chrome调试工具Xpath使用(点击看高清图

22Python lxml库的安装和使用

lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 Xpath 表达式提供了良好的支持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。

安装lxml库

lxml 属于 Python 第三方库,因此需要使用如下方法安装:

pip3 install lxml

CMD 命令行验证是否安装成功。若引入模块,不返回错误则说明安装成功。

>>> import lxml

>>>

lxml使用流程

lxml 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面我们简单介绍一下 lxml 库的使用流程,如下所示:

1) 导入模块

from lxml import etree

2) 创建解析对象

调用 etree 模块的 HTML() 方法来创建 HTML 解析对象。如下所示:

parse_html = etree.HTML(html)

HTML() 方法能够将 HTML 标签字符串解析为 HTML 文件,该方法可以自动修正 HTML 文本。示例如下:

  1. from lxml import etree
  2. html_str = '''
  • '''
  • html = etree.HTML(html_str)
  • # tostring()将标签元素转换为字符串输出,注意:result为字节类型
  • result = etree.tostring(html)
  • print(result.decode('utf-8'))
  • 输出结果如下:

      •  class="item1"> href="link1.html">Python
      •  class="item2"> href="link2.html">Java
      •  class="site1"> href="c.biancheng.net">C语言中文网
      •  class="site2"> href="www.baidu.com">百度
      •  class="site3"> href="www.jd.com">京东
  • 上述 HTML 字符串存在缺少标签的情况,比如“C语言中文网”缺少一个 闭合标签,当使用了 HTML() 方法后,会将其自动转换为符合规范的 HTML 文档格式。

    3) 调用xpath表达式

    最后使用第二步创建的解析对象调用 xpath() 方法,完成数据的提取,如下所示:

    r_list = parse_html.xpath('xpath表达式')

    lxml库数据提取

    下面通过一段 HTML 代码实例演示如何使用 lxml 库提取想要的数据。HTML 代码如下所示:

    1.  class="wrapper">
    2.  href="www.biancheng.net/product/" id="site">website product
    3.  id="sitename">
    4.  href="http://www.biancheng.net/" title="编程帮">编程
    5.  href="http://world.sina.com/" title="新浪娱乐">微博
    6.  href="http://www.baidu.com" title="百度">百度贴吧
    7.  href="http://www.taobao.com" title="淘宝">天猫淘宝
    8.  href="http://www.jd.com/" title="京东">京东购物
    9.  href="http://c.bianchneg.net/" title="C语言中文网">编程
    10.  href="http://www.360.com" title="360科技">安全卫士
    11.  href="http://www.bytesjump.com/" title=字节">视频娱乐
    12. 站">年轻娱乐
    13. 浏览器">搜索引擎
  • from lxml import etree
  • # 创建解析对象
  • parse_html=etree.HTML(html)
  • # 书写xpath表达式,提取文本最终使用text()
  • xpath_bds='//a/text()'
  • # 提取文本数据,以列表形式输出
  • r_list=parse_html.xpath(xpath_bds)
  • # 打印数据列表
  • print(r_list)
  • 1) 提取所有a标签内的文本信息

    输出结果:

    ['website product', '编程', '微博', '百度贴吧', '天猫淘宝', '京东购物', '编程', '安全卫士', '视频娱乐', '年轻娱乐', '搜索引擎']

    2) 获取所有href的属性值

    1. from lxml import etree
    2. # 创建解析对象
    3. parse_html=etree.HTML(html)
    4. # 书写xpath表达式,提取文本最终使用text()
    5. xpath_bds='//a/@href'
    6. # 提取文本数据,以列表形式输出
    7. r_list=parse_html.xpath(xpath_bds)
    8. # 打印数据列表
    9. print(r_list)

    输出结果:

    ['http://www.biancheng.net/product/', 'http://www.biancheng.net/', 'http://world.sina.com/', 'http://www.baidu.com', 'http://www.taobao.com', 'http://www.jd.com/', 'http://c.bianchneg.net/', 'http://www.360.com', 'http://www.bytesjump.com/', 'http://bzhan.com/', 'http://hao123.com/']

    3) 不匹配href=" www.biancheng.net/priduct"

    1. from lxml import etree
    2. # 创建解析对象
    3. parse_html=etree.HTML(html)
    4. # 书写xpath表达式,提取文本最终使用text()
    5. xpath_bds='//a/@href'
    6. # 提取文本数据,以列表形式输出
    7. xpath_bds='//ul[@id="sitename"]/li/a/@href'
    8. # 打印数据列表
    9. print(r_list)

    输出结果:

    ['http://www.biancheng.net/', 'http://world.sina.com/', 'http://www.baidu.com', 'http://www.taobao.com', 'http://www.jd.com/', 'http://c.bianchneg.net/', 'http://www.360.com', 'http://www.bytesjump.com/', 'http://bzhan.com/', 'http://hao123.com/'

    23Python lxml解析库实战应用

    确定信息元素结构

    首先明确要抓取信息的网页元素结构,比如电影名称、主演演员、上映时间。通过简单分析可以得知,每一部影片的信息都包含在

    标签中,而每一
    标签又包含在
    标签中,因此对于dd标签而言,dl标签是一个更大的节点,也就是它的父辈节点,如下所示:

     

    1:分析元素结构
     

    当一个

    标签内的影片信息提取完成时,您需要使用同样的 Xpath 表达式提取下一影片信息,直到所有影片信息提取完成,这种方法显然很繁琐。那么有没有更好的方法呢?

    基准表达式

    因为每一个节点对象都使用相同 Xpath 表达式去匹配信息,所以很容易想到 for 循环。我们将 10 个

    节点放入一个列表中,然后使用 for 循环的方式去遍历每一个节点对象,这样就大大提高了编码的效率。

    通过
    节点的父节点
    可以同时匹配 10 个
    节点,并将这些节点对象放入列表中。我们把匹配 10个
    节点的 Xpath 表达式称为“基准表达式”。如下所示:

    xpath_bds='//dl[@class="board-wrapper"]/dd'

    下面通过基准表达式匹配

    节点对象,代码如下:

    1. # 匹配10dd节点对象
    2. xpath_bds='//dl[@class="board-wrapper"]/dd'
    3. dd_list=parse_html.xpath(xpath_bds)

    输出结果:

    [, , , , , , , , , ]

    提取数据表达式

    因为我们想要抓取的信息都包含在

    节点中,接下来开始分析
    节点包含的 HTML 代码,下面随意选取的一段
    节点包含的影片信息,如下所示:

    1.  class="board-index board-index-4">4
    2.  href="/films/1292" title="海上钢琴师" class="image-link" data-act="boarditem-click" data-val="{movieId:1292}">
    3.  src="//s3plus.meituan.net/v1/mss_e2821d7f0cfe4ac1bf9202ecf9590e67/cdn-prod/file:5788b470/image/loading_2.e3d934bf.png" alt="" class="poster-default">
    4.  alt="海上钢琴师" class="board-img" src="https://p0.meituan.net/movie/609e45bd40346eb8b927381be8fb27a61760914.jpg@160w_220h_1e_1c">
    5.  class="board-item-main">
    6.  class="board-item-content">
    7.  class="movie-item-info">
    8.  class="name"> href="/films/1292" title="海上钢琴师" data-act="boarditem-click" data-val="{movieId:1292}">海上钢琴师

    9.  class="star">
    10. 主演:蒂姆·罗斯,比尔·努恩,克兰伦斯·威廉姆斯三世
    11.  class="releasetime">上映时间:2019-11-15

          
  •  class="movie-item-number score-num">
  •  class="score"> class="integer">9. class="fraction">3

           
  • 分析上述代码段,写出待抓取信息的 Xpath 表达式,如下所示:

    提取电影名信息:xpath('.//p[@class="name"]/a/text()')

    提取主演信息:xpath('.//p[@class="star"]/text()')

    提取上映时间信息:xpath('.//p[@class="releasetime"]/text()')

    完整程序代码

    上述内容介绍了编写程序时用到的 Xpath 表达式,下面正式编写爬虫程序,代码如下所示:

    1. # coding:utf8
    2. import requests
    3. from lxml import etree
    4. from ua_info import ua_list
    5. import random
    6. class MaoyanSpider(object):
    7. def __init__(self):
    8. self.url='https://maoyan.com/board/4?offset=50'
    9. self.headers={'User-Agent':random.choice(ua_list)}
    10. def save_html(self):
    11. html=requests.get(url=self.url,headers=self.headers).text
    12. #jiexi
    13. parse_html=etree.HTML(html)
    14. # 基准 xpath 表达式,匹配10
      节点对象
    15. dd_list=parse_html.xpath('//dl[@class="board-wrapper"]/dd') #列表放10dd
    16. print(dd_list)
    17. # .// 表示dd节点的所有子节点后代节点
    18. # 构建item空字典将提取的数据放入其中
    19. item={}
    20. for dd in dd_list:
    21. # 处理字典数据,注意xpath表达式匹配结果是一个列表,因此需要索引[0]提取数据
    22. item['name']=dd.xpath('.//p[@class="name"]/a/text()')[0].strip()
    23. item['star']=dd.xpath('.//p[@class="star"]/text()')[0].strip()
    24. item['time']=dd.xpath('.//p[@class="releasetime"]/text()')[0].strip()
    25. #输出数据
    26. print(item)
    27. def run(self):
    28. self.save_html()
    29. if __name__ == '__main__':
    30. spider=MaoyanSpider()
    31. spider.run()

    输出结果如下:

    {'name': '飞屋环游记', 'star': '主演:爱德华·阿斯纳,乔丹·长井,鲍勃·彼德森', 'time': '上映时间:2009-08-04'}

    {'name': '窃听风暴', 'star': '主演:乌尔里希·穆埃,塞巴斯蒂安·科赫,马蒂娜·格德克', 'time': '上映时间:2006-03-23(德国)'}

    {'name': '美国往事', 'star': '主演:罗伯特·德尼罗,詹姆斯·伍兹,伊丽莎白·麦戈文', 'time': '上映时间:2015-04-23'}

    {'name': '乱世佳人', 'star': '主演:费雯·丽,克拉克·盖博,奥利维娅·德哈维兰', 'time': '上映时间:1939-12-15(美国)'}

    {'name': '大话西游之大圣娶亲', 'star': '主演:周星驰,朱茵,莫文蔚', 'time': '上映时间:2014-10-24'}

    {'name': '美丽心灵', 'star': '主演:罗素·克劳,詹妮弗·康纳利,艾德·哈里斯', 'time': '上映时间:2001-12-13(美国)'}

    {'name': '消失的爱人', 'star': '主演:本·阿弗莱克,裴淳华,尼尔·帕特里克·哈里斯', 'time': '上映时间:2014-09-26(美国)'}

    {'name': '罗马假日', 'star': '主演:格利高里·派克,奥黛丽·赫本,埃迪·艾伯特', 'time': '上映时间:1953-08-20(意大利)'}

    {'name': '一一', 'star': '主演:吴念真,金燕玲,李凯莉', 'time': '上映时间:2017-07-28(中国台湾)'}

    {'name': '蝴蝶效应', 'star': '主演:约翰·帕特里克·阿梅多利,罗根·勒曼,卡梅隆·布莱特', 'time': '上映时间:2004-01-23(美国)'}

     

    24浏览器实现抓包过程详解

    乎所有浏览器都提供了抓取数据包的功能,因为浏览器为抓包提供了一个专门的操作界面,因此这种抓包方式也被称为“控制台抓包”。本节以 Chrome 浏览器为例进行抓包演示。

    控制台抓包指的是利用浏览器开的发者调试工具抓取客户端与后端服务器交互的数据,它能够将网络传输中发送与接收的数据进行截获、重发和编辑。

    控制台抓包非常适合于 POST 请求类型。我们知道,POST 请求使用 Form 表单向服务器提交数据,通过抓包可以获取 POST 请求体的数据以及相应参数,从而对响应内容进行分析。下面以有道翻译(http://fanyi.youdao.com/)为例,讲解如何进行控制台抓包。

    控制台界面

    关于开发者调试工具,您应该不会感到陌生,它除了有检查网页结构、元素构成的功能外,还有许多其他重要功能,比如抓取数据包。下面对如何抓包做重点介绍。

    首先访问有道翻译网站,然后使用快捷键 F12 打开控制台,并找到Network选项卡,最后在有道翻译的输入框内输入“hello world”进行翻译,控制台主界面如下所示:


    1:开发者调试工具
     

    下面对上图 1 中控制台的常用选项做简单介绍:

    1) NetWork

    该选项主要用于抓取网络数据包,比如查看请求信息、响应信息等。它有三个常用选项卡,分别是 All、XHR、JS,其作用如下:

    • All:抓取所有的网络数据包
    • XHR:抓取所有异步加载的网络数据包
    • JS:抓取所有的JS文件

    2) Sources

    该选项主要用于查看页面的 HTML 、JavaScript 、CSS 的源代码,除此之外,最重要的是它还可以调试 JS 源代码,可以给 JS 代码打断点调试,有助于分析爬虫程序中的一些参数。

    3) Console

    交互模式,能够执行 JavaScript 代码,一般用于对当前程序中 JS 代码进行测试,同时也可以查看 JavaScript 对象,或者调试日志、异常信息等。

    4) Application

    该选项用于查看、修改本地存储(Local Storage)以及会话存储(Session Stroage)等,同时它也可以用来查看 Cookie 信息。

    Cookie 是网站服务器为了辨别用户身份,而储存在客户端浏览器上一段加密字符串。某些网站需要用户登录后才可以看到相应的数据。如果想要爬取此类网站的数据,就需要使用 Cookie 模拟用户登录。

    数据包抓取

    有道翻译采用了 JS 异步加载的方式获取翻译结果,并将该结果渲染到指定的输出框内。所谓异步加载,即不需要刷新页面,就可实现页面的局部渲染。对于这样数据,可以通过 NetWork 的中 XHR 选项来抓取数据包,并选择查看最后一个数据包,如下图 2 所示:


    2:抓取数据包
     

    因为异步加载几乎是实时响应的,所以当您在输入框内输入“hello world”的过程中,每输出一个单词都会向服务器发送一次异步请求(若输出很慢时,一个字母都会发送一次请求),除非您事先将要查询的单词复制好,一次性粘贴到输入框内,只有这样才会得到一个数据包。因此,在这里选择了最后一个数据包进行分析。

    接下来,对上图 2 中常用选项做简单介绍:Headers 用来描述整个请求信息,Preview 用来对响应内容进行预览,Response 用于查看响应内容,Cookies 用于查看客户端 Cookie 信息。

     

    3:预览响应内容

    看变化规律

    在有道翻译的输入框内多输入几个单词或者汉字,查看 Form Data 的变换规律。你会发现有些参数的值总是变化的,而有些参数值没有变化,比如 salt、sign、lts 总是变化的,而 bv 等参数是不变化的,而参数 i 代表用户输入的单词。如下所示:

    i: hello world   #你输入的单词

    salt: 16161405904876   

    sign: a6f9d57d297acc79f31b049e2a542519

    lts: 1616140590487

    bv: cda1e53e0c0eb8dd4002cefc117fa588

    经过分析,最终您会发现如下规律:lts 代表毫秒时间戳;salt 和 lts 之间存在着某种关联,因为两者只有最后一个数字是不同的;而 sign 对应的值是一个加密后的字符串。在下一节我们将讲解如何破解有道翻译,将它作为 API 接口实现在线实时翻译。

    抓包是分析请求、响应数据,以及监视 HTTP(S) 通信的常用方法,它能够帮助我们明确要请求的 URL、请求参数/参数值、Cookies,以及其他响应信息,这对于构建 POST 请求方法十分重要。

    除了使用浏览器自带的调试工具外,您也可以使用 Fiddler 抓包工具,这款软件不仅适用于 Web 抓包,同样也适用于手机移动端抓包, 如果感兴趣的话可以点击了解(https://www.telerik.com/fiddler

     

    25Python爬虫破解有道翻译

    有道翻译是以异步方式实现数据加载的,要实现对此类网站的数据抓取,其过程相对繁琐,本节我以有道翻译为例进行详细讲解。

    上一节《浏览器实现抓包过程详解》,通过控制台抓包,我们得知了 POST 请求的参数以及相应的参数值,如下所示:


    1:有道翻译POST请求参数


    并发现以下了规律:salt、sign、lts 总是变化的,而 bv 等其他参数是不变化的。其中 lts 代表毫秒时间戳,salt 和 lts 之间存在着某种关联,因为两者只有最后一个数字是不同的;而 sign 对应的值是一个加密后的字符串。

    如果想要实现实时地抓取翻译结果,就需要将 salt 和 sign 转换为 用 Python 代码表示的固定形式。最后将所有参数放入到 requests.post() 中,如下所示:

    response = requests.post(url,data=data,headers=headers)

    其中 data 是字典格式参数,它用来构建 POST 请求方法的参数和参数值。

    JS代码slat与sign

     salt、sign 加密有两种实现方式:一种是通过前端 JS 实现,另一种是后台服务器生成加密串,并在返回响应信息时,将加密信息交给接浏览器客户端。但是,通过预览响应信息可知,并没有涉及 salt、sign 的信息,因此可以排除这种方法。


    2:预览响应信息
     

    那么要如何找到关于salt、sign 的 JS 代码呢?此时就要用到另外一个调试工具选项卡—— JS。如下图所示:


    3:js文件


    点击上图所示的搜索按钮来检索 JS 代码,输入 "salt",结果如图所示:


    4:找到相关js文件


    或者您也可以使用 Sources 选项卡将 fanyi.min.js 文件中的 JS 代码格式化输出,并使用 Ctrl+F 找到相应的 "salt" 位置,如下图所示:

     

    5:Sources选项卡应用

    提示:将所有 JS 代码 copy 下来,通过站长工具也可以实现格式化输出。

    通过上述方法就找到了 salt 与 sign(两个参数项是在一起的)JS 代码,如下所示:

    1. var r = function(e) {
    2. var t = n.md5(navigator.appVersion),
    3. r = "" + (new Date).getTime(),
    4. i = r + parseInt(10 * Math.random(), 10);
    5. return {
    6. ts: r,
    7. bv: t,
    8. salt: i,
    9. sign: n.md5("fanyideskweb" + e + i + "Tbh5E8=q6U3EXe+&L[4c@")
    10. }
    11. };

    注意,找到上述代码是解决本节问题的关键,大家一定要要掌握方法。

    Python代码表示参数

    通过上述 JS 代码的简单分析可知: r 变量等同于 lts,salt 变量等同于 i,而 sign 是一个经过 md5 加密的字符串。接下来使用 Python 代码来表示上述参数,如下所示:

    1. #lts毫秒时间戳
    2. str(int(time.time()*1000))
    3. #saltlts+0-9的随机数
    4. lts+str(random.randint(0,9))
    5. #sign加密字符串
    6. from hashlib import md5
    7. #word为要翻译的单词等同于js代码中的"e"
    8. string = "fanyideskweb" + word + salt + "Tbh5E8=q6U3EXe+&L[4c@"
    9. s = md5()
    10. #md5的加密串必须为字节码
    11. s.update(string.encode())
    12. #16进制加密
    13. sign = s.hexdigest()

    完整程序实现

    完整代码如下所示:

    1. #coding:utf8
    2. import random
    3. import time
    4. from hashlib import md5
    5. import requests
    6. class YoudaoSpider(object):
    7. def __init__(self):
    8. # url一定要写抓包时抓到的POST请求的提交地址,但是还需要去掉 url中的“_o”
    9. # “_o”这是一种url反爬策略,做了页面跳转,若直接访问会返回{"errorCode":50}
    10. self.url='http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule'
    11. self.headers={
    12. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36",
    13. }
    14. # 获取lts时间戳,salt加密盐,sign加密签名
    15. def get_lts_salt_sign(self,word):
    16. lts=str(int(time.time()*1000))
    17. salt=lts+str(random.randint(0,9))
    18. string = "fanyideskweb" + word + salt + "Tbh5E8=q6U3EXe+&L[4c@"
    19. s=md5()
    20. s.update(string.encode())
    21. sign=s.hexdigest()
    22. print(lts,salt,sign)
    23. return lts,salt,sign
    24. def attack_yd(self,word):
    25. lts,salt,sign=self.get_lts_salt_sign(word)
    26. #构建form表单数据
    27. data={
    28. "i": word,
    29. "from": "AUTO",
    30. "to": "AUTO",
    31. "smartresult": "dict",
    32. "client": "fanyideskweb",
    33. "salt": salt,
    34. "sign": sign,
    35. "lts": lts,
    36. "bv": "cda1e53e0c0eb8dd4002cefc117fa588",
    37. "doctype": "json",
    38. "version": "2.1",
    39. "keyfrom": "fanyi.web",
    40. "action": "FY_BY_REALTlME"
    41. }
    42. #使用 reqeusts.post()方法提交请求
    43. res = requests.post(
    44. url=self.url,
    45. data=data,
    46. headers=self.headers,
    47. )
    48. # res.json() json格式的字符串转为python数据类型
    49. # 客户端与服务器数据交互以json字符串传递,因此需要将它转换为python数据类型
    50. html=res.json()
    51. print(html)
    52. # 查看响应结果response  html:{"translateResult":[[{"tgt":"hello","src":"你好"}]],"errorCode":0,"type":"zh-CHS2en"}
    53. result=html["translateResult"][0][0]["tgt"]
    54. print('翻译结果:', result)
    55. def run(self):
    56. try:
    57. word=input('请输入要翻译的单词:')
    58. self.attack_yd(word)
    59. except Exception as e:
    60. print(e)
    61. if __name__ == '__main__':
    62. spider=YoudaoSpider()
    63. spider.run()

    输出结果:

    请输入要翻译的单词:大家好,这里是C语言中文网Python爬虫教程

     

    lts,salt,sign 输出结果:

    1616472092090 16164720920902 fcc592626aee42e1067c5195cf4c4576

     

    html 响应内容:

    {'type': 'ZH_CN2EN', 'errorCode': 0, 'elapsedTime': 25, 'translateResult': [[{'src': '大家好,这里是C语言中文网Python爬虫教程', 'tgt': 'Everybody is good, here is the Chinese Python crawler C language tutorial'}]]}

     

    翻译结果: Everybody is good, here is the Chinese Python crawler C language tutorial

    26抓取动态数据

    本节讲解如何抓取豆瓣电影“分类排行榜”中的电影数据(https://movie.douban.com/chart),比如输入“犯罪”则会输出所有犯罪影片的电影名称、评分,效果如下所示:

    剧情|喜剧|动作|爱情|科幻|动画|悬疑|惊悚|恐怖|纪录片|短片|情色|同性|音乐|歌舞|家庭|儿童|传记|历史|战争|犯罪|西部|奇幻|冒险|灾难|武侠|古装|运动|黑色电影|

    你想了解什么类型电影:犯罪

    {'name': '肖申克的救赎', 'score': 9.7}

    {'name': '控方证人', 'score': 9.6}

    ...

    电影总数量:302部

    确定网站类型

    首先要明确豆瓣电影网站的类型,即是动态还是静态。检查方法:右键查看网页源码 —> 搜索“辛德勒的名单”关键字,如下图所示:


    1:分析网站类型


    最终发现源码页中没有出现想要抓取的数据,只有一大堆的 JS 代码,由此确定该网站为动态网站。

    影片详情信息

    接下来,使用快捷键 F12 打开控制台进行抓包,点击NetWork选项卡 —>XHR选项 —> Preview选项卡 —> 刷新当前页面抓取数据包,如下图所示:


    2:抓取动态网站数据包


    从图 2 可知,我们想要抓取的数据取全部包含在当前的数据包中。当我们向下滚动鼠标滑轮时,左侧栏内的数据包会实现自动加载,这是使用?Ajax?异步加载技术实现的。

    通过查看数据 Headers 选项可以明确 url 地址、查询参数等信息,如下所示:


    3:分析Headers信息
     

    从上图可以得知请求的基准 URL (由于还未拼接查询参数,所以称之为基准 URL),如下所示:

    'https://movie.douban.com/j/chart/top_list?'

    继续滚动鼠标滑轮可知查询参数具有如下规律:

    type: 4  # 电影类型

    interval_id: 100:90  #代表网页上滑动条的百分比(好于100%-90%的历史片)

    action: ''  # 空

    start: 0  # 每次加载电影的起始索引值 0 20 40 60

    limit: 20 # 每次加载的电影数量,1为初始值,后续加载时20固定不变

    注意:寻找规律时,后加载出来的数据包会排在最前面,除去第一个数据包外,其余数据包如下所示:


    4:寻找查询参数值的规律

    影片总数量

    注意:第一个数据包反映了每个类型中电影的总数量,其 url 与响应信息如下:

    请求的URL地址 : https://movie.douban.com/j/chart/top_list_count?type=4&interval_id=100%3A90

    Response信息:{"playable_count":41,"total":104,"unwatched_count":104}

    影片类型与类型码

    影片的类型与类型码包含在电影排行榜的主界面中,如下所示:


    5:影片类型与类型码
     

    分析上述页面结构,然后使用正则表达式来提取想要的数据,并定义选择菜单“menu”,代码如下所示:

    1. import re
    2. def get_all_type_films(self):
    3. # 获取影片类型和类型码
    4. url = 'https://movie.douban.com/chart'
    5. headers = self.get_headers()
    6. html = requests.get(url=url, headers=headers).text
    7. re_bds = r''
    8. pattern = re.compile(re_bds, re.S)
    9. r_list = pattern.findall(html)
    10. # 存放所有类型和对应类型码大字典
    11. type_dict = {}
    12. # 定义一个选择电影类型的菜单
    13. menu = ''
    14. # r_list[{'剧情 , 11'},{},..]
    15. for r in r_list:
    16. type_dict[r[0].strip()] = r[1].strip()
    17. # 获取input的菜单,显示所有电影类型
    18. menu += r[0].strip() + '|'
    19. #返回类型字典以供后续函数调用,并返回输入菜单menu
    20. # {'剧情': '11', '喜剧': '24',...}
    21. return type_dict, menu

    编写完整程序

    完成上述分析后,下面开始编写 Python 爬虫程序,代码如下:

    1. #coding:utf8
    2. import requests
    3. import time
    4. import random
    5. import re
    6. import json
    7. from ua_info import ua_list
    8. class DoubanSpider(object):
    9. def __init__(self):
    10. self.url = 'https://movie.douban.com/j/chart/top_list?'
    11. self.i = 0
    12. # 获取随机headers
    13. def get_headers(self):
    14. headers = {'User-Agent':random.choice(ua_list)}
    15. return headers
    16. # 获取页面
    17. def get_page(self,params):
    18. # json转换为 python 数据类型,并返回
    19. html = requests.get(url=self.url,params=params,headers=self.get_headers()).text
    20. html=json.loads(html)
    21. self.parse_page(html)
    22. # 解析并保存数据
    23. def parse_page(self,html):
    24. item = {}
    25. # html列表类型: [{电影1},{电影2},{电影3}...]
    26. for one in html:
    27. # 名称 + 评分
    28. item['name'] = one['title'].strip()
    29. item['score'] = float(one['score'].strip())
    30. print(item)
    31. self.i += 1
    32. # 获取电影总数
    33. def total_number(self,type_number):
    34. # F12抓包抓到的地址,type表示电影类型
    35. url = 'https://movie.douban.com/j/chart/top_list_count?type={}&interval_id=100%3A90'.format(type_number)
    36. headers = self.get_headers()
    37. html = requests.get(url=url,headers=headers).json()
    38. total = int(html['total'])
    39. return total
    40. # 获取所有电影的类型和对应type
    41. def get_all_type_films(self):
    42. # 获取类型与类型码
    43. url = 'https://movie.douban.com/chart'
    44. headers = self.get_headers()
    45. html = requests.get(url=url,headers=headers).text
    46. re_bds = r''
    47. pattern = re.compile(re_bds,re.S)
    48. r_list = pattern.findall(html)
    49. # 存放所有类型和对应类型码大字典
    50. type_dict = {}
    51. #定义一个选择电影类型的菜单
    52. menu = ''
    53. for r in r_list:
    54. type_dict[r[0].strip()] = r[1].strip()
    55. # 获取input的菜单,显示所有电影类型
    56. menu += r[0].strip() + '|'
    57. return type_dict,menu
    58. # 主程序入口函数
    59. def main(self):
    60. # 获取type的值
    61. type_dict,menu = self.get_all_type_films()
    62. menu = menu + '\n你想了解什么类型电影:'
    63. name = input(menu)
    64. type_number = type_dict[name]
    65. # 获取电影总数
    66. total = self.total_number(type_number)
    67. for start in range(0,(total+1),20):
    68. #构建查询参数
    69. params = {
    70. 'type' : type_number,
    71. 'interval_id' : '100:90',
    72. 'action' : '',
    73. 'start' : str(start),
    74. 'limit' : '20'
    75. }
    76. # 调用函数,传递params参数
    77. self.get_page(params)
    78. # 随机休眠1-3
    79. time.sleep(random.randint(1,3))
    80. print('电影总数量:%d'%self.i )
    81. if __name__ == '__main__':
    82. spider = DoubanSpider()
    83. spider.main()

    输出示例:

    剧情|喜剧|动作|爱情|科幻|动画|悬疑|惊悚|恐怖|纪录片|短片|情色|同性|音乐|歌舞|家庭|儿童|传记|历史|战争|犯罪|西部|奇幻|冒险|灾难|武侠|古装|运动|黑色电影|

    你想了解什么类型电影:科幻

    {'name': '盗梦空间', 'score': 9.3}

    {'name': '星际穿越', 'score': 9.3}

    {'name': '楚门的世界', 'score': 9.3}

    {'name': '机器人总动员', 'score': 9.3}

    {'name': '蝙蝠侠:黑暗骑士', 'score': 9.2}

    {'name': '超感猎杀:完结特别篇', 'score': 9.2}

    {'name': '新世纪福音战士 第0:0话 诞生之始', 'score': 9.2}

    {'name': '少年骇客:变身之谜', 'score': 9.2}

    ...

    ...

    电影总数量:147部

    最后我们对抓取动态网站数据做简单地总结:

    • 1. 确定网站是否为动态网站,通过查看源码搜索相应的关键字即可确定。
    • 2. 动态网站主要通过异步方式加载数据。触发数据加载的 JS 事件主要有滚动鼠标滑轮、鼠标点击、拉动滚动条等有关动作, 也有一些网站通过局部更新的方式加载数据,比如有道翻译案例。

    27json

    JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,遵循欧洲计算机协会制定的 JavaScript 规范(简称 ECMAScript)。JSON 易于人阅读和编写,同时也易于机器解析和生成,能够有效的提升网信息的传输效率,因此它常被作为网络、程序之间传递信息的标准语言,比如客户端与服务器之间信息交互就是以 JSON 格式传递的。

    简单地说,JSON 可以将 JavaScript 对象表示的一组数据转换为字符串格式,以便于在网络、程序间传输这个字符串。并且在需要的时候,您还可以将它转换为编程语言所支持的数据格式。本节主要介绍如何实现 JSON 数据与 Python 数据类型间的相互转换。

    Python 语言内置了专门处理 JOSN 数据的模块 —— jons 模块,通过该模块就可以完成 JSON 与 Python 两种数据格式的相互转换。

    jons.loads()

    该方法可以将 json 格式的字符串转换成 Python 对象(比如列表、字典、元组、整型以及浮点型),其中最常用的是转换为字典类型。示例如下:

    1. # coding:utf8
    2. import json
    3. #JOSN字符串
    4. website_info='{"name" : "c语言中文网","PV" : "50","UV" : "20","create_time" : "2010"}'
    5. py_dict=json.loads(website_info)
    6. print("python字典数据格式:%s;数据类型:%s"% (py_dict,type(py_dict)))

    输出结果:

    python字典数据格式:{'name': 'c语言中文网', 'PV': '50万', 'UV': '20万', 'create_time': '2010年'};数据类型:

    注意:上述示例中 JSON 字符串看上去和 Python 字典非常相似,但是其本质不同,JOSN 是字符串类型,而 Python 字典是 dict 类型。

    json.dump()

    它可以将 Python 对象(字典、列表等)转换为 json 字符串,并将转换后的数据写入到 json 格式的文件中 ,因此该方法必须操作文件流对象。比如当使用爬虫程序完成数据抓取后,有时需要将数据保存为 json 格式,此时就用到了 json.dump() 方法,语法格式如下:

    json.dump(object,f,inden=0,ensure_ascii=False)

    参数说明如下:

    • object:Python 数据对象,比如字典,列表等
    • f:文件流对象,即文件句柄。
    • indent:格式化存储数据,使 JSON 字符串更易阅读。
    • ensure_ascii:是否使用 ascii 编码,当数据中出现中文的时候,需要将其设置为 False。


    示例示例如下:

    1. import json
    2. ditc_info={"name" : "c语言中文网","PV" : "50","UV" : "20","create_time" : "2010"}
    3. with open("web.josn","a") as f:
    4. json.dump(ditc_info,f,ensure_ascii=False)

    打开 web.json 文件,其内容如下所示:

    {

    "name": "c语言中文网",

    "PV": "50万",

    "UV": "20万",

    "create_time": "2010年"

    }

    您也可以将 Python 列表转换成 JSON 字符串,并保存至 json 文件中,如下所示:

    1. import json
    2. item_list = []
    3. item = {'website': 'C语言中文网', 'url': "c.biancheng.net"}
    4. for k,v in item.items():
    5. item_list.append(v)
    6. with open('info_web.json', 'a') as f:
    7. json.dump(item_list, f, ensure_ascii=False)

    打开 info_web.json 文件,其内容如下:

    ["C语言中文网", "c.biancheng.net"]

    json.load()

    该方法用于操作文件流对象,不过它与 dump() 恰好相反,它表示从  json 文件中读取 JSON 字符串,并将读取内容转换为 Python 对象。使用示例如下:

    1. import json
    2. site = {'name':'c语言中文网',"url":"c.biancheng.net"}
    3. filename = 'website.json'
    4. with open (filename,'w') as f:
    5. json.dump(site,f,ensure_ascii=False)
    6. with open (filename,'r') as f:
    7. print(json.load(f))

    输出结果如下:

    {'name': 'c语言中文网', 'url': 'c.biancheng.net'}

    json.dumps()

    该方法可以将 Python 对象转换成 JSON 字符串。示例如下:

    1. import json
    2. #python字典
    3. item = {'website': 'C语言中文网', 'rank': 1}
    4. # json.dumps之后
    5. item = json.dumps(item,ensure_ascii=False)
    6. print('转换之后的数据类型为:',type(item))
    7. print(item)

    输出结果如下:

    转换之后的数据类型为:

    {"website": "C语言中文网", "url": "c.biancheng.net"}

    最后对上述方法做简单地总结,如下表所示:

    JSON方法总结

    方法

    作用

    json.dumps()

    Python 对象转换成 JSON 字符串。

    json.loads()

    JSON 字符串转换成 Python 对象。

    json.dump()

    Python 中的对象转化成 JSON 字符串储存到文件中。

    json.load()

    将文件中的 JSON 字符串转化成 Python 对象提取出来。


    综上所述 json.load() 与 json.dump() 操作的是文件流对象,实现了 json 文件的读写操作,而 json.loads() 与 json.dumps() 操作的是 Python 对象或者 JOSN 字符串

    28Python爬虫实现Cookie模拟登录

    在使用爬虫采集数据的规程中,我们会遇到许多不同类型的网站,比如一些网站需要用户登录后才允许查看相关内容,如果遇到这种类型的网站,又应该如何编写爬虫程序呢?Cookie 模拟登录技术成功地解决了此类问题。

    Cookie 是一个记录了用户登录状态以及用户属性的加密字符串。当你第一次登陆网站时,服务端会在返回的 Response Headers 中添加 Cookie, 浏览器接收到响应信息后,会将 Cookie 保存至浏览器本地存储中,当你再次向该网站发送请求时,请求头中就会携带 Cookie,这样服务器通过读取 Cookie 就能识别登陆用户了。

    提示:我们所熟知的“记住密码”功能,以及“老用户登陆”欢迎语,这些都是通过 Cookie 实现的。

    下面介绍如何实现 Cookie 模拟登录,本节以模拟登录人人网(http://life.renren.com/)为例进行讲解。

    注册登录

    首先你要注册一个人人网的账号,注册过中要填写个人资料,以便后续抓取数据使用。注册成功后点击上方头像进入我的主页(或左侧主页选项卡),如下图所示:


    1:人人网主界面


    然后使用 F12 打开调试工具,刷新页面来抓取登录时的数据包(包名:timeline...开头), 并在 Headers 选项中找到请求头中的 Cookie 信息,将 Cookie 值拷贝下来,以备后续使用。如下所示:


    2:浏览Headers信息

    分析网页结构

    确定了 Cookie 信息后,接下来分析页面元素结构。通过调试工具审查如图 1 所示的个人信息栏,其元素结构如下:

    1.  id="operate_area" class="clearfix">
    2.  class="tl-information">
      •  class="school">就读于电子科技大学
      •  class="birthday">
      • 男生 
      • 77
      •  class="hometown">来自北京西城区
      •  class="address">现居 北京
    3. ...

    由此可知其 Xpath 表达式为:

    r_school = parse_html.xpath('//*[@id="operate_area"]/div[1]/ul/li[1]/span/text()'

    r_birthday = parse_html.xpath('//li[@class="birthday"]/span/text()')

    home_info=parse_html.xpath('//*[@id="operate_area"]/div[1]/ul/li/text()')

    编写完整程序

    完成程序如下所示:

    1. import requests
    2. from lxml import etree
    3. class RenrenLogin(object):
    4. def __init__(self):
    5. # 个人主页的url地址
    6. self.url = 'http://www.renren.com/972496145/profile'
    7. self.headers = {
    8. # 将拷贝的cookie值放在此处
    9. 'Cookie':'anonymid=kmol2vxqgd4n0e; depovince=HEB; _r01_=1; ick_login=c577d6c0-0ec3-465a-89d0-9e2b8f23e107; taihe_bi_sdk_uid=0738130d7f4532165841f09abc596215; taihe_bi_sdk_session=6277ea795624ba1eddb2603d7fe45c85; _de=1D29BC9596B9643C92425970B59A3DAE; p=3e6989099ff75de92407b791266376095; first_login_flag=1; ln_uact=18519784236; ln_hurl=http://hdn.xnimg.cn/photos/hdn321/20191017/0945/h_main_El46_9a13000ecbe41986.jpg; t=7f25f8a4d3515786d146143f63d108b25; societyguester=7f25f8a4d3515786d146143f63d108b25; id=972496145; xnsid=9770206d; wpsid=15900539012757; ver=7.0; loginfrom=null; wp_fold=0; jebecookies=59f8dfaf-8416-4dbc-a539-016a7ae1b6c5|||||',
    10. # 注意,useragent不能改变,否则cookie失效
    11. 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36'
    12. }
    13. def get_html(self):
    14. html = requests.get(url=self.url,headers=self.headers).text
    15. self.parse_html(html)
    16. def parse_html(self,html):
    17. parse_html = etree.HTML(html)
    18. r_school = parse_html.xpath('//*[@id="operate_area"]/div[1]/ul/li[1]/span/text()')
    19. print(r_school)
    20. r_birthday = parse_html.xpath('//li[@class="birthday"]/span/text()')
    21. print(r_birthday)
    22. home_info=parse_html.xpath('//*[@id="operate_area"]/div[1]/ul/li/text()')
    23. item = {}
    24. item['hometown'] = home_info[2].strip()
    25. item['address'] = home_info[3].strip()
    26. print(item)
    27. if __name__ == '__main__':
    28. spider = RenrenLogin()
    29. spider.get_html()

    最后关闭“人人网”网站,运行上述程序,其结果如下:

    ['就读于电子科技大学']

    ['男生', ',7月7日']

    {'hometown': '来自 北京 西城区', 'address': '现居 上海'

     

    29Python多线程爬虫详解

    网络爬虫程序是一种 IO 密集型程序,程序中涉及了很多网络 IO 以及本地磁盘 IO 操作,这些都会消耗大量的时间,从而降低程序的执行效率,而 Python 提供的多线程能够在一定程度上提升 IO 密集型程序的执行效率。

    如果想学习 Python 多进程、多线程以及 Python GIL 全局解释器锁的相关知识,可参考《Python并发编程教程》。

    多线程使用流程

    Python 提供了两个支持多线程的模块,分别是 _thread 和 threading。其中 _thread 模块偏底层,它相比于 threading 模块功能有限,因此推荐大家使用 threading 模块。 threading 中不仅包含了  _thread 模块中的所有方法,还提供了一些其他方法,如下所示:

    • threading.currentThread() 返回当前的线程变量。
    • threading.enumerate() 返回一个所有正在运行的线程的列表。
    • threading.activeCount() 返回正在运行的线程数量。


    线程的具体使用方法如下所示:

    1. from threading import Thread
    2. ?#线程创建、启动、回收
    3. t = Thread(target=函数名) # 创建线程对象
    4. t.start() # 创建并启动线程
    5. t.join()  # 阻塞等待回收线程

    创建多线程的具体流程:

    1. t_list = []
    2. for i in range(5):
    3. t = Thread(target=函数名)
    4. t_list.append(t)
    5. t.start()
    6. for t in t_list:
    7. t.join()

    除了使用该模块外,您也可以使用  Thread  线程类来创建多线程。

    在处理线程的过程中要时刻注意线程的同步问题,即多个线程不能操作同一个数据,否则会造成数据的不确定性。通过 threading 模块的 Lock 对象能够保证数据的正确性。

    比如,使用多线程将抓取数据写入磁盘文件,此时,就要对执行写入操作的线程加锁,这样才能够避免写入的数据被覆盖。当线程执行完写操作后会主动释放锁,继续让其他线程去获取锁,周而复始,直到所有写操作执行完毕。具体方法如下所示:

    1. from threading import Lock
    2. lock = Lock()
    3. # 获取锁
    4. lock.acquire()
    5. wirter.writerows("线程锁问题解决")
    6. # 释放锁
    7. lock.release()

    Queue队列模型

    对于 Python 多线程而言,由于 GIL 全局解释器锁的存在,同一时刻只允许一个线程占据解释器执行程序,当此线程遇到 IO 操作时就会主动让出解释器,让其他处于等待状态的线程去获取解释器来执行程序,而该线程则回到等待状态,这主要是通过线程的调度机制实现的。

    由于上述原因,我们需要构建一个多线程共享数据的模型,让所有线程都到该模型中获取数据。queue(队列,先进先出) 模块提供了创建共享数据的队列模型。比如,把所有待爬取的 URL 地址放入队列中,每个线程都到这个队列中去提取 URL。queue 模块的具体使用方法如下:

    1. # 导入模块
    2. from queue import Queue
    3. q = Queue() #创界队列对象
    4. q.put(url) 向队列中添加爬取一个url链接
    5. q.get() # 获取一个url,当队列为空时,阻塞
    6. q.empty() # 判断队列是否为空,True/False

    多线程爬虫案例

    下面通过多线程方法抓取小米应用商店(https://app.mi.com/)中应用分类一栏,所有类别下的 APP 的名称、所属类别以及下载详情页 URL 。如下图所示:


    1:小米应用商城

    抓取下来的数据 demo 如下所示:

    三国杀,棋牌桌游,http://app.mi.com/details?id=com.bf.sgs.hdexp.mi

    1) 案例分析

    通过搜索关键字可知这是一个动态网站,因此需要抓包分析。

    刷新网页来重新加载数据,可得知请求头的 URL 地址,如下所示:

    https://app.mi.com/categotyAllListApi?page=0&categoryId=1&pageSize=30

    其中查询参数 pageSize 参数值不变化,page 会随着页码的增加而变化,而类别 Id 通过查看页面元素,如下所示

    1.  class="category-list">
    2.  class="current" href="/category/15">游戏
    3.  href="/category/5">实用工具
    4.  href="/category/27">影音视听
    5.  href="/category/2">聊天社交
    6.  href="/category/7">图书阅读
    7.  href="/category/12">学习教育
    8.  href="/category/10">效率办公
    9.  href="/category/9">时尚购物
    10.  href="/category/4">居家生活
    11.  href="/category/3">旅行交通
    12.  href="/category/6">摄影摄像
    13.  href="/category/14">医疗健康
    14.  href="/category/8">体育运动
    15.  href="/category/11">新闻资讯
    16.  href="/category/13">娱乐消遣
    17.  href="/category/1">金融理财

    因此,可以使用 Xpath 表达式匹配 href 属性,从而提取类别 ID 以及类别名称,表达式如下:

    基准表达式:xpath_bds = '//ul[@class="category-list"]/li'

    提取 id 表达式:typ_id = li.xpath('./a/@href')[0].split('/')[-1]

    类型名称:typ_name = li.xpath('./a/text()')[0]

    点击开发者工具的 response 选项卡,查看响应数据,如下所示:

    {

    count: 2000,

    data: [

    {

    appId: 1348407,

    displayName: "天气暖暖-关心Ta从关心天气开始",

    icon: "http://file.market.xiaomi.com/thumbnail/PNG/l62/AppStore/004ff4467a7eda75641eea8d38ec4d41018433d33",

    level1CategoryName: "居家生活",

    packageName: "com.xiaowoniu.WarmWeather"

    },

    {

    appId: 1348403,

    displayName: "贵斌同城",

    icon: "http://file.market.xiaomi.com/thumbnail/PNG/l62/AppStore/0e607ac85ed9742d2ac2ec1094fca3a85170b15c8",

    level1CategoryName: "居家生活",

    packageName: "com.gbtc.guibintongcheng"

    },

    ...

    ...

    通过上述响应内容,我们可以从中提取出 APP 总数量(count)和 APP (displayName)名称,以及下载详情页的 packageName。由于每页中包含了 30 个 APP,所以总数量(count)可以计算出每个类别共有多少页。

    pages = int(count) // 30 + 1

    下载详情页的地址是使用 packageName 拼接而成,如下所示:

    link = 'http://app.mi.com/details?id=' + app['packageName']

    ?2) 完整程序

    完整程序如下所示:

    1. # -*- coding:utf8 -*-
    2. import requests
    3. from threading import Thread
    4. from queue import Queue
    5. import time
    6. from fake_useragent import UserAgent
    7. from lxml import etree
    8. import csv
    9. from threading import Lock
    10. import json
    11. class XiaomiSpider(object):
    12. def __init__(self):
    13. self.url = 'http://app.mi.com/categotyAllListApi?page={}&categoryId={}&pageSize=30'
    14. # 存放所有URL地址的队列
    15. self.q = Queue()
    16. self.i = 0
    17. # 存放所有类型id的空列表
    18. self.id_list = []
    19. # 打开文件
    20. self.f = open('XiaomiShangcheng.csv','a',encoding='utf-8')
    21. self.writer = csv.writer(self.f)
    22. # 创建锁
    23. self.lock = Lock()
    24. def get_cateid(self):
    25. # 请求
    26. url = 'http://app.mi.com/'
    27. headers = { 'User-Agent': UserAgent().random}
    28. html = requests.get(url=url,headers=headers).text
    29. # 解析
    30. parse_html = etree.HTML(html)
    31. xpath_bds = '//ul[@class="category-list"]/li'
    32. li_list = parse_html.xpath(xpath_bds)
    33. for li in li_list:
    34. typ_name = li.xpath('./a/text()')[0]
    35. typ_id = li.xpath('./a/@href')[0].split('/')[-1]
    36. # 计算每个类型的页数
    37. pages = self.get_pages(typ_id)
    38. #往列表中添加二元组
    39. self.id_list.append( (typ_id,pages) )
    40. # 入队列
    41. self.url_in()
    42. # 获取count的值并计算页数
    43. def get_pages(self,typ_id):
    44. # 获取count的值,即app总数
    45. url = self.url.format(0,typ_id)
    46. html = requests.get(
    47. url=url,
    48. headers={'User-Agent':UserAgent().random}
    49. ).json()
    50. count = html['count']
    51. pages = int(count) // 30 + 1
    52. return pages
    53. # url入队函数,拼接url,并将url加入队列
    54. def url_in(self):
    55. for id in self.id_list:
    56. # id格式:('4',pages)
    57. for page in range(1,id[1]+1):
    58. url = self.url.format(page,id[0])
    59. # URL地址入队列
    60. self.q.put(url)
    61. # 线程事件函数: get() -请求-解析-处理数据,三步骤
    62. def get_data(self):
    63. while True:
    64. # 判断队列不为空则执行,否则终止
    65. if not self.q.empty():
    66. url = self.q.get()
    67. headers = {'User-Agent':UserAgent().random}
    68. html = requests.get(url=url,headers=headers)
    69. res_html = html.content.decode(encoding='utf-8')
    70. html=json.loads(res_html)
    71. self.parse_html(html)
    72. else:
    73. break
    74. # 解析函数
    75. def parse_html(self,html):
    76. # 写入到csv文件
    77. app_list = []
    78. for app in html['data']:
    79. # app名称 + 分类 + 详情链接
    80. name = app['displayName']
    81. link = 'http://app.mi.com/details?id=' + app['packageName']
    82. typ_name = app['level1CategoryName']
    83. # 把每一条数据放到app_list,并通过writerows()实现多行写入
    84. app_list.append([name,typ_name,link])
    85. print(name,typ_name)
    86. self.i += 1
    87. # CSV文件中写入数据
    88. self.lock.acquire()
    89. self.writer.writerows(app_list)
    90. self.lock.release()
    91. # 入口函数
    92. def main(self):
    93. # URL入队列
    94. self.get_cateid()
    95. t_list = []
    96. # 创建多线程
    97. for i in range(1):
    98. t = Thread(target=self.get_data)
    99. t_list.append(t)
    100. # 启动线程
    101. t.start()
    102. for t in t_list:
    103. # 回收线程   
    104. t.join()
    105. self.f.close()
    106. print('数量:',self.i)
    107. if __name__ == '__main__':
    108. start = time.time()
    109. spider = XiaomiSpider()
    110. spider.main()
    111. end = time.time()
    112. print('执行时间:%.1f' % (end-start))

    运行上述程序后,打开存储文件,其内容如下:

    在我们之间-单机版,休闲创意,http://app.mi.com/details?id=com.easybrain.impostor.gtx

     

    粉末游戏,模拟经营,http://app.mi.com/details?id=jp.danball.powdergameviewer.bnn

     

    三国杀,棋牌桌游,http://app.mi.com/details?id=com.bf.sgs.hdexp.mi

     

    腾讯欢乐麻将全集,棋牌桌游,http://app.mi.com/details?id=com.qqgame.happymj

     

    快游戏,休闲创意,http://app.mi.com/details?id=com.h5gamecenter.h2mgc

     

    皇室战争,战争策略,http://app.mi.com/details?id=com.supercell.clashroyale.mi

     

    地铁跑酷,跑酷闯关,http://app.mi.com/details?id=com.kiloo.subwaysurf

    30Python BS4解析库用法详解

    Beautiful Soup 简称 BS4(其中 4 表示版本号)是一个 Python 第三方库,它可以从 HTML 或 XML 文档中快速地提取指定的数据。Beautiful Soup 语法简单,使用方便,并且容易理解,因此您可以快速地学习并掌握它。本节我们讲解 BS4 的基本语法。


    1:BS4官网LOGO图

    BS4下载安装

    由于 Bautiful Soup 是第三方库,因此需要单独下载,下载方式非常简单,执行以下命令即可安装:

    pip install bs4

    由于 BS4 解析页面时需要依赖文档解析器,所以还需要安装 lxml 作为解析库:

    pip install lxml

    Python 也自带了一个文档解析库 html.parser, 但是其解析速度要稍慢于 lxml。除了上述解析器外,还可以使用 html5lib 解析器,安装方式如下:

    pip install html5lib

    该解析器生成 HTML 格式的文档,但速度较慢。

    “解析器容错”指的是被解析的文档发生错误或不符合格式时,通过解析器的容错性仍然可以按照既定的正确格式实现解析。

    BS4解析对象

    创建 BS4 解析对象是万事开头的第一步,这非常地简单,语法格式如下所示:

    1. #导入解析包
    2. from bs4 import BeautifulSoup
    3. #创建beautifulsoup解析对象
    4. soup = BeautifulSoup(html_doc, 'html.parser')

    上述代码中,html_doc 表示要解析的文档,而 html.parser 表示解析文档时所用的解析器,此处的解析器也可以是 'lxml' 或者 'html5lib',示例代码如下所示:

    1. #coding:utf8
    2. html_doc = """
    3. "c<span style="font-family: 宋体">语言中文网</span><span style="font-family: Calibri">"
    4. c.biancheng.net

    5. 一个学习编程的网站

    6. python教程
    7. c语言教程
    8. """
    9. from bs4 import BeautifulSoup
    10. soup = BeautifulSoup(html_doc, 'html.parser')
    11. #prettify()用于格式化输出html/xml文档
    12. print(soup.prettify())

    输出结果:

    1. </strong></li> <li>"c<span style="font-family: 宋体">语言中文网</span><span style="font-family: Calibri">"</span></li> <li><strong>
    2.  class="title">
    3. c.biancheng.net
    4.  class="website">
    5. 一个学习编程的网站
    6.  href="http://c.biancheng.net/python/" id="link1">
    7. python教程
    8.  href="http://c.biancheng.net/c/" id="link2">
    9. c语言教程

    如果是外部文档,您也可以通过 open() 的方式打开读取,语法格式如下:

    soup = BeautifulSoup(open('html_doc.html', encoding='utf8'), 'lxml')

    BS4常用语法

    下面对爬虫中经常用到的 BS4 解析方法做详细介绍。

    Beautiful Soup 将 HTML 文档转换成一个树形结构,该结构有利于快速地遍历和搜索 HTML 文档。下面使用树状结构来描述一段 HTML 文档:

    c语言中文网

    c.biancheng.net

    一个学习编程的网站

    树状图如下所示:


    1:HTML文档树结构图
     

    文档树中的每个节点都是 Python 对象,这些对象大致分为四类:Tag , NavigableString , BeautifulSoup , Comment 。其中使用最多的是 Tag 和 NavigableString。

    • Tag:标签类,HTML 文档中所有的标签都可以看做 Tag 对象。
    • NavigableString:字符串类,指的是标签中的文本内容,使用 text、string、strings 来获取文本内容。
    • BeautifulSoup:表示一个 HTML 文档的全部内容,您可以把它当作一个人特殊的 Tag 对象。
    • Comment:表示 HTML 文档中的注释内容以及特殊字符串,它是一个特殊的 NavigableString。

    1) Tag节点

    标签(Tag)是组成 HTML 文档的基本元素。在 BS4 中,通过标签名和标签属性可以提取出想要的内容。看一组简单的示例:

    1. from bs4 import BeautifulSoup
    2. soup = BeautifulSoup('

      c.biancheng.net

      ', 'html.parser')
    3. #获取整个p标签的html代码
    4. print(soup.p)
    5. #获取b标签
    6. print(soup.p.b)
    7. #获取p标签内容,使用NavigableString类中的stringtextget_text()
    8. print(soup.p.text)
    9. #返回一个字典,里面是多有属性和值
    10. print(soup.p.attrs)
    11. #查看返回的数据类型
    12. print(type(soup.p))
    13. #根据属性,获取标签的属性值,返回值为列表
    14. print(soup.p['class'])
    15. #class属性赋值,此时属性值由列表转换为字符串
    16. soup.p['class']=['Web','Site']
    17. print(soup.p)

    输出结果如下:

    soup.p输出结果:

    c.biancheng.net

     

    soup.p.b输出结果:

    c.biancheng.net

     

    soup.p.text输出结果:

    c.biancheng.net

     

    soup.p.attrs输出结果:

    {'class': ['Web', 'site', 'url']}

     

    type(soup.p)输出结果:

     

    soup.p['class']输出结果:

    ['Web', 'site', 'url']

     

    class属性重新赋值:

    c.biancheng.net

    遍历节点

    Tag 对象提供了许多遍历 tag 节点的属性,比如 contents、children 用来遍历子节点;parent 与 parents 用来遍历父节点;而 next_sibling 与 previous_sibling 则用来遍历兄弟节点 。示例如下:

    1. #coding:utf8
    2. from bs4 import BeautifulSoup
    3. html_doc = """
    4. "c<span style="font-family: 宋体">语言中文网</span><span style="font-family: Calibri">"
    5. c.biancheng.net

    6. 一个学习编程的网站

    7. python教程,
    8. c语言教程 and
    9. """
    10. soup = BeautifulSoup(html_doc, 'html.parser')
    11. body_tag=soup.body
    12. print(body_tag)
    13. #以列表的形式输出,所有子节点
    14. print(body_tag.contents)

    输出结果:

    c.biancheng.net

    一个学习编程的网站

    python教程,

    c语言教程 and

    #以列表的形式输出

    ['\n',

    c.biancheng.net

    , '\n',

    一个学习编程的网站

    , '\n', python教程, '\n', c语言教程, '\n']

    Tag 的 children 属性会生成一个可迭代对象,可以用来遍历子节点,示例如下:

    1. for child in body_tag.children:
    2. print(child)

    输出结果:

    #注意此处已将换行符"\n"省略

    c.biancheng.net

    一个学习编程的网站

    想了解更多相关示例可参考官方文档:点击前往

    find_all()与find()

    find_all() 与 find() 是解析 HTML 文档的常用方法,它们可以在 HTML 文档中按照一定的条件(相当于过滤器)查找所需内容。find() 与 find_all() 的语法格式相似,希望大家在学习的时候,可以举一反三。

    BS4 库中定义了许多用于搜索的方法,find() 与 find_all() 是最为关键的两个方法,其余方法的参数和使用与其类似。

    1) find_all()

    find_all() 方法用来搜索当前 tag 的所有子节点,并判断这些节点是否符合过滤条件,最后以列表形式将符合条件的内容返回,语法格式如下:

    find_all( name , attrs , recursive , text , limit )

    参数说明:

    • name:查找所有名字为 name 的 tag 标签,字符串对象会被自动忽略。
    • attrs:按照属性名和属性值搜索 tag 标签,注意由于 class 是 Python 的关键字吗,所以要使用 "class_"。
    • recursive:find_all() 会搜索 tag 的所有子孙节点,设置 recursive=False 可以只搜索 tag 的直接子节点。
    • text:用来搜文档中的字符串内容,该参数可以接受字符串 、正则表达式 、列表、True。
    • limit:由于 find_all() 会返回所有的搜索结果,这样会影响执行效率,通过 limit 参数可以限制返回结果的数量。


    find_all() 使用示例如下:

    1. from bs4 import BeautifulSoup
    2. import re
    3. html_doc = """
    4. "c<span style="font-family: 宋体">语言中文网</span><span style="font-family: Calibri">"
    5. c.biancheng.net

    6. 一个学习编程的网站

    7. python教程
    8. c语言教程
    9. django教程
    10. 加入我们阅读所有教程

    11. 成为vip
    12. """
    13. #创建soup解析对象
    14. soup = BeautifulSoup(html_doc, 'html.parser')
    15. #查找所有a标签并返回
    16. print(soup.find_all("a"))
    17. #查找前两条a标签并返回
    18. print(soup.find_all("a",limit=2))
    19. #只返回两条a标签

    最后以列表的形式返回输出结果,如下所示:

    [python教程, c语言教程, django教程, 成为vip]

     

    [python教程, c语言教程]

    按照标签属性以及属性值查找 HTML 文档,如下所示

    1. print(soup.find_all("p",class_="website"))
    2. print(soup.find_all(id="link4"))

    输出结果:

    [

    一个学习编程的网站

    ]

    [成为vip]

    正则表达式、列表,以及 True 也可以当做过滤条件,使用示例如下:

    1. #列表行书查找tag标签
    2. print(soup.find_all(['b','a']))
    3. #正则表达式匹配id属性值
    4. print(soup.find_all('a',id=re.compile(r'.\d')))
    5. print(soup.find_all(id=True))
    6. #True可以匹配任何值,下面代码会查找所有tag,并返回相应的tag名称
    7. for tag in soup.find_all(True):
    8. print(tag.name,end=" ")
    9. #输出所有以b开始的tag标签
    10. for tag in soup.find_all(re.compile("^b")):
    11. print(tag.name)

    输出结果如下:

    第一个print输出:

    [c.biancheng.net, python教程, c语言教程, django教程, 成为vip]

    第二个print输出:

    [python教程, c语言教程, django教程, 成为vip]

    第三个print输出:

    [python教程, c语言教程, django教程, 成为vip]

    第四个print输出:

    html head title body p b p a a a p a

    最后一个输出:

    body b

    BS4 为了简化代码,为 find_all() 提供了一种简化写法,如下所示:

    1. #简化前
    2. soup.find_all("a")
    3. #简化后
    4. soup("a")

    上述两种的方法的输出结果是相同的。

    2) find()

    find() 方法与 find_all() 类似,不同之处在于 find_all() 会将文档中所有符合条件的结果返回,而 find() 仅返回一个符合条件的结果,所以 find() 方法没有limit参数。使用示例如下:

    1. from bs4 import BeautifulSoup
    2. import re
    3. html_doc = """
    4. "c<span style="font-family: 宋体">语言中文网</span><span style="font-family: Calibri">"
    5. c.biancheng.net

    6. 一个学习编程的网站

    7. python教程
    8. c语言教程
    9. django教程
    10. 加入我们阅读所有教程

    11. 成为vip
    12. """
    13. #创建soup解析对象
    14. soup = BeautifulSoup(html_doc, 'html.parser')
    15. #查找第一个a并直接返回结果
    16. print(soup.find('a'))
    17. #查找title
    18. print(soup.find('title'))
    19. #匹配指定href属性的a标签
    20. print(soup.find('a',href='http://c.biancheng.net/python/'))
    21. #根据属性值正则匹配
    22. print(soup.find(class_=re.compile('tit')))
    23. #attrs参数值
    24. print(soup.find(attrs={'class':'vip'}))

    输出结果如下:

    a标签:

    python教程

    指定href属性:

    python教程

    title:

    "c语言中文网"

    正则匹配:

    c.biancheng.net

    #attrs参数值

    加入我们阅读所有教程

    使用 find() 时,如果没有找到查询标签会返回 None,而 find_all() 方法返回空列表。示例如下:

    1. print(soup.find('bdi'))
    2. print(soup.find_all('audio'))

    输出结果如下:

    None

    []

    BS4 也为 find()提供了简化写法,如下所示:

    1. #简化写法
    2. print(soup.head.title)
    3. #上面代码等价于
    4. print(soup.find("head").find("title"))

    两种写法的输出结果相同,如下所示:

    "c语言中文网"

    "c语言中文网"

    CSS选择器

    BS4 支持大部分的 CSS 选择器,比如常见的标签选择器、类选择器、id 选择器,以及层级选择器。Beautiful Soup 提供了一个 select() 方法,通过向该方法中添加选择器,就可以在 HTML 文档中搜索到与之对应的内容。应用示例如下:

    1. #coding:utf8
    2. html_doc = """
    3. "c<span style="font-family: 宋体">语言中文网</span><span style="font-family: Calibri">"
    4. c.biancheng.net

    5. 一个学习编程的网站

    6. python教程
    7. c语言教程
    8. django教程
    9. 加入我们阅读所有教程

    10. 成为vip
    11. 介绍:

    12. 关于网站
    13. 关于站长
    14. """
    15. from bs4 import BeautifulSoup
    16. soup = BeautifulSoup(html_doc, 'html.parser')
    17. #根据元素标签查找
    18. print(soup.select('title'))
    19. #根据属性选择器查找
    20. print(soup.select('a[href]'))
    21. #根据类查找
    22. print(soup.select('.vip'))
    23. #后代节点查找
    24. print(soup.select('html head title'))
    25. #查找兄弟节点
    26. print(soup.select('p + a'))
    27. #根据id选择p标签的兄弟节点
    28. print(soup.select('p ~ #link3'))
    29. #nth-of-type(n)选择器,用于匹配同类型中的第n个同级兄弟元素
    30. print(soup.select('p ~ a:nth-of-type(1)'))
    31. #查找子节点
    32. print(soup.select('p > a'))
    33. print(soup.select('.introduce > #link5'))

    输出结果:

    第一个输出:

    ["c语言中文网"]

     

    第二个输出:

    [python教程, c语言教程, django教程, 成为vip, 关于网站, 关于站长]

     

    第三个输出:

    [

    加入我们阅读所有教程

    ]

     

    第四个输出:

    ["c语言中文网"]

     

    第五个输出:

    [python教程, 成为vip]

     

    第六个输出:

    [django教程]

     

    第七个输出:

    [python教程]

     

    第八个输出:

    [关于网站, 关于站长]

     

    最后的print输出:

    [关于网站]

    如果想了解更多关于 BS4 库的使用方法,可以参考官方文档:https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/#

    31Pyhon爬虫下载小说

    案例简单分析

    首先判网站属于静态网站,因此您的主要任务是分析网页元素的组成,然后使用 BS4 提取所需的信息。如下所示:


    1:网页元素分析


    提取到 a 标签是解决本程序的重点,a 标签的页面代码结构如下所示:

    1.  class="book-mulu">
      •  href="/book/liangjinyanyi/1.html">自序
      •  href="/book/liangjinyanyi/2.html">第一回 祀南郊司马开基 立东宫庸雏伏祸
      •  href="/book/liangjinyanyi/3.html">第二回 堕诡计储君纳妇 慰痴情少女偷香
      • ...

    从上述代码可以看出,a 标签包含了目录名称以及详情页的地址链接。那么如何获取 a 标签呢?经过简单分析后可知 a 标签属于 div > ul > li 的子节点,因此可以使用 BS4 的 select() 获取。如下所示:

    list_name = soup.select('.book-mulu > ul > li > a')

    上述代码的返回值是一个列表,列表中每一个元素都是一个 Tag 对象,类型为 

    下载详情页的 URL 也非常容易获得,它是由发起请求的 URL 与 a 标签的 herf 链接拼接而成。因此通过字符串拼接就可以获取下载详内容页的 URL。

    https://www.shicimingju.com/book/liangjinyanyi/2.html

    https://www.shicimingju.com/book/liangjinyanyi/3.html

    最后一步是提取具体的内容。通过分析详情页的元素构成可知,我们想要的内容都包含在以下标签中:

    具体内容

    因此使用 BS4 的 find() 方法就可以获取所需内容,如下所示:

    artist = soup.find('div', class_='chapter_content')

    之后把获取的内容写入到 txt 文件中就可以了。下面我使用之前学习过的 urllib 模块与 BS4 模块编写爬虫程序,这样才能做到温故而知新。

    编写爬虫程序

    代码如下所示,程序中已经做了详细的注释:

    1. import urllib.request
    2. import random
    3. from bs4 import BeautifulSoup
    4. import time
    5. def request_html(url):
    6. headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36'}
    7. request = urllib.request.Request(url, headers=headers)
    8. return request
    9. def parse_html(html, f):
    10. # 生成soup对象
    11. soup = BeautifulSoup(html, 'lxml')
    12. # 查找所有的章节链接和标题内容
    13. list_name = soup.select('.book-mulu > ul > li > a')
    14. # 遍历每一个列表中的tag对象,获取链接个目录
    15. for item in list_name:
    16. # 获取链接
    17. #item: 自序
    18. #拼接目录链接,此处item类型为,使用下面方法可以值获取href属性值
    19. href = 'http://www.shicimingju.com' + item['href']
    20. # 获取标题
    21. title = item.text
    22. print('正在下载:-**--%s--**-......' % title)
    23. # 获取章节内容函数
    24. text = get_text(href)
    25. # 写入文件
    26. f.write(title + '\n' + text)
    27. print('结束下载:-**--%s--**-' % title)
    28. time.sleep(random.uniform(0,1))
    29. # 提取章节内容
    30. def get_text(href):
    31. #创建请求对象
    32. request = request_html(href)
    33. content = urllib.request.urlopen(request).read().decode('utf8')
    34. soup = BeautifulSoup(content, 'lxml')
    35. # 查找包含内容的tag--div
    36. artist = soup.find('div', class_='chapter_content')
    37. #获取tag标签中的文本内容
    38. return artist.text
    39. def run():
    40. # 打开文件
    41. f = open('两晋演义.txt', 'w', encoding='utf8')
    42. url = 'http://www.shicimingju.com/book/liangjinyanyi.html'
    43. # 构建请求对象
    44. request = request_html(url)
    45. # 发送请求,得到响应,转换为HTML对象
    46. html = urllib.request.urlopen(request).read().decode('utf8')
    47. # 解析内容
    48. parse_html(html,f)
    49. #关闭文件
    50. f.close()
    51. if __name__ == '__main__':
    52. run()

    程序运行结果:

    正在下载:-**--自序--**-......

    结束下载:-**--自序--**-

    正在下载:-**--第一回 祀南郊司马开基 立东宫庸雏伏祸--**-......

    结束下载:-**--第一回 祀南郊司马开基 立东宫庸雏伏祸--**-

    正在下载:-**--第二回 堕诡计储君纳妇 慰痴情少女偷香--**-......

    ....

    由于生成的 .txt 文件中内容过多,这里就不再做展示了。

     

    32Python Selenium的下载和安装

    Selenium 是一个用于测试 Web 应用程序的自动化测试工具,它直接运行在浏览器中,实现了对浏览器的自动化操作,它支持所有主流的浏览器,包括 IE,Firefox,Safari,Chrome 等。

    Selenium 支持所有主流平台(如,Windows、Linux、IOS、Android、Edge、Opera等),同时,它也实现了诸多自动化功能,比如软件自动化测试,检测软件与浏览器兼容性,自动录制、生成不同语言的测试脚本,以及自动化爬虫等。本节及后续两节主要围绕自动化爬虫展开讲解。


    1:Python Selenium


    Selenium 提供了一个工具集,包括 Selenium WebDriver(浏览器驱动)、Selenium IDE(录制测试脚本)、Selenium Grid(执行测试脚本)。后面两个主要用于测试脚本的录制、执行,因此不做介绍。我们只对 Selenium WebDriver 做重点讲解。

    关于 Selenium IDE/Grid 的相关知识可参考官网文档 https://www.selenium.dev/

    Selenium下载安装

    Selenium 安装非常简单,Linux、Mac 用户执行以下命令即可:

    sudo pip install Selenium

    Windows 用户执行以下命令即可实现安装:

    python -m pip install selenium

    除了使用上述命令安装外,您也可以通过官方网站下载 Selenium WebDriver 安装包,点击前往下载

    安装浏览器驱动

    若想使 Selenium 能够调用浏览器,那么必须通过 webdriver 驱动来实现。不同的浏览器需要使用不同驱动程序,下面列出了不同浏览器驱动程序的下载地址:

    • 谷歌浏览器 chromedrive:http://chromedriver.storage.googleapis.com/index.html
    • 火狐浏览器 geckodriver:https://github.com/mozilla/geckodriver/releases
    • IE 浏览器 IEDriver:http://selenium-release.storage.googleapis.com/index.html


    各种浏览器的驱动安装规程基本一致。不过需要注意:安装 Chrome、Firefox 驱动时,需要下载与浏览器版本相匹配的驱动程序,否则不能驱动浏览器。而 IE 较为特殊,您需要下载与 Selenium 版本相匹配的驱动文件,如下所示:


    1:IE 驱动下载


    下面以 Windows10 平台 Chrome 浏览器为例讲解。首先检查浏览器版本号,并下载相应驱动文件,然后解压文件,将 Chromedriver.exe 文件拷贝到 Python 安装目录的 Scripts 目录下,最后将其添加到系统环境变量中。使用如下命令可查看 Python 安装路径:

    where python

    您需要根据自己安装路径进行配置,我的环境变量配置如下:


    2:配置环境变量
     

    上述操作完成后,在 CMD 命令行启动驱动程序,如下所示:


    3:驱动开启成功
     

    开启成功后,驱动程序会在后台运行。

    自动访问百度

    编写如下代码,实现自动化访问百度。

    1. # 导入seleinum webdriver接口
    2. from selenium import webdriver
    3. import time
    4. # 创建Chrome浏览器对象
    5. browser = webdriver.Chrome()
    6. #访问百度网站
    7. browser.get('http://www.baidu.com/')
    8. #阻塞3
    9. time.sleep(3)
    10. # 自动退出浏览器
    11. browser.quit()

    截取了运行中的显示结果,如下所示:


    4:Selenium自动化访问百度


    经过上述代测试,说明我们安装的浏览器驱动可以正常工作。Selenium WebDriver 实现了许多操作浏览器功能。比如实现自动点击、自动输入、自动搜索、自动登录等等。

    毫不夸张的说,Selenium 自动化爬虫是一种万能的爬虫程序,它可以仿照人的样子去打开网站,并拿到你想要的数据,因此你无须在意反爬措施。不过它最致命的缺点就是效率很低,因为每次点击、输入等操作都需要花费一定的时间,因此它仅适用于小批量的数据抓取。

     

    33Python Selenium基本用法

    Selenium 作为一款 Web 自动化测试框架,提供了诸多操作浏览器的方法,本节对其中的常用方法做详细介绍。

    定位节点

    Selenium 提供了 8 种定位单个节点的方法,如下所示:

    定位节点方法

    方法

    说明

    find_element_by_id()

    通过 id 属性值定位

    find_element_by_name()

    通过 name 属性值定位

    find_element_by_class_name()

    通过 class 属性值定位

    find_element_by_tag_name()

    通过 tag 标签名定位

    find_element_by_link_text()

    通过标签内文本定位,即精准定位。

    find_element_by_partial_link_text()

    通过标签内部分文本定位,即模糊定位。

    find_element_by_xpath()

    通过 xpath 表达式定位

    find_element_by_css_selector()

    通过 css 选择器定位


    假设下面代码某个页面的代码片段,如下所示:

    1.  link="#cc0916">
    2.  id="logo" href="http://c.biancheng.net" onclick="">
    3.  id="form" class="fm" name="f" action="c.biancheng.net">
    4.  class="btn">
    5.  id="kw" class="s_ipt_wr" name="wd" value="" maxlength="255" autocomplete="off">

    下面使用表格中提供的方法定位 input 输出框。如下所示:

    1. #创建browewr是浏览器对象
    2. browser = webdriver.Chrome()
    3. #访问某个url得到上述代码片段
    4. browser.get('url')
    5. #通过id定义输入框
    6. brower.dr.find_element_by_id("kw")
    7. #通过class定义
    8. brower.find_element_by_class_name("s_ipt_wr")
    9. #通过name定位
    10. brower.find_element_by_name("wd")
    11. #通过tag name定位:
    12. brower.find_element_by_tag_name("input")
    13. #通过xpath定位
    14. brower.find_element_by_xpath("//*[@id='kw']")
    15. #通过css选择器定位
    16. brower.find_element_by_css_selector("#kw")

    通过 a 标签内的文本内容定位节点,如下所示:

    C语言中文网

    hao123

    示例如下:

    1. #使用全部文本内容定位链接
    2. brower.find_element_by_link_text("c语言中文网")
    3. #使用部分文本内容定位链接
    4. brower.find_element_by_partial_link_text("123")

    如果您想定位一组元素,方法如下所示:

    find_elements_by_id()

    find_elements_by_name()

    find_elements_by_class_name()

    find_elements_by_tag_name()

    find_elements_by_link_text()

    find_elements_by_partial_link_text()

    find_elements_by_xpath()

    find_elements_by_css_selector()

    定位一组元素的方法与定位单个元素类似,唯一的区别就是 element 后面多了一个 s(表示复数),因此上述方法的返回值是一个列表,您可以使用 for 循环拿到所有的元素节点。

    控制浏览器

    Selenium 可以操控浏览器的窗口大小、刷新页面,以及控制浏览器的前进、后退等

    1) 设置浏览器窗口大小、位置

    1. from selenium import webdriver
    2. driver = webdriver.Chrome()
    3. driver.get("http://www.baidu.com")
    4. #参数数字为像素点
    5. driver.set_window_size(480, 800)
    6. #设置窗口位置
    7. driver.set_window_position(100,200)
    8. #同时设置窗口的大小和坐标
    9. driver.set_window_rect(450,300,32,50)
    10. #退出浏览器
    11. driver.quit()
    12. from selenium import webdriver
    13. driver = webdriver.Chrome()
    14. # 访问C语言中文网首页
    15. first_url= 'http://c.biancheng.net'
    16. driver.get(first_url)
    17. # 访问c语言教程
    18. second_url='http://c.biancheng.net/c/'
    19. driver.get(second_url)
    20. # 返回(后退)到c语言中文网首页
    21. driver.back()
    22. # 前进到C语言教程页
    23. driver.forward()
    24. # 刷新当前页面相当于F5
    25. driver.refresh() 
    26. # 退出/关闭浏览器
    27. driver.quit()

    2) 控制网页前进、后退、刷新页面

    WebDriver常用方法

    上文中介绍了如何定位元素,以及如何设置浏览的大小、位置。 定位元素节点只是第一步, 定位之后还需要对这个元素进行操作, 比如单击按钮,或者在输入框输入文本 , 下面介绍 WebDriver 中的最常用方法:

    # 请求url

    get(url)

    # 模拟键盘输入文本

    send_keys (value)

    # 清除已经输入的文本

    clear():

    # 单击已经定位的元素

    click():

    # 用于提交表单,比如百度搜索框内输入关键字之后的“回车” 操作

    submit():

    #返回属性的属性值,返回元素的属性值,可以是id、name、type 或其他任意属性

    get_attribute(name)

    # 返回布尔值,检查元素是否用户可见,比如 display属性为hidden或者none

    is_displayed()

    示例如下:

    1. from selenium import webdriver
    2. import time
    3. driver = webdriver.Chrome()
    4. driver.get("https://www.baidu.com")
    5. #模拟键盘,输出文本
    6. driver.find_element_by_id("kw").send_keys("C语言中文网")
    7. #单击百度一下查找
    8. driver.find_element_by_id("su").click()
    9. time.sleep(3)
    10. #退出浏览器
    11. driver.quit()

    除了上述方法外, WebDriver 还有一些常用属性,如下所示:

    1. from selenium import webdriver
    2. driver = webdriver.Chrome()
    3. # 获取HTML结构源码
    4. driver.page_source
    5. #在源码中查找指定的字符串
    6. driver.page_source.find('字符串')
    7. # 返回百度页面底部备案信息
    8. text = driver.find_element_by_id("cp").text
    9. print(text)
    10. # 获取输入框的尺寸
    11. size = driver.find_element_by_id('kw').size
    12. print(size)

    输出结果:

    ?2015 Baidu 使用百度前必读 意见反馈 京 ICP 证 030173 号

     

    {'width': 500, 'height': 22}

    Selenium事件处理

    Selenium WebDriver 提供了一些事件处理函数(鼠标、键盘等),下面我们对常用的事件函数做简单介绍。

    1) 鼠标事件

    Selenium WebDriver 将关于鼠标的操作方法都封装在 ActionChains 类中,使用时需要引入 ActionChains 类,如下所示:

    from selenium.webdriver.common.action_chains import ActionChains

    该类包含了鼠标操作的常用方法:

    鼠标事件

    方法

    说明

    ActionChains(driver)

    构造 ActionChains 鼠标对象。

    click()

    单击

    click_and_hold(on_element=None) 

    单击鼠标左键,不松开

    context_click()

    右击

    double_click()

    双击

    drag_and_drop()

    拖动

    move_to_element(above)

    执行鼠标悬停操作

    context_click()

    用于模拟鼠标右键操作, 在调用时需要指定元素定位。

    perform()

    将所有鼠标操作提交执行。


    示例如下:

    1. from selenium import webdriver
    2. #导入 ActionChains
    3. from selenium.webdriver.common.action_chains import ActionChains
    4. driver = webdriver.Chrome()
    5. driver.get("http://c.biancheng.net")
    6. # 通过xpath表达式定位到要悬停的元素
    7. above = driver.find_element_by_xpath('//ul[@id="ad-link-top"]/li[1]')
    8. # 对定位到的元素执行鼠标悬停操作
    9. ActionChains(driver).move_to_element(above).perform()

    2) 键盘事件

    Selenium  WebDriver 的 Keys 模块提供了模拟键盘输入的 send_keys() 方法,除此之外,该模块也提供了操作键盘的其他方法,比如复制、粘贴等等。

    在使用之前,首先需要导入 Keys 类,如下所示:

    from selenium.webdriver.common.keys import Keys

    下面列举了一些常用方法:

    键盘操作

    方法

    说明

    send_keys(Keys.BACK_SPACE)

    删除键(BackSpace)

    send_keys(Keys.SPACE)

    空格键(Space)

    send_keys(Keys.TAB)

    制表键(Tab)

    send_keys(Keys.ESCAPE)

    回退键(Esc)

    send_keys(Keys.ENTER)

    回车键(Enter)

    send_keys(Keys.CONTROL,'a')

    全选(Ctrl+A)

    send_keys(Keys.CONTROL,'c')

    复制(Ctrl+C)

    send_keys(Keys.CONTROL,'x')

    剪切(Ctrl+X)

    send_keys(Keys.CONTROL,'v')

    粘贴(Ctrl+V)

    send_keys(Keys.F1…Fn)

    键盘 F1…Fn

    keys.down(value,element=None)

    按下键盘上的某个键

    keys.up(value,element=None)

    松开键盘上的某个键


    示例如下:

    1. from selenium import webdriver
    2. # 引入 Keys 模块
    3. from selenium.webdriver.common.keys import Keys
    4. driver = webdriver.Chrome()
    5. driver.get("http://www.baidu.com")
    6. # 输入框输入内容
    7. driver.find_element_by_id("kw").send_keys("C语言中文网H")
    8. # 删除多输入的一个H
    9. driver.find_element_by_id("kw").send_keys(Keys.BACK_SPACE)
    10. #单击百度一下查找
    11. driver.find_element_by_id("su").click()
    12. time.sleep(3)
    13. driver.quit()

    其它键盘操作方法,如下所示:

    1. # 输入空格键 + “Python教程
    2. driver.find_element_by_id("kw").send_keys(Keys.SPACE)
    3. driver.find_element_by_id("kw").send_keys("Python教程")
    4. # ctrl+a 全选输入框内容
    5. driver.find_element_by_id("kw").send_keys(Keys.CONTROL, 'a')
    6. # ctrl+x 剪切输入框内容
    7. driver.find_element_by_id("kw").send_keys(Keys.CONTROL, 'x')
    8. # ctrl+v 粘贴内容到输入框
    9. driver.find_element_by_id("kw").send_keys(Keys.CONTROL, 'v')
    10. # 使用回车键来代替单击操作click
    11. driver.find_element_by_id("su").send_keys(Keys.ENTER)

    无界面浏览器

    Chromedriver 每一次运行都要打开浏览器,并执行相应的输入、搜索等操作,这样会导致浏览器交互能力变差,浪费许多时间。 Selenium 为了增强浏览器的交互能力,允许您使用无头浏览器模式,也就是无界面浏览器,它被广泛的应用于爬虫和自动化测试中。通过以下代码可以设置无头浏览器模式:

    1. from selenium import webdriver
    2. import time
    3. options=webdriver.ChromeOptions()
    4. options.add_argument('--headless')#无界面浏览
    5. driver=webdriver.Chrome(options=options)
    6. driver.get('https://www.baidu.com')
    7. kw1=driver.find_element_by_id('kw')
    8. print(driver.title)
    9. time.sleep(3)
    10. #关闭当前界面,只有一个窗口
    11. driver.close()
    12. #关闭所有界面
    13. driver.quit()

    除了可以设置无头界面之外,Selenium 还支持其他一些浏览器参数设置,如下所示:

    opption.add_argument('--window-size=600,600') #设置窗口大小

    opption.add_argument('--incognito') #无痕模式

    opption.add_argument('--disable-infobars') #去掉chrome正受到自动测试软件的控制的提示

    opption.add_argument('user-agent="XXXX"') #添加请求头

    opption.add_argument("--proxy-server=http://200.130.123.43:3456")#代理服务器访问

    opption.add_experimental_option('excludeSwitches', ['enable-automation'])#开发者模式

    opption.add_argument('blink-settings=imagesEnabled=false')  #禁止加载图片

    opption.add_argument('lang=zh_CN.UTF-8') #设置默认编码为utf-8

    opption.add_extension(create_proxyauth_extension(

               proxy_host='host',

               proxy_port='port',

               proxy_username="username",

               proxy_password="password"

           ))# 设置有账号密码的代理

    opption.add_argument('--disable-gpu')  # 这个参数可以规避谷歌的部分bug

    opption.add_argument('--disable-javascript')  # 禁用javascript

    opption.add_argument('--hide-scrollbars')  # 隐藏滚动条

    执行JS脚本

    WebDriver 提供了 execute_script() 方法来执行 JavaScript 代码,比如控制浏览器的滚动条。示例如下:

    1. from selenium import webdriver
    2. from time import sleep
    3. # 访问百度
    4. driver=webdriver.Chrome()
    5. driver.get("http://www.baidu.com")
    6. # 最大化浏览器窗口
    7. driver.maximize_window()
    8. # 搜索
    9. driver.find_element_by_id("kw").send_keys("C语言中文网")
    10. driver.find_element_by_id("su").click()
    11. sleep(3)
    12. # 通过js代码设置滚动条位置,数值代表(左边距,上边距)
    13. js="window.scrollTo(100,500);"
    14. #执行js代码
    15. driver.execute_script(js)
    16. sleep(5)
    17. driver.quit()

    如果想了解更多关于 Selenium 的知识,请参考官方文档:https://www.selenium.dev/documentation/en/

     

    34Python Selenium爬虫实战应用

    本节讲解 Python Selenium 爬虫实战案例,通过对实战案例的讲解让您进一步认识 Selenium 框架。

    实战案例目标:抓取京东商城(https://www.jd.com/)商品名称、商品价格、评论数量,以及商铺名称。比如输入搜索“Python书籍”,则抓取如下数据:

    {'name': 'Python编程 从入门到实践 第2版 人民邮电出版社', 'price': '¥52.50', 'count': '200+条评价', 'shop': '智囊图书专营店'}

    {'name': 'Python编程 从入门到实践 第2版(图灵出品)', 'price': '¥62.10', 'count': '20万+条评价', 'shop': '人民邮电出版社'}

    ...

    Selenium 框架的学习重点在于定位元素节点,关于如何定位,我们已经介绍了 8 种方法,其中 Xpath 表达式适用性强,并且便捷。因此,建议大家多多熟悉 Xpath 表达式的相关语法规则。本节案例中很大一部分采用了 Xpath 表达式定位元素,希望帮助您温故知新。

    本节案例中涉及了几个技术难点:第一,如何下拉滚动条下载商品,第二,如何实现翻页,也就是抓取下一页的内容,第三,如何判断数据已经抓取完毕,即终止页。下面我们逐步讲解。

    实现自动搜索

    实现自动输出、自动搜索是最基础的一步。首先定位输入框的的节点,其次定位搜索按钮节点,这与实现百度自动搜索思路一致,最关键就是要正确定位元素节点。

    通过开发者调试工具检查相应的的位置,可得如下 Xpath 表达式:

    输入框表达式://*[@id="key"]

    搜索按钮表达式://*[@class='form']/button

    代码如下所示:

    1. from selenium import webdriver
    2. broswer=webdriver.Chrome()
    3. broswer.get('https://www.jd.com/')
    4. broswer.find_element_by_xpath('//*[@id="key"]').send_keys("python书籍")
    5. broswer.find_element_by_xpath("//*[@class='form']/button").click()

    滚动滑动条

    实现了自动搜索后,接下来就是要抓取页面中的商品信息,而您会发现只有将滑动条滚动至底部,商品才会全部加载完毕。滚动滑轮操作的代码如下:

    1. # scrollTo(xpos,ypos)
    2. # execute_script()执行js语句,拉动进度条件
    3. #scrollHeight属性,表示可滚动内容的高度
    4. self.browser.execute_script(
    5. 'window.scrollTo(0,document.body.scrollHeight)'#拉动进度条至底部
    6. )

    之后在通过 Xpath 表达式匹配所有商品,并将它们放入一个大列表中,通过循环列表取出每个商品,最后提取出想要的信息。

    1. li_list=self.browser.find_elements_by_xpath('//*[@id="J_goodsList"]/ul/li')
    2. for li in li_list:
    3. item={} 
    4. # 提取商品名
    5. item['name']=li.find_element_by_xpath('.//div[@class="p-name"]/a/em').text.strip()
    6. # 提取价格
    7. item['price']=li.find_element_by_xpath('.//div[@class="p-price"]').text.strip()
    8. # 提取评论数量
    9. item['count']=li.find_element_by_xpath('.//div[@class="p-commit"]/strong').text.strip()
    10. # 提取商家店铺
    11. item['shop']=li.find_element_by_xpath('.//div[@class="p-shopnum"]').text.strip()

    实现翻页抓取

    如何实现翻页抓取数据,并判断数据数据已经抓取完毕呢?这其实并不难想到,我们可以先跳至终止页(即最后一页)。此时最后一页的“下一页”处于不可用状态,其元素节点如下:

    终止页下一页class属性:下一页 >

    其他页下一页class属性:下一页 >

    如果页面源码中有上述代码存在,则证明此页是最后一页,若没有则不是。因此通过 if ...else 语句即可实现上述需求,如下所示:

    1. #-1说明没找到,不是最后一页,执行点击 下一页操作
    2. if self.browser.page_source.find('pn-next disabled')==-1:
    3. browser.find_element_by_class_name('pn-next').click()

    完整程序代码

    完整程序代码如下所示:

    1. #coding:utf8
    2. from selenium import webdriver
    3. import time
    4. import pymongo
    5. class JdSpider(object):
    6. def __init__(self):
    7. self.url='http://www.jd.com/'
    8. self.options=webdriver.ChromeOptions() # 无头模式
    9. self.options.add_argument('--headless')
    10. self.browser=webdriver.Chrome(options=self.options) # 创建无界面参数的浏览器对象
    11. self.i=0  #计数,一共有多少件商品
    12. #输入地址+输入商品+点击按钮,切记这里元素节点是京东首页的输入栏、搜索按钮
    13. def get_html(self):
    14. self.browser.get(self.url)
    15. self.browser.find_element_by_xpath('//*[@id="key"]').send_keys('python书籍')
    16. self.browser.find_element_by_xpath("//*[@class='form']/button").click()
    17. #把进度条件拉倒最底部+提取商品信息
    18. def get_data(self):
    19. # 执行js语句,拉动进度条件
    20. self.browser.execute_script(
    21. 'window.scrollTo(0,document.body.scrollHeight)'
    22. )
    23. # 给页面元素加载时预留时间
    24. time.sleep(2)
    25. #xpath 提取每页中所有商品,最终形成一个大列表
    26. li_list=self.browser.find_elements_by_xpath('//*[@id="J_goodsList"]/ul/li')
    27. for li in li_list:
    28. #构建空字典
    29. item={}  
    30. item['name']=li.find_element_by_xpath('.//div[@class="p-name"]/a/em').text.strip()
    31. item['price']=li.find_element_by_xpath('.//div[@class="p-price"]').text.strip()
    32. item['count']=li.find_element_by_xpath('.//div[@class="p-commit"]/strong').text.strip()
    33. item['shop']=li.find_element_by_xpath('.//div[@class="p-shopnum"]').text.strip()
    34. print(item) 
    35. self.i+=1
    36. def run(self):
    37. #搜索出想要抓取商品的页面
    38. self.get_html()
    39. #循环执行点击下一页操作
    40. while True:
    41. #获取每一页要抓取的数据
    42. self.get_data()
    43. #判断是否是最一页
    44. if self.browser.page_source.find('pn-next disabled')==-1:
    45. self.browser.find_element_by_class_name('pn-next').click()
    46. #预留元素加载时间
    47. time.sleep(1)
    48. else:
    49. print('数量',self.i)
    50. break
    51. if __name__ == '__main__':
    52. spider=JdSpider()
    53. spider.run()


    输出结果如下:

    {'name': 'Python编程 从入门到实践 第2版(图灵出品) 人民邮电出版社', 'price': '¥52.50', 'count': '200+条评价', 'shop': '智囊图书专营店'}

    {'name': 'Python编程 从入门到实践 第2版(图灵出品)', 'price': '¥62.10', 'count': '20万+条评价', 'shop': '人民邮电出版社'}

    {'name': 'Python编程三剑客:Python编程从入门到实践第2版+快速上手第2版+极客编程(套装共3册)', 'price': '¥206.90', 'count': '5万+条评价', 'shop': '人民邮电出版社'}

    {'name': 'Python数据分析从入门到实践(excel高效办公)(3全彩版)', 'price': '¥46.10', 'count': '10万+条评价', 'shop': '明日科技京东自营旗舰店'}

    {'name': '零基础学Python(编程入门 项目实践 同步视频)(3全彩版)', 'price': '¥37.50', 'count': '10万+条评价', 'shop': '明日科技京东自营旗舰店'}

    {'name': 'Python编程快速上手 让繁琐工作自动化 第2版', 'price': '¥44.50', 'count': '10万+条评价', 'shop': '人民邮电出版社'}

    {'name': '现货包邮Python学习手册(原书第5版)上下册2本/计算机编程设计|8053406', 'price': '¥142.30', 'count': '100+条评价', 'shop': '互动创新图书专营店'}

    {'name': '零基础入门学习Python(第2版)', 'price': '¥70.30', 'count': '1万+条评价', 'shop': '清华大学出版社'}

    {'name': '超简单:用Python让Excel飞起来', 'price': '¥34.90', 'count': '2万+条评价', 'shop': '机械工业出版社自营官方旗舰店'}

    {'name': '流畅的Python(图灵出品)', 'price': '¥109.80', 'count': '2万+条评价', 'shop': '人民邮电出版社'}

    {'name': 'Python编程从入门到实践第二版 python编程从入门到实战零基础自学教程计算机基础语言数据分析', 'price': '¥49.80', 'count': '5000+条评价', 'shop': '墨马图书旗舰店'}

    {'name': 'Python深度学习:基于PyTorch', 'price': '¥73.40', 'count': '5万+条评价', 'shop': '机械工业出版社自营官方旗舰店'}

    {'name': 'Python自然语言处理实战:核心技术与算法 自然语言处理教程', 'price': '¥48.30', 'count': '37条评价', 'shop': '芝麻开门图书专营店'}

    {'name': 'Effective Python:编写高质量Python代码的90个有效方法(原书第2版)', 'price': '¥110.60\n¥105.10', 'count': '2万+条评价', 'shop': '机械工业出版社自营官方旗舰店'}

    ...

    Selenium 自动化爬虫让你无须关心网站的类型(静态或者动态),只需您按部就班的寻找元素节点,并依此点击,即可实现数据抓取。不过  Selenium 最大的缺点就是效率低,因此它只适合做小规模的数据采集工作。

     

    35Python Scrapy爬虫框架详解

    Scrapy 是一个基于 Twisted 实现的异步处理爬虫框架,该框架使用纯 Python 语言编写。Scrapy 框架应用广泛,常用于数据采集、网络监测,以及自动化测试等。

    提示:Twisted 是一个基于事件驱动的网络引擎框架,同样采用 Python 实现。

    Scrapy下载安装

    Scrapy 支持常见的主流平台,比如 Linux、Mac、Windows 等,因此你可以很方便的安装它。本节以 Windows 系统为例,在 CMD 命令行执行以下命令:

    python -m pip install Scrapy

    由于 Scrapy 需要许多依赖项,因此安装时间较长,大家请耐心等待,关于其他平台的安装方法,可参考官方文档《Scrapy安装指南》。

    验证安装,如下所示:

    C:\Users\Administrator>python

    Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 19:29:22) [MSC v.1916 32 bit (Intel)] on win32

    Type "help", "copyright", "credits" or "license" for more information.

    >>> import scrapy

    >>> exit()

    如果可以正常执行exit()操作,并且没有出现 ERROR 错误,则说明安装成功。

    创建Scrapy爬虫项目

    Scrapy 框架提供了一些常用的命令用来创建项目、查看配置信息,以及运行爬虫程序。常用指令如下所示:

    常用指令

    命令

    格式

    说明

    startproject

    scrapy startproject <项目名>

    创建一个新项目。

    genspider 

    scrapy genspider <爬虫文件名> <域名>

    新建爬虫文件。

    runspider

    scrapy runspider <爬虫文件>

    运行一个爬虫文件,不需要创建项目。

    crawl

    scrapy crawl

    运行一个爬虫项目,必须要创建项目。

    list

    scrapy list

    列出项目中所有爬虫文件。

    view

    scrapy view

    从浏览器中打开 url 地址。

    shell

    csrapy shell

    命令行交互模式。

    settings

    scrapy settings 

    查看当前项目的配置信息。

    1) 创建第一个Scrapy爬虫项目

    下面创建名为 Baidu 的爬虫项目,打开 CMD 命令提示符进行如下操作:

    C:\Users\Administrator>cd Desktop

     

    C:\Users\Administrator\Desktop>scrapy startproject Baidu

    New Scrapy project 'Baidu', using template directory 'd:\python\python37\lib\site-packages\scrapy\templates\project', created in:

        C:\Users\Administrator\Desktop\Baidu

    # 提示后续命令操作

    You can start your first spider with:

        cd Baidu

        scrapy genspider example example.com

    打开新建的项目“Baidu”会有以下项目文件,如图所示:


    1:项目文件


    接下来,创建一个爬虫文件,如下所示:

    C:\Users\Administrator\Desktop>cd Baidu

    C:\Users\Administrator\Desktop\Baidu>scrapy genspider baidu www.baidu.com

    Created spider 'baidu' using template 'basic' in module:

      Baidu.spiders.baidu

    下面呈现了项目的目录树结构,以及各个文件的作用:

    Baidu                   # 项目文件夹

    ├── Baidu               # 用来装载项目文件的目录

    │   ├── items.py        # 定义要抓取的数据结构

    │   ├── middlewares.py  # 中间件,用来设置一些处理规则

    │   ├── pipelines.py    # 管道文件,处理抓取的数据

    │   ├── settings.py     # 全局配置文件

    │   └── spiders         # 用来装载爬虫文件的目录  

    │       ├── baidu.py    # 具体的爬虫程序

    └── scrapy.cfg          # 项目基本配置文件

     从上述目录结构可以看出,Scrapy 将整个爬虫程序分成了不同的模块,让每个模块负责处理不同的工作,而且模块之间紧密联系。因此,您只需要在相应的模块编写相应的代码,就可以轻松的实现一个爬虫程序。

    Scrapy爬虫工作流程

    Scrapy 框架由五大组件构成,如下所示:

    Scrapy 五大组件

    名称

    作用说明

    Engine(引擎)

    整个 Scrapy 框架的核心,主要负责数据和信号在不同模块间传递。

    Scheduler(调度器)

    用来维护引擎发送过来的 request 请求队列。

    Downloader(下载器)

    接收引擎发送过来的 request 请求,并生成请求的响应对象,将响应结果返回给引擎。

    Spider(爬虫程序)

    处理引擎发送过来的 response, 主要用来解析、提取数据和获取需要跟进的二级URL,然后将这些数据交回给引擎。

    Pipeline(项目管道)

    用实现数据存储,对引擎发送过来的数据进一步处理,比如存  MySQL 数据库等。


    在整个执行过程中,还涉及到两个 middlewares 中间件,分别是下载器中间件(Downloader Middlewares)和蜘蛛中间件(Spider Middlewares),它们分别承担着不同的作用:

    • 下载器中间件,位于引擎和下载器之间,主要用来包装 request 请求头,比如 UersAgent、Cookies 和代理 IP 等
    • 蜘蛛中间件,位于引擎与爬虫文件之间,它主要用来修改响应对象的属性。


    Scrapy 工作流程示意图如下所示:


    1:工作流程示意图


    上述示意图描述如下,当一个爬虫项目启动后,Scrapy 框架会进行以下工作:

    • 第一步:由“引擎”向爬虫文件索要第一个待爬取的 URL,并将其交给调度器加入 URL 队列当中(对应图中1/2步骤)。 
    • 第二步:调度器处理完请求后, 将第一个 URL 出队列返回给引擎;引擎经由下载器中间件将该  URL 交给下载器去下载 response 对象(对应3/4步骤)。
    • 第三步:下载器得到响应对象后,将响应结果交给引擎,引擎收到后,经由蜘蛛中间件将响应结果交给爬虫文件(对应5/6步骤)。
    • 第四步:爬虫文件对响应结果进行处理、分析,并提取出所需要的数据。
    • 第五步:最后,提取的数据会交给管道文件去存数据库,同时将需要继续跟进的二级页面 URL 交给调度器去入队列(对应7/8/9步骤)。


    上述过程会一直循环,直到没有要爬取的 URL 为止,也就是 URL 队列为空时才会停止。

    settings配置文件

    在使用 Scrapy 框架时,还需要对配置文件进行稍微改动。下面使用 Pycharm 打开刚刚创建的“Baidu”项目,对配置文件进行如下修改:

    # 1、定义User-Agent

    USER_AGENT = 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)'

    # 2、是否遵循robots协议,一般设置为False

    ROBOTSTXT_OBEY = False

    # 3、最大并发量,默认为16

    CONCURRENT_REQUESTS = 32

    # 4、下载延迟时间

    DOWNLOAD_DELAY = 1

    其余常用配置项介绍:

    # 设置日志级别,DEBUG < INFO < WARNING < ERROR < CRITICAL

    LOG_LEVEL = ' '

    # 将日志信息保存日志文件中,而不在终端输出

    LOG_FILE = ''

    # 设置导出数据的编码格式(主要针对于json文件)

    FEED_EXPORT_ENCODING = ''

    # 非结构化数据的存储路径

    IMAGES_STORE = '路径'

    # 请求头,此处可以添加User-Agent、cookies、referer等

    DEFAULT_REQUEST_HEADERS={

       'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',

       'Accept-Language': 'en',

    }

    # 项目管道,300 代表激活的优先级 越小越优先,取值1到1000

    ITEM_PIPELINES={

      'Baidu.pipelines.BaiduPipeline':300

    }

    # 添加下载器中间件

    DOWNLOADER_MIDDLEWARES = {}

    想要了解更多关于 Scrapy 框架的知识,可参考官方文档:https://docs.scrapy.org/en/latest/index.html

     

    36实战一

    通过上一节《Python Scrapy爬虫框架详解》的学习,您已经对 Scrapy 框架有了一个初步的认识,比如它的组件构成,配置文件,以及工作流程。本节将通过一个的简单爬虫项目对 Scrapy 框架做进一步介绍。

    首先看一个简单的示例,比如把 C语言中文网首页的“title”抓取下来,如下所示:

    1.  lang="zh-cn">
    2.  charset="gb2312" />
    3.  name="baidu-site-verification" content="6B13HRSfYA" />
    4.  rel="shortcut icon" href="/cpp/favicon.ico" />
    5. </strong>C<span style="font-family: 宋体">语言中文网:</span><span style="font-family: Calibri">c</span><span style="font-family: 宋体">语言程序设计门户网站</span><span style="font-family: Calibri">(</span><span style="font-family: 宋体">入门教程、编程软件</span><span style="font-family: Calibri">)</span><strong>
    6. ....

    创建项目

    CMD 命令行执行以下命令创建项目以及爬虫文件:

    # 创建项目

    1) scrapy startproject Title

    # 进入项目

    2) cd Title

    # 创建爬虫文件

    3)scrapy genspider title c.biancheng.net

    编写代码

    打开爬虫文件 title.py ,编写如下代码,其中一些代码  Scrapy 框架已经自动生成:

    1) 编写爬虫文件

    1. import scrapy
    2. class TitleSpider(scrapy.Spider):
    3. name = 'title'
    4. #要抓取数据的网站域名
    5. allowed_domains = ['c.biancheng.net']
    6. #第一个抓取的url,初始url,被当做队列来处理
    7. start_urls = ['http://c.biancheng.net/']
    8. def parse(self,response):
    9. #.extract():提取文本内容,将列表中所有元素序列化为Unicode字符串
    10. #.extract_first():提取列表中第1个文本内容
    11. # 以下是我们自己编写的代码,而自动生成的代码不用改动
    12. result = response.xpath('/html/head/title/text()').extract_first()
    13. print('-' * 60 )
    14. print(result)
    15. print('-' * 60)

    2) 修改配置文件

    下面修改 settings 文件的配置项,如下所示:

    USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1'

    ROBOTSTXT_OBEY = False

    DEFAULT_REQUEST_HEADERS = {

      'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',

      'Accept-Language': 'en',

    }

    3) 使用Pycharm IDE运行项目

    为了省去终端敲命令的环节,您可以在项目中自定义一个运行文件 mian.py(注意:该文件与 scrapy.cfg 同目录),并编写如下代码:

    1. from scrapy import cmdline
    2. # 注意,cmdline.execute()是为了减少输入命令的操作,该方法的参数必须为列表。
    3. # 执行爬虫文件来启动项目
    4. cmdline.execute('scrapy crawl title'.split())

    编写完成后,执行 main.py 文件,您会看到很多的输出内容,内容大致如下:

    # scrapy 框架运行时输出的日志,包含很多信息,比如版本信息,执行过程等等

    2021-04-09 16:35:15 [scrapy.utils.log] INFO: Scrapy 2.5.0 started (bot: Title)

    2021-04-09 16:35:15 [scrapy.utils.log] INFO: Versions: lxml 4.5.0.0, libxml2 2.9.5, cssselect 1.1.0, parsel 1.6.0, w3lib 1.22.0, Twisted

    ...

    2021-04-09 16:35:16 [scrapy.middleware] INFO: Enabled downloader middlewares:

    ...

    2021-04-09 16:35:16 [scrapy.core.engine] DEBUG: Crawled (200) (referer: None)

    # 此处是我们要的信息

    --------------------------------------------------

    C语言中文网:c语言程序设计门户网站(入门教程、编程软件)

    --------------------------------------------------

    2021-04-09 16:35:16 [scrapy.core.engine] INFO: Closing spider (finished)

    2021-04-09 16:35:16 [scrapy.statscollectors] INFO: Dumping Scrapy stats:  #描述Scrapy执行状态,如请求次数,时间,接受字节数等

    {'downloader/request_bytes': 231,

    ...

    上述过程中,我们只在 title.py 中使用 Xpath 做了解析数据的操作,就拿到了我们想要的数据。由此可见 URL 入队出队, 发起 request 请求,返回  response 响应等操作都是由 Scrapy 框架自动完成的。

    Scrapy 框架中,每个模块之间既紧密联系,但又相互独立,这样的设计减少了代码之间耦合度,是代码简洁易懂。

    猫眼电影案例

    最后,我们使用 Scrapy 框架完成一个完整的案例:抓取猫眼电影 Top100 榜。

    1) 创建项目

    scrapy startproject Maoyan100

    #进入项目目录

    cd Maoyan100

    # 创建爬虫文件,注意url 一定要是网站域名

    scrapy genspider maoyan www.maoyan.com

    2) 定义数据结构

    首先在 items.py 中定义要抓取的数据结构,如下所示:

    1. name = scrapy.Field()
    2. star = scrapy.Field()
    3. time = scrapy.Field()

    3) 编写爬虫文件

    接下来编写爬虫文件 maoyan.py 代码如下所示:

    1. import scrapy
    2. from Maoyan100.items import Maoyan100Item
    3. class Maoyan100Spider(scrapy.Spider):
    4. # name 指定爬虫文件名字
    5. name = 'maoyan'
    6. allowed_domains = ['maoyan.com']  # 网站域名
    7. start_urls = ['https://maoyan.com/board/4?offset=0']  # 第一个要抓取的url
    8. offset = 0  #查询字符串参数
    9. # response start_urls中影响对象
    10. def parse(self,response):
    11. # 基准xpath,匹配电影信息的dd节点对象列表
    12. dd_list = response.xpath('//dl[@class="board-wrapper"]/dd')
    13. # items.py 中的类:Maoyan100Item()实例化
    14. item = Maoyan100Item()
    15. for dd in dd_list:
    16. item['name'] = dd.xpath('./a/@title').get().strip()  # 1.6以后版本使用   原来用 extract_first()
    17. item['star'] = dd.xpath('.//p[@class="star"]/text()').get().strip()
    18. item['time'] = dd.xpath('.//p[@class="releasetime"]/text()').get().strip()
    19. yield item
    20. if self.offset < 90:  # 判断条件
    21. self.offset += 10
    22. url = 'https://maoyan.com/board/4?offset=' + str(self.offset)
    23. # url交给secheduer入队列
    24. # response会自动传给 callback 回调的 parse()函数
    25. #Scrapy.request()url发起请求,并将响应结果交给回调的解析函数
    26. yield scrapy.Request(url=url, callback=self.parse)  

    4) 实现数据存储

    通过编写管道文件 pipelinse.py 文件实现数据的存储,将抓取的数据存放在 MySQL 数据库,这里需要提前建库、建表,因为前面章节已经创建过,此处不再赘述。代码编写如下:

    1. import pymysql
    2. from Maoyan100.settings import *
    3. class Maoyan100Pipeline(object):
    4. def process_item(self, item, spider):
    5. print(item['name'], item['star'], item['time'])
    6. return item  # 多个管道有体现
    7. # 存入mysql数据库的管道
    8. class Maoyan100MysqlPipeline(object):
    9. #开始
    10. def open_spider(self, spider):
    11. # 爬虫项目启动,执行连接数据操作
    12. # 以下常量需要定义在settings配置文件中
    13. self.db = pymysql.connect(
    14. host=MYSQL_HOST,
    15. user=MYSQL_USER,
    16. password=MYSQL_PWD,
    17. database=MYSQL_DB,
    18. charset=MYSQL_CHARSET
    19. )
    20. self.cursor = self.db.cursor()
    21. # 向表中插入数据
    22. def process_item(self, item, spider):
    23. ins = 'insert into movieinfo values(%s,%s,%s)'
    24. L = [
    25. item['name'], item['star'], item['time']
    26. ]
    27. self.cursor.execute(ins, L)
    28. self.db.commit()
    29. return item
    30. # 结束存放数据,在项目最后一步执行
    31. def close_spider(self, spider):
    32. # close_spider()函数只在所有数据抓取完毕后执行一次,
    33. self.cursor.close()
    34. self.db.close()
    35. print('执行了close_spider方法,项目已经关闭')

    5) 定义启动文件

    下面定义项目启动文件 run.py, 代码如下:

    1. from scrapy import cmdline
    2. #执行爬虫文件 -o 指定输出文件的格式
    3. cmdline.execute('scrapy crawl maoyan -o maoyan.csv'.split()) #执行项目,并且将数据存csv文件格式

    注意:指定 -o 参数,可以将数据以特定的文件格式保存,比如 csv、txt、josn 等。

    6) 修改配置文件

    最后修改配置文件,主要有修改以下内容:添加日志输出、激活管道 pipelines、定义数据库常量,以及其他一些常用选项,如下所示:

    #设置 robots.txt 为False

    ROBOTSTXT_OBEY = False

    #设置日志级别: DEBUG < INFO < WARNING < ERROR < CRITICAL

    #日志需要自己添加,配置文件中没有,在空白处添加即可

    LOG_LEVEL='DEBUG'

    #定义日志输出文件

    LOG_FILE='maoyan.log'

    #设置导出数据的编码格式

    FEED_EXPORT_ENCODING='utf-8'

    #设置下载器延迟时间,秒为单位

    DOWNLOAD_DELAY = 1

    #请求头,添加useragent等信息

    DEFAULT_REQUEST_HEADERS = {

      'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',

      'Accept-Language': 'en',

      'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1'

    }

    #激活管道,并添加数据存放mysql的类,200为执行优先级

    ITEM_PIPELINES = {

       'Maoyan100.pipelines.Maoyan100Pipeline': 300,

        # 执行数据存储mysql

       'Maoyan100.pipelines.Maoyan100MysqlPipeline': 200

     

    }

    #在配置文件末尾添加mysql常用变量

    MYSQL_HOST='localhost'

    MYSQL_USER='root'

    MYSQL_PWD='123456'

    MYSQL_DB='maoyandb'

    MYSQL_CHARSET='utf8'

    最后执行 run.py 文件,日志文件 maoyan.log 内容大致如下:

    2021-04-09 19:38:11 [scrapy.utils.log] INFO: Scrapy 2.5.0 started (bot: Maoyan100)

    2021-04-09 19:38:11 [scrapy.utils.log] INFO: Versions: lxml 4.5.0.0, libxml2 2.9.5, cssselect 1.1.0

    ...

    2021-04-09 19:38:13 [scrapy.core.scraper] DEBUG: Scraped from <200 https://maoyan.com/board/4?offset=0>

     

    {'name': '我不是药神', 'star': '主演:徐峥,周一围,王传君', 'time': '上映时间:2018-07-05'}

    2021-04-09 19:38:13 [scrapy.core.scraper] DEBUG: Scraped from <200 https://maoyan.com/board/4?offset=0>

     

    {'name': '肖申克的救赎',

    'star': '主演:蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿',

    ...

    maoyan.csv 文件大致如下:

    name,star,time

    我不是药神,"主演:徐峥,周一围,王传君",上映时间:2018-07-05

    肖申克的救赎,"主演:蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿",上映时间:1994-09-10(加拿大)

    绿皮书,"主演:维果·莫腾森,马赫沙拉·阿里,琳达·卡德里尼",上映时间:2019-03-01

    海上钢琴师,"主演:蒂姆·罗斯,比尔·努恩,克兰伦斯·威廉姆斯三世",上映时间:2019-11-15

    小偷家族,"主演:中川雅也,安藤樱,松冈茉优",上映时间:2018-08-03

    哪吒之魔童降世,"主演:吕艳婷,囧森瑟夫,瀚墨",上映时间:2019-07-26

    霸王别姬,"主演:张国荣,张丰毅,巩俐",上映时间:1993-07-26

    ...

    黑天鹅,"主演:娜塔莉·波特曼,文森特·卡索,米拉·库妮丝",上映时间:2010-09-01(意大利)