Chapter 1 开发环境配置
安装Python
略
安装请求库
- 爬虫简单分成三步:抓取页面,分析页面,存储数据
- 抓取页面的时候,需要模拟浏览器向服务器发出请求,需要用到一些Python库来实现HTTP请求的操作
request
库pip install requests
Selenium
库pip install selenium
- Selenium 是一个自动化测试工具,利用它可以驱动浏览器执行特定的操作。对于一些JavaScript渲染的页面来说,这种方法十分有效
ChromeDriver
- 使用ChromeDriver以及Chrome配合Selenium进行网页抓取
- ChromeDriver版本要与Chrome版本匹配
- ChromeDriver要配置环境变量
aiohttp
pip install aiohttp
- requests库是阻塞式HTTP请求库,aiohttp是一个提供了异步Web服务的库
安装解析库
lxml
库pip install lxml
- lxml是一个Python解析库,支持HTML和XMl的解析,支持XPath解析方式,并且解析效率很高
Beautiful Soup
库pip install beautifulsoup4
- beautifulsoup4是Python的一个HTML和XMl的解析库,可以用它来方便的从网页中提取数据,它有强大的API和多样的解析方式
pyquery
库pip install pyquery
- 提供了和jQuery类似的语法来解析HTML文档,支持CSS选择器
tesserocr
- tesserocr是Python的一个OCR识别库,可以用来解决验证码问题
- 步骤
- 下载安装
tesseract
,它为tesserocr
提供了支持 pip install tesserocr pillow
- 把tesseract目录中的
tessdata
目录复制到Python环境中,比如复制到Anaconda目录下面 ## 安装数据库
- 下载安装
- 关系型:MySQL
- 非关系型:MongoDB(存储类似JSON对象),Redis
安装存储库
- Python利用存储库来进行和数据库的交互
- Python:PyMySQL
- MongoDB:PyMongo
- Redis::redis-py
安装Web库
- 通过Web服务提供一个API接口,比如可以把代理放在Redis中,向Web服务的代理池API请求获取代理
Flask
pip install flask
Tornado
pip install tornado
- 支持异步的I/O框架
安装APP爬取库
- APP中页面加载出来需要通过请求服务器接口获取数据。在爬虫的时候,一般通过抓包技术来获取数据
- APP爬虫时,简单接口可以使用Charles或者mitmproxy,复杂接口需要使用mitmdump对抓到的请求和响应进行实时保存和处理,规模化采集自动化工具是Appium(类似Selenium)
Charles
- 对于HTTPS抓包,需要在PC和手机上配之SSL证书
- 需要手动设置监听的端口和地址,监听所有HTTPS的话配置为:
"*"
mitmproxy
pip install mitmproxy
- mitmproxy支持HTTP和HTTPS抓包,它有两个关联组件。其中mitmdump是命令行接口,可以对接Python脚本,实现监听后的处理,mitmweb是一个Web程序,可以观察到mitmproxy捕获的请求
- 对于HTTPS请求,需要设置CA证书
APPium
- 直接通过GitHub安装即可
安装爬虫框架
- 规模化爬虫框架可以让使用者不用关心功能实现,只用你关心爬虫逻辑
pyspider
pip install pyspider
- 功能强大,支持JavaScript渲染页面的爬取(需要PhantomJS)
Scrapy
conda install Scrapy
- 功能强大,依赖库多(如
lxml
,Twisted
,pyOpenSSL
) Scrapy-Splash
是支持JavaScript渲染的工具Scrapy-Redis
是分布式扩展模块,可以方便的搭建分布式爬虫
安装部署相关库
Docker
- 可以讲爬虫制作为Docker镜像,只要主机安装了Docker,就可以直接运行爬虫,无需担心环境配置和版本问题
- Docker是一中容器技术,可以将应用和运行环境打包,形成一个独立的应用。这个独立应用可以直接被分发到任何一个支持Docker的环境中,通过简单的命令就可以启动运行。Docker和虚拟化技术相比更加轻量级,资源管理的粒度更细。
- 安装Docker
- Windows使用
Docker for Windows
- Linux使用官方一键安装脚本
- Mac使用
Docker for Mac
- Windows使用
Scrapyd
pip install scrapyd
- Scrapyd是一个用于部署Scrapy项目的工具,通过它可以将项目上传到云主机,并通过API来控制它的运行
- 将代码打包成EGG文件,并上传到云主机的过程可以手动实现,也可以使用Scrapyd-Client实现
pip install scrapyd-client
- 通过
scrapyd-deploy
来完成项目部署
- 通过Scrapy API来查看当前主机的Scrapy项目
pip install python-scrapyd-api
- 使用
1
2
3from scrapyd_api import ScrapydAPI
scrapyd = ScrapydAPI('http://localhost:6800')
print(scrapyd.list_projects())
- 通过Scrapyrt的HTTP接口而不是Scrapy命令来进行任务调度
pip install scrapyrt
- 启动:
scrapyrt
,默认端口号9080 - 以指定端口启动:
scrapyrt -p xxxxx
Chapter 2 爬虫基础
HTTP基本原理
- URI和URL
- URI(uniform resource identifier)
- URL(uniform resource locator): 可以唯一确定一个网络资源
- URN(uniform resource name)
- URI(uniform resource identifier)
- 超文本
- 我们看到的网页内容是由HTML代码解析而成的,HTML代码是一种超文本
- HTTP和HTTPS
- HTTP(Hyper Text Transfer Protocol)
- 从网络传输超文本数据到本地浏览器的传送协议
- 由万维网协会(World Wide Web Consortium)和Internet工作小组(Internet Envineering Task Force)共同制定的规范
- HTTPS(Hyper Text Transfer Protocol over Secure Socket Layer)
- 以安全为目的的HTTP通道,是HTTP下加入SSL层进行加密
- 建立了一个信息安全的通道来保证数据传输的安全
- 可以确认网站的真实性: 使用HTTPS的网站都可以通过浏览器地址栏的锁头标志查看网站认证之后的真实信息,也可以通过CA(Certificate Authority)机构颁发的安全签章来查询
- 在爬取使用HTTPS的网站时,需要设置忽略证书的选项,否则会提示SSL链接错误
- HTTP(Hyper Text Transfer Protocol)
- HTTP请求过程
- 浏览器输入URL会向服务器发送请求
- 服务器收到请求后进行处理和解析
- 服务器返回对应的响应,响应里包括页面源代码等内容
- 浏览器对响应进行解析,将网页呈现出来
- 请求
- 请求方法(Request Method)
GET
请求: 请求参数直接包含到URL里面,提交的数据最多1024字节POST
请求: 请求参数大多在表单提交,URL中无法直接看到,数据量没有限制- 还有比如
HEAD
,PUT
,DELETE
,CONNECT
,OPTIONS
,TRACE
等请求
- 请求的网址(Request URL)
- 请求头(Request Head)
Cookie
:网站为了辨别用户进行会话跟踪而储存在用户本地的数据,它的主要功能是维持当前访问的会话,每次访问服务器,都会在请求头中加入Cookie,以供服务器进行识别。比如登录状态的保持User-Aget(UA)
: 特殊的字符串头,可以是服务器识别用户的操作系统,浏览器版本等信息。爬虫时加上这个信息可以伪装浏览器,否则很容易被识别出来Referer
: 标识请求从哪个页面发过来的- 其他:
Accept
,Accept-Language
,Accept-Encoding
,Host
,Content-Type(POST请求注意写对此项)
- 请求体(Request Body)
- 对POST请求,请求体包含了表单中的内容,对于GET请求,请求体为空
- 请求方法(Request Method)
- 响应
- 响应状态码(Response Status Code)
- 200: 成功,服务器成功处理请求
- 401: 未授权,请求没有进行身份验证或者验证未通过
- 403: 禁止访问,服务器拒绝此请求
- 404: 未找到,服务器找不到请求的页面
- 500: 服务器内部错误,服务器遇到错误,无法完成请求
- 响应头(Response Head)
- 服务器对请求的应答信息
- 如:
Date
,Last-Modified
,Content-Encoding
,Server
,Content-Type
,Set-Cookie(将此内容放到Cookies中,下次请求需要携带)
,Expires
- 响应体(Response Body)
- 响应的正文内容,爬虫需要解析的对象
- 响应状态码(Response Status Code)
网页基础
- 网页的组成
- 三部分:HTML(骨架),CSS(肌肉),JavaScript(皮肤)
- HTML(Hyper Text Marking Language)
- 不同类型的元素通过不同类型的标签来表示,各种标签通过排列组合以及嵌套形成了网页的框架
- CSS(Cascading Style Sheet)
- 当HTML引用了多个CSS文件,且他们有冲突的时候会按照层叠顺序进行处理
- 可以在HTML文件中通过
link
引入
- JavaScript
- 一中脚本语言,给网页提供动态效果
- 在HTML文件中通过
script
引入
- 网页的结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html> - 节点树和节点之间的关系
- DOM(Document Object Model),文档对象模型,W3C DOM是中立于平台和语言的接口,它允许程序和脚本动态的访问和更新文档的内容,结构和样式
- W3C DOM的三个不同部分:
- 核心DOM: 针对任何结构化文档的标准模型
- XML DOM: 针对XMl文档的标准模型
- HTML DOM: 针对HTML文档的标准模型
- HTML DOM:
- 整个文档是一个文档节点
- 每个HTML元素是一个元素节点
- HTML元素内的文本是文本节点
- 每个HTML属性是属性节点
- 注释是注释节点
- 所有节点构成了HTML DOM树,他们可以通过JavaScript访问
- CSS选择器
.class
class选择器#id
id选择器*
选择所有节点element
选择所有element节点element1, element2
选择所有element1节点和所有element2节点element1 element2
选择element1节点内部的所有element2节点element1>element2
选择父节点为element1节点的所有element2节点element1+element2
选择紧邻element1节点之后的所有element2节点- ...
爬虫基本原理
- 爬虫概述
- 简单来说,爬虫就是获取网页并提取和保存信息的自动化程序。
- 获取网页
- 就是获取网页的源代码
- 关键在于构造请求并发送给服务器,然后收到响应并解析出来
- 提取信息
- 得到源代码之后需要从中提取我们想要的数据,最通用的方是就是使用正则表达式进行提取
- 还可以根据网页节点属性,CSS选择器,XPath提取等
- 保存数据
- 可以保存成TXT文本或者JSON文本
- 也可以保存在数据库中
- 能抓取什么样的数据
- 常规网页,得到的是HTML源代码
- 大多数API接口返回的是JSON字符串
- 图片,视频,音频等返回的是二进制数据
- 如CSS文件,JS文件,配置文件等也可以通过爬虫得到
- JavaScript渲染页面
- 很多网页使用Ajax(Asynchronous Javascript And XML),前端模块化工具来构建,整个网页由JavaScript渲染出来(在HTML中引入script,这样Javascript文件会改变HTML中节点,向其添加内容,最后得到完整页面),原始HTML就是一个空壳,这会导致直接爬虫的源代码和浏览器中看到的不一样
- 普通爬虫库不会读取JavaScript文件,可以分析后台Ajax接口,也可以使用Selenium,Splash等库来实现模拟JavaScript渲染
- 会话和Cookies
- 动态网页和静态网页
- 静态网页:加载速度快,编写简单,可维护性差,不能根据URL灵活多变的显示内容
- 动态网页: 可以解析URL中参数的变化,关联数据库并动态呈现不同的页面内容,还可以实现用户登录,注册等功能
- 无状态HTTP
- HTTP对事务处理没有记忆能力,如果后续请求依赖于前面的请求,则必须重传前面的请求,这十分浪费资源
- 维持HTTP连接状态的技术:会话和Cookies
- 会话: 在服务端,用于保存用户的会话信息
- 用户在应用程序的不同Web页面跳转时,会话一直维持
- 如果用户还没创建会话,则Web服务器将自动创建一个会话
- 当会话过期或被放弃以后,服务器将终止会话
- Cookies:
在客户端(浏览器),是网站为了辨别用户身份,进行会话追踪而储存在用户本地终端的数据
- 用户第一次请求,响应头里会有
Set-Cookies
,用来标记用户身份,客户端会把Cookies保存到本地 - 以后请求的时候,会把Cookies放入请求头,服务器检查Cookies可以找到对应会话是什么,再判断会话来识别用户状态(比如是否登录等)
- Cookies结构
Name
: 一经创建不能修改Value
: 如果是Unicode数据,需要字符编码,如果是二进制数据,需要BASE64编码Domain
: 可以访问Cookie的域名Max Age
: 如果正数,则Max Age秒之后失效,如果负数,则关闭浏览器Cookie立刻失效Secure
: 默认false,true的时候需要HTTPS- ...
- 用户第一次请求,响应头里会有
- 关闭浏览器会话并不会消逝,服务器不会删除这个会话。只是大部分会话都是Max Age为负数,Cookies消失后下次就找不到会话了。如果是设置为Max Age很大的正数,则可以保存比如登录状态等。有时服务器会设置失效时间,超过失效时间以后,服务器才会删除这个会话。
- 动态网页和静态网页
代理基本原理
- 前言
- 如果不设置代理,服务器检测到某个IP单位时间内请求次数超过阈值的话,就会禁止访问,响应状态403,这就是封IP,还有可能需要输入验证码,这些都需要代理来伪装IP
- 基本原理
- 在客户端和服务器之间加入代理服务器,服务器识别的是代理服务器
- 代理分类
- 根据协议区分
- FTP代理: 主要用于访问FTP服务器
- HTTP代理: 主要用于访问网页
- SSL/TSL代理: 主要用于访问加密网站
- RTSP代理: 主要用于访问Real流媒体服务器
- Telnet代理: 主要用于telnet远程控制
- POP3/SMTP代理: 主要用于POP3/SMPT方式收发邮件
- SOCKS代理: 单纯传递数据包,不关心具体协议和用法
- 根据匿名程度划分
- 高度匿名代理: 原封不动转发数据包,完全模拟普通客户端
- 普通匿名代理: 会在数据包进行改动,服务器可能发现代理
- 透明代理: 改动数据包,并告知服务器客户端的真实IP
- 间谍代理
- 根据协议区分
Chapter 3 基本库的使用
使用urllib
- 前言
- urllib是Python内置的HTTP请求库,不需要额外安装就可以使用
- 它由4个模块构成
request
: 基本的HTTP请求模块,可以模拟发送请求error
: 异常处理模块parse
: 工具模块,提供了很多URL处理方法,比如拆分,解析,合并等robotparser
: 主要用来识别网站的robots.txt
文件,然后判断网站是否可以爬
- 发送请求
urlopen()
- 可以用来模拟浏览器请求发起过程,并且可以处理授权验证(authentication),重定向(redirection),浏览器Cookies等内容
- 返回一个
http.client.HTTPResponse
对象,它是一个字节流,需要解码,其charset是响应头Content-Type规定的 - 基本用法
1
2
3
4
5
6
7
8
9from urllib.request import urlopen
response = urlopen('https://www.python.org')
charset = response.headers.get_content_charset()
resource = responser.read().decode(charset)
print(response.status)
print(response.getheaders()) # return tuples (header, value)
print(response.getheader('Server')) - 传递参数
data
变为POST模式1
2
3
4
5from urllib.request import urlopen
from urllib.parse import urlencode
data = byptes(urlencode({'word': 'hello'}), encoding='utf-8') # 传入参数 key = word, value = hello,使用urlencode把字典变成字符串,然后再用bytes变成字节流
response = urlopen(url='https://httpbin.org/post', data=data) - 传递参数
timeout
设置超时时间,单位秒,如果超时会抛出URLError
异常1
2
3
4
5
6
7
8
9import socket
import urllib.error
from urllib.request import urlopen
try:
response = urlopen(url='https://www.google.com',timeout=0.1)
except urllib.error.URLError as e:
if isinstance(e.reason, socket.timeout):
print('Time Out!')
Request()
- 可以传入headers和host等来进行伪装
1
2
3
4
5
6
7
8
9
10
11
12from urllib.request import urlopen, Request
from urllib.parse import urlencode
url = 'https://www.httpbin.org/post'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0',
'Host': 'httpbin.org'}
# 另一种写法是用request.add_header('key','value')
dict = {'name': 'America'}
data = bytes(urlencode(dict), encoding='utf-8')
request = Request(url=url,headers=headers,data=data,method='POST')
response = urlopen(request)
print(response.read().decode('utf-8'))
- 可以传入headers和host等来进行伪装
- 使用Handler进行高级操作
urllib.request.BaseHandler
类是其他Handler类的父类- 在遇到需要登录验证的时候,使用
urllib.request.HTTPBasicAuthHandler
来处理 - 在爬虫需要添加代理的时候,使用
urllib.request.ProxyHandler
来处理 - 处理Cookies的时候,使用
urllib.request.HTTPCookieProcessor
- 处理异常
前言
urllib.error
模块定义了由request模块产生的异常。如果出现了问题,request模块就会抛出error模块中定义的异常
URLError
- URLError继承了OSError类,是error异常模块的基类,所有由request模块产生的异常都可以由它来处理
- 它有1个
reason
属性,返回错误的原因1
2
3
4
5
6
7from urllib.error import URLError
from urllib.request import urlopen
try:
response = urlopen(url='https://www.google.com', timeout=0.1)
except URLError as e:
print(e.reason)
HTTPError
- HTTPError是URLError的子类,专门处理HTTP请求错误
- 它有3个属性:
code
: 返回HTTP响应状态码reason
: 返回错误的原因headers
: 返回请求头
- 应该先捕获子类HTTPError,再捕获父类URLError
1
2
3
4
5
6
7
8
9
10
11from urllib.request import urlopen
from urllib.error import HTTPError
try:
response = urlopen(url='https://test.github.io/about_me.html')
except HTTPError as e:
print(e.code, e.reason, e.headers, sep='\n')
except URLError as e:
print(e.reason)
else:
print('Connected successfully!') - 有时候reason不一定是字符串,可能是一个对象
1
2
3
4
5
6
7
8
9import socket
from urllib.request import urlopen
from urllib.error import URLError
try:
response = urlopen(url='https://test.github.io/index.html',timeout=0.1)
except URLError as e:
if isinstance(e.reason, socket.timeout):
print('Time Out!')
- 解析连接
前言
urllib.parse
模块定义了处理URL的标准接口
urlparse()
- 实现了URL的识别和分段
- 返回一个
urllib.parse.ParseResult
对象,它是一个长度为6的元组,可以用index或者名字调用item - URL标准格式:
scheme://netloc/path[;params][?query]#comment
scheme
:协议,://
之前的netloc
:域名,第一个/
之前的path
: 访问路径,第一个/
之后的params
: 参数, 用;
之后的query
: 查询条件,?
之后的comment
: 锚点,页面内部的某个节点,#
之后的1
2
3
4
5
6from urllib.parse import urlparse
url = 'https://www.google.com;user?id=5#comment'
result = urlparse(url=url)
print(result)
print(result[0]) # print(result.scheme)
- 参数
url
: URLStringscheme
: 在URL未指定scheme的时候需要这个参数allow_fragments
: 如果是False,则fragment会和前面一项(params和query是可选的,因此前一项可能直接就是path)连在一起,并且结果的fragment为空1
2
3
4
5
6
7
8from urllib.parse import urlparse
url = 'www.google.com;user?id=5#comment'
result = urlparse(url=url,scheme='https',allow_fragments=False)
print(result)
print(result[0]) # https
print(result.query) # id=5#comment
print(result.fragment) # None
urlunparse()
- 把一个长度为6的iterable object整合成一个URL,就是urlparse()反过来
urlsplit()
- 和urlparse()的区别在于不会解析params部分,如果有这一部分的话,直接放在path里面
- 返回
urllib.parse.SplitResult
对象,它是一个长度为5的元组,可以用index或者名字调用item
urlunsplit()
- 把一个长度为5的iterable object整合成一个URL,就是urlsplit()反过来
urljoin()
- 根据base_url的scheme, netloc, path对目标URL缺失部分(目标url已有但和base_url不同的以目标URL为准)进行补全,base_url的params, query, fragment对目标url没有影响
- 返回一个string
urlencode()
- 在构造GET请求参数的时候十分方便,可以把字典转化成query_string
1
2
3
4
5
6
7
8from urllib.parse import urlencode
params = {
'name': 'america'
'age': '200'
}
url = 'https://www.google.com' + urlencode(params)
print(url) # https://www.google.com?name=america&age=200
parse_qs()
- 把GET请求query_string转成字典
1
2
3
4from urllib.parse import parse_qs
query = 'name=america&age=200'
print(parse_qs(query)) # {'name' : 'america', 'age': 200}
parse_qsl()
- 把GET请求query_string转成列表,列表中每一项是(key,value)形式的元组
1
2
3
4from urllib.parse import parse_qsl
query = 'name=america&age=200'
print(parse_qsl(query)) # [('name', 'america'), ('age', 200)]
quote()
- 把任意编码内容转换成URL编码的格式,可以用来解决中文乱码的问题
1
2
3
4from urllib.parse import quote
word = '乱码'
url = 'https://wwww.google.com?wd=' + quote(word) # https://www.google.com?wd=%E4%B9%B1%E7%A0%81
unquote()
- quote的反面,它可以进行URL编码
1
2
3
4from urllib.parse import unquote
url = 'https://www.google.com?wd=%E4%B9%B1%E7%A0%81'
print(unquote(url)) # https://www.google.com?wd=乱码
- Robots协议
前言
- Robots协议(Robots Exclusion
Protocol),用来告诉爬虫和搜索引擎哪些页面可以爬取,哪些不可以爬取。它通常是一个
robots.txt
的文本文件,一般放在网站的根目录下面
- Robots协议(Robots Exclusion
Protocol),用来告诉爬虫和搜索引擎哪些页面可以爬取,哪些不可以爬取。它通常是一个
例子
- 禁止所有爬虫
1
2User-agent: *
Disallow: / - 允许所有爬虫
1
2User-agent: *
Disallow:
- 禁止所有爬虫
爬虫名称
- BaiduSpider
- Googlebot
- bingbot
- Slurp(Yahoo)
- msnbot
robotparser
- 可以使用
urllib.robotparser.RobotFileParser()
来判断是否有权限爬取某个网页1
2
3
4
5from urllib.robotparser import RobotFileParser
rp = RobotFileParser(url='https://www.google.com/robots.txt')
# 开始解析,必须有rp.read()这一行,否则下面判断都是False
rp.read()
print(rp.can_fetch(useragent='*', url='https:www.google.com'))
- 可以使用
使用requests
- 基本用法
- 使用urllib的时候,登陆验证,Cookies,设置代理都需要Opener和Handler,但是使用requests可以简单的搞定这些问题
- GET请求
- urllib中的urlopen()方法实际上是GET方是请求网页,而requests中的get()也可以实现同样的功能
- get()返回
requests.models.Response
1
2
3
4
5
6
7
8
9import requests
rp = requests.get(url='https://www.google.com')
print(rp.status_code) # 响应状态码
print(rp.text) # 响应体内容
print(rp.cookies) # Cookies
print(rp.headers) # 响应头
print(rp.url) # URL
print(rp.history) # 请求历史 - 使用data参数引入query_string,使用headers引入headers
1
2
3
4
5import requests
query = {'name': 'america', 'age': 200}
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0'}
rp = requests.get(url='https://www.google.com', params=query, headers=headers) - 抓取图片,视频,音频等二进制数据
1
2
3
4
5from requests import get
rp = get(url='https://www.github.com/facivon.ico')
with open('favion.ico', 'wb') as f:
f.write(rp.content) - 类似的有
post()
,put()
,delete()
等方法来实现POST,PUT,DELETE等请求
- POST请求
- 和urllib中Request()类似,requests中post()可以实现POST请求
1
2
3
4
5import requests
data = {'name': 'germey', 'age': '22'}
r = requests.post("http://httpbin.org/post", data=data)
print(r.text)
- 和urllib中Request()类似,requests中post()可以实现POST请求
- 响应
- requests.codes有内置状态码,可以用来和response.status_code比较判断状态
- 举例
- 200:
requests.codes.ok
- 403:
requests.codes.forbidden
- 404:
requests.codes.not_found
- 500:
requests.codes.internal_server_error
- 200:
- 高级用法
- 文件上传
1
2
3
4
5import requests
files = {'file': open('favicon.ico', 'rb')}
r = requests.post('http://httpbin.org/post', files=files)
print(r.text) - Cookies
- 获取Cookies
1
2
3
4
5from request import get
rp = get(url='https:www.google.com')
for key, value in rp.cookies.items():
print(key, '===>', value) - 把cookies复制到headers中维持登录
- 获取Cookies
- 会话维持
- 每次调用get()或者post()都是开启新的会话,相当于开了两个浏览器,如果想维持同一个登录状态,除非手动在headers里面设置同一个cookies
- 另一个简单的维持同一个会话的方法是使用
requests.Session
,使用Session对象对同一个host发起多个请求的时候,他们复用一个TCP连接,这会提高效率1
2
3
4
5from requests import Session, get
session = Session()
session.get(url='https://www.google.com')
rp = session.get(url='https:www.google.com?q=test')
- SSL证书验证
- 使用requests中方法发送HTTPS请求的时候,它会检查SSL证书,这个由verify参数控制,默认值为True,如果证书没有官方CA机构信任,会出现证书验证错误的结果
requests.exceptions.SSLError
,可以手动设置verify为False,不过这时候会有警告,可以手动忽略警告1
2
3
4
5import requests
from requests.packages import urllib3
urllib3.disable_warnings()
rp = requests.get('https://www.google.com', verify=False)
- 使用requests中方法发送HTTPS请求的时候,它会检查SSL证书,这个由verify参数控制,默认值为True,如果证书没有官方CA机构信任,会出现证书验证错误的结果
- 代理设置
- 基本设置
1
2
3
4
5
6
7from requests import get
proxies = {
'http': 'http://10.10.1.10:3128',
'https': 'https://10.10.1.10:1080'
}
rp = get(url='https://www.google.com',proxies=proxies) - 代理需要登录验证的设置格式:
scheme://user:password@host:port
1
2
3
4
5
6
7from requests import get
proxies = {
'http': 'http://user:pwd@10.10.1.10:3128',
'https': 'https://user:pwd@10.10.1.10:1080'
}
rp = get(url='https://www.google.com',proxies=proxies)
- 基本设置
- 超时设置
- 基本总时间设置
1
2
3from requests import get
rp = get(url='https://www.google.com', timeout=1) - 分成(connect, read)元组设置
1
2
3from requests import get
rp = get(url='https://www.google.com', timeout=(5,10)) - 永久等待: 不设置或设置为None
1
2
3
4from requests import get
rp1 = get(url='https://www.google.com', timeout=None)
rp2 = get(url='https://www.google.com')
- 基本总时间设置
- Prepared Request
- 和urllib中的Request类似,requests中的Prepare Request可以接受参数
1
2
3
4
5
6
7
8
9
10
11from requests import Request, Session
url = 'http://httpbin.org/post'
data = {'name': 'germey'}
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'
}
s = Session()
req = Request('POST', url, data=data, headers=headers)
prepped = s.prepare_request(req)
r = s.send(prepped)
print(r.text)
- 和urllib中的Request类似,requests中的Prepare Request可以接受参数
- 文件上传
正则表达式
可以使用regular expression 进行模式匹配
- 比如匹配URL:
[a-zA-Z]+://[^\s]*
- 比如匹配URL:
规则
\w
: 数字或字母或下划线\w
=[a-zA-Z0-9_]
\W
:非数字非字母非下划线\s
:空白字符\s
=[\t\n\r\f]
\S
: 非空白字符\d
: 数字\d
=[0-9]
\D
: 非数字^
: 一行的开头$
: 一行的结尾.
: 任意字符,除了换行符,如果需要匹配换行符要在函数中传入re.S
[...]
: 一组字符[^...]
: 非这一组字符中任意一个的字符+
: 1次或多次,如x+
=x{1,}
匹配1个或多个的所有x,贪婪匹配,若非贪婪匹配使用+?
*
: 0次或多次,如x*
=x{0,}
匹配0个或多个的所有x,贪婪匹配,若非贪婪匹配使用*?
?
: 0次或1次,x?
=x{0,1}
匹配0个或1个下,如果想要匹配问号,使用\?
{n}
: n次{n,}
: 至少n次{n,m}
: 至少n次,之多m次
re
库match()
- 从目标字符串起始位置开始匹配pattern,如果匹配到返回匹配的内容和匹配内容在原字符串的范围,未匹配到返回None
1
2
3
4
5
6
7from re import match
content = 'Hello 123 4567 World_This is Regex Demo'
pattern = '^Hello\s\d{3}\s\d{4}\s\w{10}'
result = match(pattern, content)
print(result.group()) # Hello 123 4567 World_This
print(result.span()) # (0, 25) - 使用
()
分组提取匹配的内容1
2
3
4
5
6
7
8
9
10
11
12
13from re import match
content = 'Hello 123 4567 World_This is Regex Demo'
pattern = '^Hello\s\(d{3})\s(\d{4})\s\w{10}'
result = match(pattern, content)
print(result.group()) # Hello 123 4567 World_This
print(result.span()) # (0, 25) 氛围包含0,不包含25
print(result.group(0)) # Hello 123 4567 World_This
print(result.span(0)) # (0, 25)
print(result.group(1)) # 123
print(result.span(1)) # (6, 9)
print(result.group(2)) # 4567
print(result.span(3)) # (10, 14) - 在字符串中间部分尽可能使用非贪婪匹配,防止错误
- 修饰符,需要传入函数中作为参数
re.I
: 匹配对大小写不敏感re.S
: 使得.
可以匹配换行符- ...
- 转义字符,直接加入
\
即可
- 从目标字符串起始位置开始匹配pattern,如果匹配到返回匹配的内容和匹配内容在原字符串的范围,未匹配到返回None
search()
- 从左边开始依次寻找匹配字符串,如果有匹配结果,则返回第一个匹配的字符串和位置,如果没找到,返回None
findall()
- 和search类似,但是会返回所有匹配到的结果及范围,或者None
- 所有匹配结果都是tuple,他们放在一个list里面
sub()
- 对字符串进行修改和替换
1
2
3
4
5from re import sub
content = '123sfjsddsfdsfdfsdfds'
# 删除的话只要替换成空字符串就行了
content = sub('\d+', '', conetnt)
- 对字符串进行修改和替换
compile()
- 如果要重复使用一个pattern,没必要每次都手写,可以通过compile把string形式的regular
expression变成一个
re.Pattern
对象,再所有需要传入pattern的地方都可以直接复用这个对象
- 如果要重复使用一个pattern,没必要每次都手写,可以通过compile把string形式的regular
expression变成一个
Chapter 4 解析库的使用
使用XPath
- 前言
- XPath, 全称是XML Path Language,是一门在XML文档中查找信息的语言。最初用来搜寻XML文档,但是同样适用于HTML文档的搜索
- XPath提供了简洁明了的路径选择表达式和众多内置函数
- 常见规则及举例
规则
表达式 描述 nodename 选择所有叫做"nodename"的节点 / 从根节点中选择 // 从当前节点选择,无论它们在文档中什么位置 . 选择当前节点 .. 选择当前节点的父节点 @ 选择属性 举例
表达式 描述 bookstore 选择所有叫做"bookstore"的节点 /bookstore 选择根节点bookstore bookstore/book 选择bookstore的所有孩子节点book //book 选择文档中的所有book节点 bookstore//book 选择bookstore的所有子孙节点book //@lang 选择文档中所有叫做"lang"的属性 //title[@lang='eng'] 选择文档中所有属性lang的值为eng的title节点 读取HTML数据准备解析
- lxml会自动对输入的文件进行语法检查和修正,补完成一个完整合法的HTML文件格式
- 得到的是
lxml.etree._ElementTree
对象,它是一个bytestream,需要decode才可读 - 读取string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from lxml import etree
text = '''
<div>
<ul>
<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive"><a href="link3.html">third item</a></li>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
</ul>
</div>
'''
html = etree.HTML(text) # 形成lxml.etree._ElementTree对象共后续解析使用
# 下面是转成人类可读格式
html_string = etree.tostring(html)
print(html_string.decode('utf-8')) - 读取HTML文件
1
2
3from lxml import etree
# 这个文件内容就是上面的字符串里的标签
html = etree.parse('./test.html', etree.HTMLParser())
使用
xpath()
获取节点- xpath会得到一个列表,其中每个元素都是满足要求的节点,每一个节点是
lxml.etree._Element
对象 - 所有节点
1
2
3
4
5
6
7from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
# 这里改成 results = html.xpath('//li')就是获取所有li节点
results = html.xpath('//*')
for result in results:
print(result) - 子节点和子孙节点
1
2
3
4
5
6
7
8
9
10from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
results = html.xpath('//li/a') # 获取所有li节点的a子节点
for result in results:
print(result)
results_another = html.xpath('//ul//a') # 获取所有ul节点的a子孙节点,效果和上面一样
for result in results:
print(result) - 父节点: 两种写法
1
2
3
4
5
6
7from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
# 使用/../查找父节点
result1 = html.xpath('a[href="link4.html"]/../@class') # ['item-1']
# 使用/parent::*/查找父节点,parent::表示父节点,*表示所有满足的结果
result2 = html.xpath('a[href="link4.html"]/parent::*/@class') # ['item-1'] - 属性匹配
1
2
3
4from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]') - 文本获取
1
2
3
4
5from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result1 = html.xpath('//li[@class='item-0']//text()') # ['first-item', 'second-item', '\n'], 因为在lxml对html文件进行自动修正补完后加入了换行符
result2 = html.xpath('//li[@class='item-0']/a/text()') # ['first-item', 'second-item'], 这个要手动多写一层,但是更加简洁 - 属性获取: 两种写法
1
2
3
4
5from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result1 = html.xpath('//li/a/@href')
result2 = html.xpath('//li/a/attribute::href') - 属性多值匹配
1
2
3
4
5
6
7
8
9from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
# result = html.xpath('//li[@class="li"]/a/text()') 这个不行,因为属性有两个值,这样拿不到对应的节点
result = html.xpath('//li[contains(@class, "li")]'/a/text()) # Syntax: contains(@attribute, value)
print(result) - 多属性共同定位
- 使用运算符搞定:
and
,or
,mod(% int Python)
,|(两个节点集合)
,+
,-
,*
,div(/ in Python)
,=
,!=
,>
,>=
,<
,<=
1
2
3
4
5
6
7
8from lxml import etree
text = '''
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()')
print(result)
- 使用运算符搞定:
- 按照位置选择节点
1
2
3
4
5
6
7from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
first = etree.xpath('//li[1]/a/text()')
last = etree.xpath('//li[last()]/a/text()')
first_two = etree.xpath('//li[position()<3]/a/text()')
third_last = etree.xpath('//li[last()-2]/a/text()') - 节点轴选择
- 祖先节点:
ancestor::
- 子孙节点:
descendant::
- 父节点:
parent::
- 子节点:
child::
- 当前节点之后的节点:
following::
- 当前节点之后的同级节点:
following-sibling::
- 祖先节点:
- xpath会得到一个列表,其中每个元素都是满足要求的节点,每一个节点是
使用Beautiful Soup
- 简介
- Beautiful Soup是一个工具箱,通过解析文档为用户提供需要抓取的数据
- Beautiful Soup自动将输入文档转换为Unicode编码,输入文档为UTF-8编码
- 解析器
- 分类
解析器 使用方法 优势 劣势 Python标准库 BeautifulSoup(html, 'html.parser')
内置库,速度适中,文档容错率强 低版本Python库难用 lxml HTML解析器 BeautifulSoup(html, 'lxml')
速度快,文档容错率强 需要C库 lxml XML解析器 BeautifulSoup(html, 'xml')
速度快,唯一支持XML 需要C库 html5lib BeautifulSoup(html, 'html5lib')
文档容错率最高,生成HTML5格式的文档 速度慢 - 例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19from bs4 import BeautifulSoup
# 输入html_string
html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
soup = BeautifulSoup(html, 'lxml')
# 从文件输入
soup = BeautifulSoup(open('./test.html'), 'lxml')
# prettify()可以把要解析的字符串以标准的缩进格式输出
print(soup.prettify())
- 节点选择器
- 选择元素得到
bs4.element.Tag
对象1
2
3
4
5
6from bs4 import BeautifulSoup
html = "" # 同上,omitted
soup = BeautifulSoup(html, 'lxml')
print(soup.title.string) #soup.节点
print(type(soup.title)) # bs4.element.Tag - 提取信息
- 获取名称
1
2
3
4
5from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(soup.title.name) # title - 获取属性
1
2
3
4
5
6
7
8from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(soup.p.attrs) # p节点的属性列表
print(soup.p.attrs['name']) # p节点的name属性
# 这个写法更简略,效果和上面的一致
print(soup.p['name']) # p节点的name属性 - 获取内容
1
2
3
4
5from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(soup.p.string)
- 获取名称
- 嵌套选择
1
2
3
4
5
6from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(soup.head.string)
print(soup.head.title.string) # 这个就是嵌套 - 关联选择
- 子节点
1
2
3
4
5
6
7
8
9from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(type(soup.p.contents)) # list
print(soup.p.contents)
print(type(soup.p.children)) # list_iterator
for child in soup.p.children:
print(child) - 子孙节点
1
2
3
4
5
6
7from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(type(soup.p.descendants))
for descendant in soup.p.descendants:
print(descendant) - 父节点
1
2
3
4
5
6from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(type(soup.a.parent)) # bs4.element.Tag
print(soup.a.parent) - 祖先节点
1
2
3
4
5
6
7from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(type(soup.a.parents)) # list_iterator
for parent in soup.a.parents:
print(parent) - 兄弟节点
1
2
3
4
5
6from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(soup.p.next_sibling)
print(soup.p.prev_sibling)
- 子节点
- 选择元素得到
- 方法选择器
find_all()
find_all(name=, attrs=, recursive=, text=, **kwargs)
, 查询所有符合条件的元素name
1
2
3
4
5
6from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(name='ul')) # 返回的结果是list
print(soup.find_all(name='ul')[0])attrs
1
2
3
4
5
6
7
8
9from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
attrs = {'id':'list-1'}
print(soup.find_all(attrs=attrs)) # 返回结果是list
# 对于id和class有简略写法
print(soup.find_all(id='list-1'))
print(soup.find_all(class_='element')) # Python中class是关键字,这里需要使用class_text
1
2
3
4
5
6
7import re
from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
text = re.compile('regular_expression')
print(soup.find_all(text=text)) # 返回一个list
find()
- 返回第一个匹配到的节点
find_parents()
,find_parent()
find_next_siblings()
,find_next_sibling()
find_previous_siblings()
,find_previous_sibling()
find_all_next()
,find_next()
find_all_previsou()
,find_previous()
- CSS选择器
- 基本用法
1
2
3
4
5
6from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
print(soup.select('ul li'))
print(soup.select(#list-2 .element)) - 嵌套选择
1
2
3
4
5
6from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
for ul in soup.sleect('ul'):
print(ul.select('li')) - 获取属性
1
2
3
4
5
6
7from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
for ul in soup.select('ul'):
print(ul['id'])
print(ul.attrs['id']) - 获取文本
1
2
3
4
5
6
7from bs4 import BeautifulSoup
html = ""
soup = BeautifulSoup(html, 'lxml')
for ul in soup.select('ul'):
print(ul.string)
print(ul.get_text())
- 基本用法
- BeautifulSoup总结
- 推荐使用lxml解析器,必要时使用lxml.HTMLParser
- 节点选择器筛选功能比较弱,但是速度快
- 建议使用find()或者find_all()查询匹配单个结果或者多个结果
- 如果对CSS选择器熟悉的话,可以使用select()方法选择
使用pyquery
- 初始化
- 得到的文件是
pyquery.pyquery.PyQuery
对象,其中的每一个节点也是pyquery.pyquery.PyQuery
对象 - 使用字符串初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from pyquery import PyQuery as pq
html = '''
<div>
<ul>
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
</div>
'''
doc = pq(html)
print(type(doc)) - 使用URL初始化
1
2
3
4from pyquery import PyQuery as pq
url = 'https://www.google.com'
doc = pq(url=url) - 使用文件初始化
1
2
3
4from pyquery import PyQuery as pq
file = './test.html'
doc = pq(filename=file)
- 得到的文件是
- 基本CSS选择器
1
2
3
4
5from pyquery import PyQuery as pq
html = ""
doc = pq(html)
print(doc('ul li')) # 按照选择器规则用括号表示 - 查找节点
- 子节点
1
2
3
4
5
6
7
8
9from pyquery import PyQuery as pq
html = ""
doc = pq(html)
item = doc('ul')
lis = item.children()
lis_active = item.children('.active')
print(lis)
print(lis_active) - 子孙节点
1
2
3
4
5
6
7from pyquery import PyQuery as pq
html = ""
doc = pq(html)
item = doc('ul')
descendants = item.find('li')
print(descendants) - 父节点
1
2
3
4
5
6
7from pyquery import PyQuery as pq
html = ""
doc = pq(html)
item = doc('.list')
container = item.parent()
print(container) - 祖先节点
1
2
3
4
5
6
7
8
9from pyquery import PyQuery as pq
html = ""
doc = pq(html)
item = doc('.list')
ancestors = item.parents()
ancestor = item.parents('.wrap')
print(ancestors)
print(ancestor) - 兄弟节点
1
2
3
4
5
6from pyquery import PyQuery as pq
html = ""
doc = pq(html)
li = doc('.list .item-0 .active')
print(li.siblings())
- 子节点
- 遍历
- 对于多个结果的,使用
items()
得到生成器,然后进行遍历1
2
3
4
5
6
7
8from pyquery import PyQuery as pq
html =""
doc = pq(html)
lis = doc('li').items()
print(type(lis)) # generator
for li in lis:
print(li)
- 对于多个结果的,使用
- 获取信息
- 获取属性使用
attr()
- 单个结果直接使用
1
2
3
4
5
6
7from pyquery import PyQuery
html = ""
doc = pq(html)
a = doc('.item-0.active a')
print(a.attr('href'))
print(a.attr.href) - 多个结果需要遍历,否则attr函数只能拿到第一个结果节点的对应属性
- 单个结果直接使用
- 获取文本使用
text()
和html()
- 获取纯文本
1
2
3
4
5
6from pyquery import PyQuery
html = ""
doc = pq(html)
a = doc('.item-0.active a')
print(a.text()) - 获取HTML文本
1
2
3
4
5
6from pyquery import PyQuery
html = ""
doc = pq(html)
a = doc('.item-0.active a')
print(a.html()) - 如果有多个结果节点,text函数会返回以空格分隔的字符串。html函数需要遍历,否则只会返回第一个结果节点的HTML文本
- 获取纯文本
- 获取属性使用
- 节点操作
- 改变class属性
1
2
3
4
5
6
7from pyquery import PyQuery as pq
html = ""
doc = pq(html)
li = doc('.item-0.active')
li.removeClass('active')
li.addClass('active') - 改变属性
1
2
3
4
5
6from pyquery import PyQuery as pq
html = ""
doc = pq(html)
li = doc('.item-0.active')
li.attr('name', 'link') # item.attr(key, value) 把item的key属性设置为value值 - 改变文本
1
2
3
4
5
6
7from pyquery import PyQuery as pq
html = ""
doc = pq(html)
li = doct('.item-0.active')
li.text('123') # item.text('text_string') 把item的text换成text_string
li.html('<span> new_text_string</div>') #item.html('html_string')把item的HTML文本换成html_string
- 改变class属性
- 伪类选择器(pseudo-class selector)
:first-child
: 第一个匹配的节点:last-child
: 最后一个匹配的节点:nth-child(a * n + b)
: 第a * n + b个匹配的节点, n从0开始:gt(n)
: 第n+1个和之后的匹配节点:contains('text_string')
: 文本中包含text_string的节点
Chapter 5 数据存储
文件存储
- TXT文本存储
- 特点: 操作简单,兼容众多平台,但是不利于检索
- 写法
- 打开方式
r
: 只读,默认模式rb
: 二进制只读r+
: 读写方式rb+
: 二进制读写w
: 覆盖写入wb
:二进制覆盖写入w+
: 覆盖读写a
: 追加ab
: 二进制追加a+
: 读写追加ab+
: 二进制读写追加
- 基本写法
1
2
3file = open('file_name.txt', 'a', encoding='utf-8')
file.write('content_string')
file.close() - 简化写法
1
2with open('file_name.txt', 'a', encoding='utf-8') as f:
f.write('content_string')
- 打开方式
- JSON文件存储
- JSON(JavaScript Object Notation),JavaScript对象标记,通过对对象和数组的组合来表示数据,构造简洁但是结构化程度高,是一种轻量级数据交换格式
- 对象和数组
- JavaScript语言中,所有内容都是对象,因此任何支持的类型都可以使用JSON来表示
- 对象
1
{'key1':'value1', 'key2': 'value2'}
- 数组
1
[{'key': 'value'}, {'key': 'value'}]
- JSON对象由上面的Javascript对象和数组嵌套而成
- 读取JSON: JSON字符串转成JSON对象
- 读取JSON字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import json
# JSON字符串中key和value需要使用双引号,否则会报错 json.decoder.JSONDecodeError
str = '''
[{
"name": "Bob",
"gender": "male",
"birthday": "1992-10-18"
}, {
"name": "Selina",
"gender": "female",
"birthday": "1995-10-18"
}]
'''
print(type(str)) # str
data = json.loads(str)
print(data)
print(type(data)) # list
# 索引内容方法一
print(data[0]['name']) # 如果key不在对象中会报错
# 索引内容方法二: 推荐
print(data[0].get('name')) # 如果key不在对象中会返回None
print(data[0].get('name', 'Nobody')) # 给出default value,如果key不在对象中返回default value - 读取JSON文件内容
1
2
3
4
5
6import json
with open('test.json', 'r') as file:
json_string = file.read()
data = json.loads(json_string)
print(data)
- 读取JSON字符串
- 输出JSON: JSON对象转成JSON字符串
- 基本写法
1
2
3
4
5
6
7
8
9import json
data = [{
'name': 'Bob',
'gender': 'male'
'birthday': '1992-10-18'
}]
with open('result.json', 'w') as file:
file.write(json.dumps(data)) - 输出中包含中文的JSON
1
2
3
4
5
6
7
8
9
10import json
data = [{
'name': '东方洪',
'gender': '蝻'
'birthday': '1776-7-4'
}]
# 写上编码,并在dumps函数中加入参数
with open('funny.json', 'w', encoding='utf-8') as file:
file.write(json.dumps(data, ensure_ascii=False))
- 基本写法
- CSV文件存储
- 特点: 文件以纯文本形式存储数据,整个文件是一个字符序列。记录间用换行符分隔,字段间用逗号或制表符分隔。CSV文件和XLS文件相比不包含文本,数值,公式和格式等内容,适合存储数据
- 写入
- 基本使用
1
2
3
4
5
6
7import csv
with open('test.csv', 'w') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['id', 'name', 'age'])
writer.writerow(['01', 'Bob', 20])
writer.writerow(['02', 'Vivienne', 24]) - User-defined分隔符
1
2
3
4
5import csv
with open('test.csv', 'w') as csvfile:
writer = csv.writer(csvfile, delimiter='\t')
# ...... - 一次写入多行
1
2
3
4
5import csv
with open('test.csv', 'w') as csvfile:
writer = csv.writer(csvfile)
writer.writerows(['row_0_content'], ['row_1_content'], ['row_2_content']) # 写入多行内容 - 写入字典结构化数据
1
2
3
4
5
6
7
8
9
10import csv
with open('test.csv', 'w') as csvfile:
# 定义列名,列名就是字典key的名字
fieldnames = ['name0', 'name1', 'name2']
# 表明使用字典写入
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
# 写入列名
writer.writeheader()
writer.writerow({'name0': 'value0', 'name1': 'value1', 'name2': 'value2'})
- 基本使用
- 读取
- 基本用法
1
2
3
4
5
6import csv
with open('data.csv', 'r', encoding='utf-8') as csvfile:
reader = csv.reader(csvfile)
for row in reader:
print(row) - 使用pandas
1
2
3
4import pandas as pd
df = pd.read_csv('data.csv')
print(df)
- 基本用法
关系型数据库存储
- 前言
- 关系型数据库基于关系模型的数据库,关系模型通过二维表来保存。表中列是字段,行是观测。表之间可以通过主键外键关联,多个表组成一个数据库。
- 关系型数据库举例: SQLite, MySQL, Oracle, SQL Server, DB2
- MySQL的存储
- 连接数据库
1
2
3
4
5
6
7
8import pymysql
db = pymysql.connect(host='localhost', user='root', password='123456', port=3306)
cursor = db.curser()
cursor.execute('select version()')
data = cursor.fetchone()
print(data)
db.close() - 创建表
1
2
3
4
5
6
7
8
9
10import pymysql
db = pymysql.connect(host='localhost', user='root', password='123456', port=3306)
cursor = db.curser()
create_db = 'create database spiders default character set utf8'
cursor.execute(cureate_db)
cursor.execute('use spiders')
create_table = '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(create_table)
db.close() - 插入数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import pymysql
id = '01'
user = 'Bob'
age = 20
db = pymysql.connect(host='localhost', user='root', password='123456', port=3306, db='spiders')
cursor = db.curser()
cursor = db.cursor()
insert_sql = 'INSERT INTO students(id, name, age) values(% s, % s, % s)'
try:
cursor.execute(insert_sql, (id, user, age))
db.commit() # 增删改查加上这一项才是真正的对数据库产生影响
except:
db.rollback() # 如果失败就回滚
db.close() - 更新数据
1
2
3
4
5
6
7
8
9
10
11import pymysql
db = pymysql.connect(host='localhost', user='root', password='123456', port=3306, db='spiders')
cursor = db.curser()
update_sql = 'UPDATE students SET age = % s WHERE name = % s'
try:
cursor.execute(update_sql, (25, 'Bob'))
db.commit()
except:
db.rollback()
db.close() - 删除数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14import pymysql
db = pymysql.connect(host='localhost', user='root', password='123456', port=3306, db='spiders')
cursor = db.curser()
table = 'students'
condition = 'age > 20'
delete_sql = 'DELETE FROM {table} WHERE {condition}'.format(table=table, condition=condition)
try:
cursor.execute(delete_sql)
db.commit()
except:
db.rollback()
db.close() - 查询数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import pymysql
db = pymysql.connect(host='localhost', user='root', password='123456', port=3306, db='spiders')
cursor = db.curser()
query_sql = 'SELECT * FROM students WHERE age >= 20'
try:
cursor.execute(query_sql)
print('Count:', cursor.rowcount)
row = cursor.fetchone()
while row:
print('Row:', row)
row = cursor.fetchone()
except:
print('Error')
- 连接数据库
非关系型数据库存储
- 前言
- NoSQL(Not Only SQL),基于键值对的,不需要经过SQL层的解析,数据之间没有耦合性,新能非常高
- 细分
- 键值存储数据库: Redis, Oracle BDB
- 列存储数据库: HBase, Cassandra
- 文档型数据库: MongoDB
- 图形数据库: NEO4J
- MongoDB
- 略,没装
- Redis
- 略,没装
Chapter 6 Ajax数据爬取
什么是Ajax
- Ajax(Asynchronous JavaScript and XML),指的是利用JavaScript在保证页面不被刷新,页面链接不改变的情况下与服务器交换数据并且更细网页的技术。传统网页更新必须刷新数据,Ajax在后台和服务器进行交互,获取到数据以后,利用JavaScript对网页进行更新。
- 基本原理
- 发送请求:
建立了
XMLHTTPRequest
对象,调用onreadystatechange
属性设置监听,然后调用方法向服务器发送请求。 - 解析内容:
因为设置了监听,当服务器返回响应的时候,
onreadystatechange
对应的方法就会被除法,然后在这个方法里面解析响应内容 - 渲染网页: 解析完响应内容之后,JavaScript通过DOM操作对网页进行更新
- 发送请求:
建立了
Ajax分析方法
- 查看请求
- Chrome开发者工具查看源代码,Ajax请求类型是
XHR
- 在Request
Headers中的
X-Requested-With: XMLHTTPRequest
说明是Ajax请求
- Chrome开发者工具查看源代码,Ajax请求类型是
- 过滤请求
- 在类比里面把
ALL
改成XHR
- 在类比里面把
Ajax结果提取
- 分析请求头和响应头的字段
Chapter 7 动态渲染页面爬取
Selenium
的使用
- 前言
- JavaScript动态渲染页面分类
- 基本Ajax: 分析渲染过程可以直接爬取
- 加密Ajax: 如淘宝,其Ajax接口有很多加密参数,很难找出规律从而不能通过分析Ajax来进行爬取
- 由JavaScript但是未使用Ajax生成的页面
- 为了解决这些问题,我们可以使用模拟浏览器运行的方式实现,这样可以做到所见即可爬,不用再管网页内部的JavaScript使用什么算法渲染页面,不用管后台的Ajax接口到底有哪些参数
- JavaScript动态渲染页面分类
- 基本用法
- 声明浏览器对象
1
2
3
4
5
6
7from selenium import webdriver
browser = webdriver.Chrome()
browser = webdriver.Edge()
browser = webdriver.Firefox()
browser = webdriver.Safari()
browser.close() - 访问页面
1
2
3
4
5
6from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.google.com')
print(browser.page_source)
browser.close() - 查找节点
- 查找单个节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from selenium import webdriver
from selenium.webdriver.common.by import By
# 具体选择函数
browser = webdriver.Chrome()
browser.get('https://www.google.com')
item1 = browser.find_element_by_id('id_string')
item1 = browser.find_element_by_css_selector('#id_string')
item1 = browser.find_element_by_xpath('//*[id="id_string"]')
# 其他函数: find_element_by_name, find_element_by_link_text, find_element_by_partial_link_text, find_element_by_tag_name, find_element_by_class_name, ...
# find_element函数
item1 = browser.find_element(By.ID, 'id_string')
print(item1)
browser.close() - 查找多个节点
1
2
3
4
5
6
7
8
9
10from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.google.com')
items = browser.find_element_by_css_selector('.class_string')
for item in items:
print(item)
browser.close()
items = browser.find_elements(By.CLASS_NAME, 'class_string')
browser.close() - 节点交互
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.google.com')
# 找到输入框
item = browser.find_element_by_id('id_string')
# 模拟输入文字内容,使用send_keys()方法
item.send_keys('input_string')
# 删除输出的文字使用clear()方法
item.clear()
# 找到按钮
item1 = browser.find_element_by_id('button')
# 点击按钮
item1.click()
browser.close() - 动作链:
对于无节点参与的动作,比如拖拽鼠标,键盘按键等动作,使用动作链来完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 拖拽鼠标举例
from selenium import webdriver
from selenium.webdriver import ActionChains
browser = webdriver.Chrome()
url = 'http://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()
browser.close() - 模拟JavaScript
1
2
3
4
5
6
7
8
9from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.zhihu.com/explore')
# 拖拽进度条到底部
browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
# 弹出对话框
browser.execute_script('alert("To Bottom")')
browser.close() - 获取节点信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19from selenium import webdriver
browser = webdriver.Chrome()
brower.get('https://www.google.com')
item = browser.find_element_by_id('q')
# 获取属性
print(item.get_attribute('class'))
# 获取文本值
print(item.get_text())
# 获取id
print(item.id)
# 获取位置
print(item.location)
# 获取标签名
print(item.tag_name)
# 获取大小
print(item.size)
browser.close() - 延时等待
- 说明:
get()
函数会在网页全加载完之后再执行,如果再没加载完(比如有Ajax)就打印page_resouce的话,可能不是完整页面。可以设置一定的等待时间,以供网页加载。 - 隐式等待:
如果节点没有出现,则进行等待一段时间再查找节点,如果还没出现则抛出异常
1
2
3
4
5
6
7
8from selenium import webdriver
browser = webdriver.Chrome()
browser.implicitly_wait(10)
browser.get('https://www.google.com')
input = browser.find_element_by_id('q')
print(input)
browser.close() - 显式等待:
指定最长等待时间,如果时间内节点条件达成(加载出来/节点可见/节点可以被点击etc)就返回节点,如果超出时间限制则抛出异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
browser = webdriver.Chrome()
browser.get('https://www.google.com/')
# 设置最长等待时间
wait = WebDriverWait(browser, 10)
# 设置等待条件为节点出现
input = wait.until(EC.presence_of_element_located((By.ID, 'q')))
# 设置等待条件为节点可以被点击
button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '.btn-search')))
print(input, button)
browser.close()
- 说明:
- 前进和后退
1
2
3
4
5
6
7
8from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.google.com')
browser.get('https://www.bing.com')
browser.back()
browser.forward()
browser.close() - Cookies
1
2
3
4
5
6
7
8
9
10
11
12from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.zhihu.com/explore')
# 获取Cookies
print(browser.get_cookies())
# 以字典形式添加Cookies
browser.add_cookie({'name': 'name', 'domain': 'www.zhihu.com', 'value': 'germey'})
print(browser.get_cookies())
# 删除所有Cookies
browser.delete_all_cookies()
print(browser.get_cookies())
- 查找单个节点
- 声明浏览器对象
Splash
的使用
略
Splash
负载均衡配置
略
Chapter 8 验证码的识别
图形验证码的识别
- 最简单的图形或者数字验证码,可能有干扰线,可以使用OCR(Optical Character Recognition)技术进行识别
- 简单验证码的识别
1
2
3
4
5
6
7
8
9import tesserocr
from PIL import Image
# 读取文件形成图片再转换
image = Image.open('./test.img')
result = tesserocr.image_to_text(image)
print(result)
# 直接读取文件然后转换
result = tesserocr.file_to_text('./test.img')
print(result) - 有干扰线的验证码的识别
- 这里只考虑简单的灰度处理(增加对比度,突出主要区域),二值化等操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import tesserocr
from PIL import Image
image = Image.open('./2.png')
# 转成灰度图片
image = image.convert('L')
# 设置二值化门槛,大于的设置为1,即灰度255,为目标,小于的设置为0,即灰度0,为背景
threshold = 160
# 这个做法默认了干扰线和目标相比是浅色不清晰的
table = []
for i in range(256):
if i < threshold:
table.append(0)
else:
table.append(1)
# 转成二值化图片
image = image.point(table, '1')
result = tesserocr.image_to_text(image)
print(result) - 还有一些用到了ML的去除干扰线的算法
- 这里只考虑简单的灰度处理(增加对比度,突出主要区域),二值化等操作
极验滑动验证码
- 拖动拼接滑块,需要注意的是,滑块对齐只是一个要素,还有拖动速度不能被检测出是机器模拟拖动,需要接近人来拖动
- 整体思路
- 模拟点击验证按钮
1
2
3
4
5
6
7
8
9def get_geetest_button(self):
"""
获取初始验证按钮
:return: 按钮对象
"""
button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_radar_tip')))
return button
button = get_geetest_button()
button.click() - 识别滑动缺口的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68def get_position(self):
"""
获取验证码位置
:return: 验证码位置元组
"""
img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_img')))
time.sleep(2)
location = img.location
size = img.size
top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size['width']
return (top, bottom, left, right)
def get_geetest_image(self, name='captcha.png'):
"""
获取验证码图片
:return: 图片对象
"""
top, bottom, left, right = self.get_position()
print(' 验证码位置 ', top, bottom, left, right)
screenshot = self.get_screenshot()
# 从验证码位置切出来完整图片
captcha = screenshot.crop((left, top, right, bottom))
return captcha
def get_slider(self):
"""
获取滑块
:return: 滑块对象
"""
slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button')))
return slider
slider = self.get_slider()
slider.click()
def is_pixel_equal(self, image1, image2, x, y):
"""
判断两个像素是否相同
:param image1: 图片 1
:param image2: 图片 2
:param x: 位置 x
:param y: 位置 y
:return: 像素是否相同
"""
# 取两个图片的像素点
pixel1 = image1.load()[x, y]
pixel2 = image2.load()[x, y]
threshold = 60
if abs(pixel1[0] - pixel2[0]) <threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(pixel1[2] - pixel2[2]) < threshold:
return True
else:
return False
def get_gap(self, image1, image2):
"""
获取缺口偏移量
:param image1: 不带缺口图片
:param image2: 带缺口图片
:return:
"""
# 缺口不可能再最左边,所以设置左侧起始位置
left = 60
for i in range(left, image1.size[0]):
for j in range(image1.size[1]):
if not self.is_pixel_equal(image1, image2, i, j):
left = i
return left
return left - 模拟拖动滑块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49# 模拟人类先加速再减速的拖动滑块过程
def get_track(self, distance):
"""
根据偏移量获取移动轨迹
:param distance: 偏移量
:return: 移动轨迹
"""
# 移动轨迹
track = []
# 当前位移
current = 0
# 减速阈值
mid = distance * 4 / 5
# 计算间隔
t = 0.2
# 初速度
v = 0
while current < distance:
if current < mid:
# 加速度为正 2
a = 2
else:
# 加速度为负 3
a = -3
# 初速度 v0
v0 = v
# 当前速度 v = v0 + at
v = v0 + a * t
# 移动距离 x = v0t + 1/2 * a * t^2
move = v0 * t + 1 / 2 * a * t * t
# 当前位移
current += move
# 加入轨迹
track.append(round(move))
return track
def move_to_gap(self, slider, tracks):
"""
拖动滑块到缺口处
:param slider: 滑块
:param tracks: 轨迹
:return:
"""
ActionChains(self.browser).click_and_hold(slider).perform()
for x in tracks:
ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
ActionChains(self.browser).release().perform()
- 模拟点击验证按钮
点触验证码的识别
chaojiying充值识别,略
微博宫格验证码识别
没见过,略
Chapter 9 代理的使用
代理的设置
urllib
代理设置- 普通代理设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener
# 代理内容
proxy = '127.0.0.1:1080'
# 通过ProxyHandler构造代理
proxy_handler = ProxyHandler({
'http': 'http://' + proxy,
'https': 'https://' + proxy
})
# 通过builder_opener构造Opener
opener = build_opener(proxy_handler)
try:
response = opener.open('http://httpbin.org/get')
print(response)
except URLError as e:
print(e.reason) - 需登录代理设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener
proxy = 'user:password@127.0.0.1:1080'
proxy_handler = ProxyHandler({
'http': 'http://' + proxy,
'https': 'https://' + proxy
})
opener = build_opener(proxy_handler)
try:
response = opener.open('http://httpbin.org/get')
print(response)
except URLError as e:
print(e.reason)
- 普通代理设置
requests
代理设置- 普通代理设置
1
2
3
4
5
6
7
8
9
10
11
12import requests
proxy = '127.0.0.1:1080'
proxies = {
'http': 'http://' + proxy,
'https': 'https://' + proxy
}
try:
response = requests.get('http:httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error:', e.args) - 需登录代理设置
1
2
3
4
5
6
7
8
9
10
11
12import requests
proxy = 'user:password@127.0.0.1:1080'
proxies = {
'http': 'http://' + proxy,
'https': 'https://' + proxy
}
try:
response = requests.get('http:httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error:', e.args)
- 普通代理设置
Selenium
代理设置- 普通代理设置
1
2
3
4
5
6
7
8# 这里需要在本地创建一个 manifest.json 配置文件和 background.js 脚本来设置认证代理。运行代码之后本地会生成一个 proxy_auth_plugin.zip 文件来保存当前配置
from selenium import webdriver
proxy = '127.0.0.1:1080'
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--proxy-server=http://' + proxy)
browser = webdriver.Chrome(chrome_options=chrome_options)
browser.get('http://httpbin.org/get') - 需要登录代理设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import zipfile
ip = '127.0.0.1'
port = 1080
username = 'foo'
password = 'bar'
manifest_json = """{"version":"1.0.0","manifest_version": 2,"name":"Chrome Proxy","permissions": ["proxy","tabs","unlimitedStorage","storage","<all_urls>","webRequest","webRequestBlocking"],"background": {"scripts": ["background.js"]
}
}
"""background_js ="""
var config = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: "http",
host: "%(ip) s",
port: %(port) s
}
}
}
chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});
function callbackFn(details) {
return {
authCredentials: {username: "%(username) s",
password: "%(password) s"
}
}
}
chrome.webRequest.onAuthRequired.addListener(
callbackFn,
{urls: ["<all_urls>"]},
['blocking']
)
""" % {'ip': ip, 'port': port, 'username': username, 'password': password}
plugin_file = 'proxy_auth_plugin.zip'
with zipfile.ZipFile(plugin_file, 'w') as zp:
zp.writestr("manifest.json", manifest_json)
zp.writestr("background.js", background_js)
chrome_options = Options()
chrome_options.add_argument("--start-maximized")
chrome_options.add_extension(plugin_file)
browser = webdriver.Chrome(chrome_options=chrome_options)
browser.get('http://httpbin.org/get')
- 普通代理设置
代理池的维护
略
付费代理的使用
略
ADSL拨号代理
略