第一章:爬虫介绍

什么是爬虫:

爬虫是一个模拟人类请求网站行为的程序。可以自动请求网页、并把数据抓取下来,然后使用一定的规则提取有价值的数据。

爬虫应用场景:

  1. 搜索引擎(百度或谷歌等)
  2. 伯乐在线。
  3. 惠惠购物助手。
  4. 数据分析。
  5. 抢票软件等。

为什么用Python写爬虫:

  1. PHP:PHP是世界是最好的语言,但他天生不是做这个的,而且对多线程、异步支持不是很好,并发处理能力弱。爬虫是工具性程序,对速度和效率要求比较高。
  2. Java:生态圈很完善,是Python爬虫最大的竞争对手。但是Java语言本身很笨重,代码量很大。重构成本比较高,任何修改会导致代码大量改动。爬虫经常要修改采集代码。
  3. C/C++:运行效率是无敌的。但是学习和开发成本高。写个小爬虫程序可能要大半天时间。
  4. Python:语法优美、代码简洁、开发效率高、支持的模块多。相关的HTTP请求模块和HTML解析模块非常丰富。还有Scrapy和Scrapy-redis框架让我们开发爬虫变得异常简单。

开发工具的安装:

安装Python:

下载地址:https://www.python/
或者使用Anaconda:https://www.anaconda/
哪个软件无所谓,只要是Python3.6以上的版本就可以了。

安装Pycharm:

Pycharm Commnity(社区版):对于不需要做web开发,不需要使用Professional(专业版),使用Community版就足够了,并且这个软件功能很强大,而且免费!
下载地址:https://www.jetbrains/pycharm/download/#section=windows

安装Chrome:

下载地址:https://www.google/chrome/
如果是下载不了,说明被墙了,需要翻墙。


Http协议:

Http协议介绍:

  1. Http协议:全称是HyperText Transfer Protocol,中文意思是超文本传输协议,是一种发布和接收HTML(HyperText Markup Language)页面的方法。服务器端口号是80端口。
  2. HTTPS协议:是HTTP协议的加密版本,在HTTP下加入了SSL层。服务器端口号是443端口。

更多介绍请参考:https://baike.baidu/item/HTTP

URL详解:

URL是Uniform Resource Locator的简写,统一资源定位符。 一个URL由以下几部分组成:
scheme://host:port/path/?query-string=xxx#anchor

scheme:代表的是访问的协议,一般为http或者https以及ftp等。
host:主机名,域名,比如www.baidu。
port:端口号。当你访问一个网站的时候,浏览器默认使用80端口。
path:查找路径。比如:www.jianshu/trending/now,后面的trending/now就是path。
query-string:查询字符串,比如:www.baidu/s?wd=python,后面的wd=python就是查询字符串。
anchor:锚点,前端用来做页面定位的。现在一些前后端分离项目,也用锚点来做导航。

在浏览器中请求一个url,浏览器会对这个url进行一个编码。除英文字母,数字和部分符号外,其他的全部使用百分号+十六进制码值进行编码。

常见的请求Method:

在Http协议中,定义了八种请求方法。这里介绍两种常用的请求方法,分别是get请求和post请求。

  1. get请求:一般情况下,只从服务器获取数据下来,并不会对服务器资源产生任何影响的时候会使用get请求。
  2. post请求:向服务器发送数据(登录)、上传文件等,会对服务器资源产生影响的时候会使用post请求。 以上是在网站开发中常用的两种方法。并且一般情况下都会遵循使用的原则。但是有的网站和服务器为了做反爬虫机制,也经常会不按常理出牌,有可能一个应该使用get方法的请求就一定要改成post请求,这个要视情况而定。

常见的请求头参数:

在http协议中,向服务器发送一个请求,数据分为三部分,第一个是把数据放在url中,第二个是把数据放在body中(在post请求中),第三个就是把数据放在head中。这里介绍在网络爬虫中经常会用到的一些请求头参数:

User-Agent:浏览器名称。这个在网络爬虫中经常会被使用到。请求一个网页的时候,服务器通过这个参数就可以知道这个请求是由哪种浏览器发送的。如果我们是通过爬虫发送请求,那么我们的User-Agent就是Python,这对于那些有反爬虫机制的网站来说,可以轻易的判断你这个请求是爬虫。因此我们要经常设置这个值为一些浏览器的值,来伪装我们的爬虫。

Referer:表明当前这个请求是从哪个url过来的。这个一般也可以用来做反爬虫技术。如果不是从指定页面过来的,那么就不做相关的响应。

Cookie:http协议是无状态的。也就是同一个人发送了两次请求,服务器没有能力知道这两个请求是否来自同一个人。因此这时候就用cookie来做标识。一般如果想要做登录后才能访问的网站,那么就需要发送cookie信息了。

常见的响应状态码:

200:请求正常,服务器正常的返回数据。
301:永久重定向。比如在访问www.jingdong的时候会重定向到www.jd。
302:临时重定向。比如在访问一个需要登录的页面的时候,而此时没有登录,那么就会重定向到登录页面。
400:请求的url在服务器上找不到。换句话说就是请求url错误。
403:服务器拒绝访问,权限不够。
500:服务器内部错误。可能是服务器出现bug了。

Chrome抓包工具:

Elements:

可以帮助我们分析网页结构,获取我们想要的数据。但是Elements下是最终呈现的网页数据,有时候网页数据是通过ajax请求得到的,因此Elements下的数据不能完全相信。

Console:

用来打印网页的一些信息。

Sources:

整个网页所加载的所有文件。

Network:

查看整个网页发送的所有网络请求。一般我们想要去查看某个请求的信息,都可以到这个里面去看。

第二章:网络请求

1.urllib库 —内置函数

urlopen函数:

创建一个表示远程url的类文件对象,然后像本地文件一样操作这个类文件对象来获取远程数据。

url:请求的url。
data:请求的data,如果设置了这个值,那么将变成post请求。
返回值:返回值是一个http.client.HTTPResponse对象,这个对象是一个类文件句柄对象。有read(size)、readline、readlines以及getcode等方法。

urlretrieve函数:

这个函数可以方便的将网页上的一个文件保存到本地。

request.urlretrieve(url,文件名)

urlencode函数:编码

urlencode可以把字典数据转换为URL编码的数据。

from urllib import parse

data = {'name':'老王','age':18,'greet':'hello world'}

qs = parse.urlencode(data)
print(qs)

#name=%E8%80%81%E7%8E%8B&age=18&greet=hello+world

parse_qs函数:解码

可以将经过编码后的url参数进行解码

print(parse.parse_qs(qs))
# {'name': ['老王'], 'age': ['18'], 'greet': ['hello world']}

urlparse和urlsplit函数:解析url

from urllib import parse

url = 'http://www.baidu/index.html;user?id=S#comment'

result = parse.urlparse(url)
# result = parse.urlsplit(url)

print(result)
print(result.scheme)
print(result.netloc)
print(result.path)
#urlparse里有params属性,而urlsplit没有这个params属性。
print(result.params)

request.Request类:网络请求 可以增加请求头

from urllib import request

headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 			(KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
}

rq = request.Request('https://www.baidu/',headers=headers)


resp = request.urlopen(rq)

print(resp.read())

ProxyHandler处理器(代理设置):封ip问题

  1. 代理原理:在请求目的网站之前,先请求代理服务器,然后让代理服务器去请求目的网站,代理服务器拿到目的网站的数据后,再转发给我们的代码。

  2. http://httpbin:这个网站可以方便的查看http请求的一些参数。

  3. 在代码中使用代理 示例:

    # 使用代理
    # 步骤
    url = 'http://httpbin/ip'
    #1. 使用ProxyHandler,传入代理构建一个handler
    handler = request.ProxyHandler({'http':'122.193.244.243:9999'})
    #2. 使用上面创建的handler构建一个opener
    opener = request.build_opener(handler)
    #3. 使用opener去发送一个请求
    resp = opener.open(url)
    print(resp.read())
    

cookie: 登录

  1. 什么是cookie:指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据

  2. cookie的格式:

    Set-Cookie: NAME=VALUE;Expires/Max-age=DATE;Path=PATH; Domain=DOMAIN_NAME;SECURE

    参数意义:

    NAME:cookie的名字。
    VALUE:cookie的值。
    Expires:cookie的过期时间。
    Path:cookie作用的路径。
    Domain:cookie作用的域名。
    SECURE:是否只在https协议下起作用。

http.cookiejar模块:提供用于存储cookie的对象

  1. CookieJar:管理HTTP cookie值、存储HTTP请求生成的cookie、向传出的HTTP请求添加cookie的对象。整个cookie都存储在内存中,对CookieJar实例进行垃圾回收后cookie也将丢失。

  2. FileCookieJar (filename,delayload=None,policy=None):从CookieJar派生而来,用来创建FileCookieJar实例,检索cookie信息并将cookie存储到文件中。filename是存储cookie的文件名。delayload为True时支持延迟访问访问文件,即只有在需要时才读取文件或在文件中存储数据。

  3. MozillaCookieJar (filename,delayload=None,policy=None):从FileCookieJar派生而来,创建与Mozilla浏览器 cookies.txt兼容的FileCookieJar实例。

  4. LWPCookieJar (filename,delayload=None,policy=None):从FileCookieJar派生而来,创建与libwww-perl标准的 Set-Cookie3 文件格式兼容的FileCookieJar实例。

实例:

from urllib import request
from urllib import parse
from http.cookiejar import  CookieJar

# 登录:https://i.meishi/login.php?redirect=https%3A%2F%2Fwww.meishij%2F
#个人网页https://i.meishi/cook.php?id=13686422

headers={
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
}

#1.登录
#1.1 创建cookiejar对象
cookiejar = CookieJar()
#1.2 使用cookiejar创建一个HTTPCookieProcess对象
handler = request.HTTPCookieProcessor(cookiejar)
#1.3 使用上一步的创建的handler创建一个opener
opener = request.build_opener(handler)
#1.4 使用opener发送登录请求  (账号和密码)

post_url = 'https://i.meishi/login.php?redirect=https%3A%2F%2Fwww.meishij%2F'
post_data = parse.urlencode({
    'username':'1097566154@qq',
    'password':'wq15290884759.'
})
req = request.Request(post_url,data=post_data.encode('utf-8'))
opener.open(req)


#2.访问个人网页
url = 'https://i.meishi/cook.php?id=13686422'
rq = request.Request(url,headers=headers)
resp = opener.open(rq)
print(resp.read().decode('utf-8'))

cookie加载与保存

from urllib import request
from http.cookiejar import MozillaCookieJar

# 保存
# cookiejar = MozillaCookieJar('cookie.txt')
# handler = request.HTTPCookieProcessor(cookiejar)
# opener = request.build_opener(handler)
# resp = opener.open('http://www.httpbin/cookies/set/course/abc')
#
# cookiejar.save(ignore_discard=True,ignore_expires=True)

# ignore_discard=True  即使cookies即将被丢弃也要保存下来
# ignore_expires=True  如果cookies已经过期也将它保存并且文件已存在时将覆盖

#加载

cookiejar = MozillaCookieJar('cookie.txt')
cookiejar.load()
handler = request.HTTPCookieProcessor(cookiejar)
opener = request.build_opener(handler)
resp = opener.open('http://www.httpbin/cookies/set/course/abc')

for cookie in cookiejar:
    print(cookie)

2.requests库 —第三方库

Requests:让HTTP服务人类

安装和文档地址:

pip install requests

发送GET请求

import requests

# 添加headers和查询参数
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
}
kw = {'wd':'中国'}
# params 接收一个字典或者字符串的查询参数,字典类型自动转换为url编码,不需要urlencode()
response = requests.get('https://www.baidu/s',headers=headers,params=kw)
print(response)

# 属性
# 查询响应内容
print(response.text)  #返回unicode格式的数据
print(response.content) #返回字节流数据
print(response.url)  #查看完整url地址
print(response.encoding) # 查看响应头部字符编码

response.text和response.content的区别:

  1. response.content :这个是直接从网络上抓取的数据,没有经过任何的编码,所以是一个bytes类型,其实在硬盘上和网络上传输的字符串都是bytes类型
  2. response.text:这个是str的数据类型,是requests库将response.content进行解码的字符串,解码需要指定一个编码方式,requests会根据自己的猜测来判断编码的方式,所以有时候可能会猜测错误,就会导致解码产生乱码,这时候就应该进行手动解码,比如使用response.content.decode('utf-8')

发送POST请求:

response = requests.post("http://www.baidu/",data=data)

POST请求方式

import requests

url = 'https://i.meishi/login.php?redirect=https%3A%2F%2Fwww.meishij%2F'
headers={
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
}
data = {
    'redirect': 'https://www.meishij/',
    'username': '1097566154@qq',
    'password': 'wq15290884759.'
}
resp = requests.post(url,headers=headers,data=data)
print(resp.text)

使用代理:

只要在请求的方法中(比如get或者post)传递proxies参数就可以了。

import requests

proxy = {
    'http':'111.77.197.127:9999'
}
url = 'http://www.httpbin/ip'
resp = requests.get(url,proxies=proxy)
print(resp.text)

cookie:

基本使用:模拟登陆

import requests
url = 'https://www.zhihu/hot'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
    'cookie':'_zap=59cde9c3-c5c0-4baa-b756-fa16b5e72b10; d_c0="APDi1NJcuQ6PTvP9qa1EKY6nlhVHc_zYWGM=|1545737641"; __gads=ID=237616e597ec37ad:T=1546339385:S=ALNI_Mbo2JturZesh38v7GzEeKjlADtQ5Q; _xsrf=pOd30ApWQ2jihUIfq94gn2UXxc0zEeay; q_c1=1767e338c3ab416692e624763646fc07|1554209209000|1545743740000; tst=h; __utma=51854390.247721793.1554359436.1554359436.1554359436.1; __utmc=51854390; __utmz=51854390.1554359436.1.1.utmcsr=zhihu|utmccn=(referral)|utmcmd=referral|utmcct=/hot; __utmv=51854390.100-1|2=registration_date=20180515=1^3=entry_date=20180515=1; l_n_c=1; l_cap_id="OWRiYjI0NzJhYzYwNDM3MmE2ZmIxMGIzYmQwYzgzN2I=|1554365239|875ac141458a2ebc478680d99b9219c461947071"; r_cap_id="MmZmNDFkYmIyM2YwNDAxZmJhNWU1NmFjOGRkNDNjYjc=|1554365239|54372ab1797cba8c4dd224ba1845dd7d3f851802"; cap_id="YzQwNGFlYWNmNjY3NDFhNGI4MGMyYjZjYjRhMzQ1ZmE=|1554365239|385cc25e3c4e3b0b68ad5747f623cf3ad2955c9f"; n_c=1; capsion_ticket="2|1:0|10:1554366287|14:capsion_ticket|44:MmE5YzNkYjgzODAyNDgzNzg5MTdjNmE3NjQyODllOGE=|40d3498bedab1b7ba1a247d9fc70dc0e4f9a4f394d095b0992a4c85e32fd29be"; z_c0="2|1:0|10:1554366318|4:z_c0|92:Mi4xOWpCeUNRQUFBQUFBOE9MVTBseTVEaVlBQUFCZ0FsVk5iZzJUWFFEWi1JMkxnQXlVUXh2SlhYb3NmWks3d1VwMXRB|81b45e01da4bc235c2e7e535d580a8cc07679b50dac9e02de2711e66c65460c6"; tgw_l7_route=578107ff0d4b4f191be329db6089ff48'
}
resp = requests.get(url,headers=headers)
print(resp.text)

session:共享cookie

案例:

post_url = 'https://i.meishi/login.php?redirect=https%3A%2F%2Fwww.meishij%2F'

post_data = {
    'username':'1097566154@qq',
    'password':'wq15290884759.'
}
headers={
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
}

# 登录
session = requests.session()
session.post(post_url,headers=headers,data=post_data)


#访问个人网页
url = 'https://i.meishi/cook.php?id=13686422'

resp = session.get(url)
print(resp.text)

处理不信任的SSL证书:

对于那些已经被信任的SSL证书的网站,比如https://www.baidu/,那么使用requests直接就可以正常的返回响应。示例代码如下:

resp = requests.get('https://inv-veri.chinatax.gov/',verify=False)
print(resp.content.decode('utf-8'))

第三章:数据请求

XPath语法和lxml模块

什么是XPath?

xpath(XML Path Language)是一门在XML和HTML文档中查找信息的语言,可用来在XML和HTML文档中对元素和属性进行遍历。

XPath开发工具

Chrome插件XPath Helper。

安装方法:

  1. 打开插件伴侣,选择插件
  2. 选择提取插件内容到桌面,桌面上会多一个文件夹
  3. 把文件夹放入想要放的路径下
  4. 打开谷歌浏览器,选择扩展程序,开发者模式打开,选择加载已解压的扩展程序,选择路径打开即可

Firefox插件Try XPath。

XPath节点

在 XPath 中,有七种类型的节点:元素、属性、文本、命名空间、处理指令、注释以及文档(根)节点。XML 文档是被作为节点树来对待的。树的根被称为文档节点或者根节点。

XPath语法

使用方式:

使用//获取整个页面当中的元素,然后写标签名,然后在写谓语进行提取,比如:

//title[@lang='en']

需要注意的知识点:

  1. /和//的区别:/代表只获取子节点,//获取子孙节点,一般//用的比较多,当然也要视情况而定

  2. contains:有时候某个属性中包含了多个值,那么可以使用contains函数,示例如下:

    //title[contains(@lang,'en')]
    
  3. 谓词中下标是从1开始的,不是从0开始的

lxml库

lxml 是 一个HTML/XML的解析器,主要的功能是如何解析和提取 HTML/XML 数据。

基本使用:

from 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文档
html = etree.HTML(text)
print(html)
# 按字符串序列化html
result = etree.tostring(html).decode('utf-8')
print(result)

从文件中读取html代码:

#读取
html = etree.parse('hello.html')

result = etree.tostring(html).decode('utf-8')
print(result)

在lxml中使用xpath语法

<!-- hello.html -->
<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"><span class="bold">third item</span></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></li>
     </ul>
 </div>

语法练习

from lxml import etree
html = etree.parse('hello.html')
# 获取所有li标签:
result = html.xpath('//li')
print(result)
for i in result:
    print(etree.tostring(i))
# 获取所有li元素下的所有class属性的值:
result = html.xpath('//li/@class')
print(result)
# 获取li标签下href为www.baidu的a标签:
result = html.xpath('//li/a[@href="www.baidu"]')
print(result)
# 获取li标签下所有span标签:
result = html.xpath('//li//span')
print(result)
# 获取li标签下的a标签里的所有class:
result = html.xpath('//li/a//@class')
print(result)
# 获取最后一个li的a的href属性对应的值:
result = html.xpath('//li[last()]/a/@href')
print(result)
# 获取倒数第二个li元素的内容:
result = html.xpath('//li[last()-1]/a')
print(result)
print(result[0].text)
# 获取倒数第二个li元素的内容的第二种方式:
result = html.xpath('//li[last()-1]/a/text()')
print(result)

BeautifulSoup4库

和 lxml 一样,Beautiful Soup 也是一个HTML/XML的解析器,主要的功能也是如何解析和提取 HTML/XML 数据。

安装和文档:

安装:
pip install bs4

中文文档:https://www.crummy/software/BeautifulSoup/bs4/doc/index.zh.html

几大解析工具对比:

简单使用:

from bs4 import BeautifulSoup

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/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example/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')
print(soup.prettify())

常见的四种对象:

  1. Tag:BeautifulSoup中所有的标签都是Tag类型,并且BeautifulSoup的对象其实本质上也是一个Tag类型。所以其实一些方法比如find、find_all并不是BeautifulSoup的,而是Tag的。
  2. NavigableString:继承自python中的str,用起来就跟使用python的str是一样的。
  3. BeautifulSoup:继承自Tag。用来生成BeaufifulSoup树的。对于一些查找方法,比如find、select这些,其实还是Tag的。
  4. Comment:这个也没什么好说,就是继承自NavigableString。

contents和children:

返回某个标签下的直接子元素,其中也包括字符串。他们两的区别是:contents返回来的是一个列表,children返回的是一个迭代器。

string和strings、stripped_strings属性以及get_text方法

  1. string:获取某个标签下的非标签字符串。返回来的是个字符串。如果这个标签下有多行字符,那么就不能获取到了。
  2. strings:获取某个标签下的子孙非标签字符串。返回来的是个生成器。
  3. stripped_strings:获取某个标签下的子孙非标签字符串,会去掉空白字符。返回来的是个生成器。
  4. get_text:获取某个标签下的子孙非标签字符串,以普通字符串形式返回

find_all的使用:

  1. 在提取标签的时候,第一个参数是标签的名字。然后如果在提取标签的时候想要使用标签属性进行过滤,那么可以在这个方法中通过关键字参数的形式,将属性的名字以及对应的值传进去。或者是使用attrs属性,将所有的属性以及对应的值放在一个字典中传给attrs属性。
  2. 有些时候,在提取标签的时候,不想提取那么多,那么可以使用limit参数。限制提取多少个。

find与find_all的区别:

  1. find:找到第一个满足条件的标签就返回。说白了,就是只会返回一个元素。
  2. find_all:将所有满足条件的标签都返回。说白了,会返回很多标签(以列表的形式)。

使用find和find_all的过滤条件:

  1. 关键字参数:将属性的名字作为关键字参数的名字,以及属性的值作为关键字参数的值进行过滤。
  2. attrs参数:将属性条件放到一个字典中,传给attrs参数。

获取标签的属性:

  1. 通过下标获取:通过标签的下标的方式。

    href = a['href']
    
  2. 通过attrs属性获取:示例代码:

    href = a.attrs['href']
    

CSS选择器:

select方法:

使用以上方法可以方便的找出元素。但有时候使用css选择器的方式可以更加的方便。使用css选择器的语法,应该使用select方法。以下列出几种常用的css选择器方法:

(1)通过标签名查找:

print(soup.select('a'))

(2)通过类名查找:

通过类名,则应该在类的前面加一个.。比如要查找class=sister的标签。示例代码如下:

print(soup.select('.sister'))

(3)通过id查找:

通过id查找,应该在id的名字前面加一个#号。示例代码如下:

print(soup.select("#link1"))

(4)组合查找:

组合查找即和写 class 文件时,标签名与类名、id名进行的组合原理是一样的,例如查找 p 标签中,id 等于 link1的内容,二者需要用空格分开:

print(soup.select("p #link1"))

直接子标签查找,则使用 > 分隔:

print(soup.select("head > title"))

(5)通过属性查找:

查找时还可以加入属性元素,属性需要用中括号括起来,注意属性和标签属于同一节点,所以中间不能加空格,否则会无法匹配到。示例代码如下:

print(soup.select('a[href="http://example/elsie"]'))

(6)获取内容

以上的 select 方法返回的结果都是列表形式,可以遍历形式输出,然后用 get_text() 方法来获取它的内容。

soup = BeautifulSoup(html, 'lxml')
print(type(soup.select('title')))
print(soup.select('title')[0].get_text())

for title in soup.select('title'):
    print(title.get_text())

正则表达式:

单字符匹配

import re
# 匹配某个字符串:
# text = "abc"
ret = re.match('b',text)
print(ret.group())


# 点(.):匹配任意的字符(除了'\n'):
text = "\nabc"
ret = re.match('.',text)
print(ret.group())


# \d:匹配任意的数字:
text = "aab"
ret = re.match('\d',text)
print(ret.group())


# \D:匹配任意的非数字:
text = "cab"
ret = re.match('\D',text)
print(ret.group())


# \s:匹配的是空白字符(包括:\n,\t,\r和空格):
text = " ab"
ret = re.match('\s',text)
print("="*30)
print(ret.group())
print("="*30)


# \S:非空白字符:
text = "\nab"
ret = re.match('\S',text)
print("="*30)
print(ret.group())
print("="*30)


# \w:匹配的是a-z和A-Z以及数字和下划线:
text = "+bc"
ret = re.match('\w',text)
print("="*30)
print(ret.group())
print("="*30)


# \W:匹配的是和\w相反的:
text = "1bc"
ret = re.match('\W',text)
print("="*30)
print(ret.group())
print("="*30)


# []组合的方式,只要满足中括号中的某一项都算匹配成功:
text = "bc"
ret = re.match('[1b]',text)
print("="*30)
print(ret.group())
print("="*30)

# 使用组合的方式[0-9]\d:
text = "abc"
ret = re.match('[^0-9]',text)
print("="*30)
print(ret.group())
print("="*30)

# 使用组合的方式实现\w:
text = "+bc"
ret = re.match('[^a-zA-Z0-9_]',text)
print("="*30)
print(ret.group())
print("="*30)

多字符匹配

import re

# *:匹配0个或者多个字符:
text = "+abc"
result = re.match('\D*',text)
print(result.group())


# +:匹配1个或者多个字符:
text = "1abc"
result = re.match('\w+',text)
print(result.group())


# ?:匹配前一个字符0个或者1个:
text = "+abc"
result = re.match('\w?',text)
print(result.group())


# {m}:匹配m个字符:
text = "+1abc"
result = re.match('\w{2}',text)
print(result.group())


# {m,n}:匹配m-n之间的个数的字符:
text = "1abc+"
result = re.match('\w{1,3}',text)
print(result.group())

正则表达式案例

# 1. 验证手机号码:手机号码的规则是以1开头,第二位可以是34587,后面那9位就可以随意了。
text = "18677889900"
result = re.match("1[34587]\d{9}",text)
print(result.group())


# 2. 验证邮箱:邮箱的规则是邮箱名称是用数字、英文字符、下划线组成的,然后是@符号,后面就是域名了。
text = "hynever@163"
result = re.match("\w+@[a-z0-9]+\.[a-z]+",text)
print(result.group())


# 3. 验证URL:URL的规则是前面是http或者https或者是ftp然后再加上一个冒号,再加上一个斜杠,再后面就是可以出现任意非空白字符了。
text = "https://baike.baidu/item/Python/407313?fr=aladdin"
result = re.match("(http|https|ftp)://\S+",text)
print(result.group())


# 4. 验证身份证:身份证的规则是,总共有18位,前面17位都是数字,后面一位可以是数字,也可以是小写的x,也可以是大写的X。
text = "36530019870716234x"
result = re.match("\d{17}[\dxX]",text)
print(result.group())

开始/结束/贪婪和非贪婪

# ^:以...开头:
text = "hello world"
result = re.search("world",text)
print(result.group())


# $:以...结尾:
text = "hello world"
result = re.search("hello$",text)
print(result.group())
text = ""
result = re.search("^$",text)
print(result.group())


# |:匹配多个字符串或者表达式:



# 贪婪和非贪婪:
text = "12345"
result = re.search("\d+?",text)
print(result.group())


# 案例1:提取html标签名称:
text = "<h1>这是标题</h1>"
result = re.search("<.+?>",text)
print(result.group())


# 案例2:验证一个字符是不是0-100之间的数字:
# 0,1,99,100
# 01
text = "101"
result = re.match("0$|[1-9]\d?$|100$",text)
print(result.group())

转义字符和原生字符串

# Python中的转义字符:
# raw
text = r"hello\nworld"
print(text)


# 正则表达式中的转义字符:
text = "apple price is $99,range price is $88"
result = re.findall("\$\d+",text)
print(result)


# 原生字符串和正则表达式:
# 正则表达式的字符串解析规则:
# 1. 先把这个字符串放在Python语言层面进行解析。
# 2. 把Python语言层面解析的结果再放到正则表达式层间进行解析。
text = "\cba c"
result = re.match("\\\\c",text) # \\\\c =(Python语言层面)> \\c =(正则表达式层面)> \c
result = re.match(r"\\c",text) # \\c =(正则表达式层面)> \c
print(result.group())

分组

text = "apple price is $99,orange price is $88"
result = re.search('.+(\$\d+).+(\$\d+)',text)
print(result.groups())

group()/group(0):匹配整个分组
group(1):匹配第一个分组
group(2):匹配第二个分组
groups():获取所有的分组

re中常用的函数

# findall:查找所有满足条件的
text = "apple price is $99,orange price is $88"
result = re.findall(r'\$\d+',text)
print(result)

# sub:根据规则替换其他字符串
text = "nihao zhongguo,hello world"
new_text = text.replace(" ","\n")
new_text = re.sub(r' |,','\n',text)
print(new_text)
html = """
<div class="job-detail">
    <p>1. 3年以上相关开发经验 ,全日制统招本科以上学历</p>
    <p>2. 精通一门或多门开发语言(Python,C,Java等),其中至少有一门有3年以上使用经验</p>
    <p>3. 熟练使用ES/mysql/mongodb/redis等数据库;</p>
    <p>4. 熟练使用django、tornado等web框架,具备独立开发 Python/Java 后端开发经验;</p>
    <p>5. 熟悉 Linux / Unix 操作系统&nbsp;</p>
    <p>6. 熟悉 TCP/IP,http等网络协议</p>
    <p>福利:</p>
    <p>1、入职购买六险一金(一档医疗+公司全额购买商业险)+开门红+全额年终奖(1年13薪,一般会比一个月高)</p>
    <p>2、入职满一年有2次调薪调级机会</p>
    <p>3、项目稳定、团队稳定性高,团队氛围非常好(汇合员工占招行总员工比例接近50%);</p>
    <p>4、有机会转为招商银行内部员工;</p>
    <p>5、团队每月有自己的活动经费,法定节假日放假安排;</p>
    <p>6、办公环境优良,加班有加班费(全额工资为计算基数,加班不超过晚上10点,平日加班为时薪1.5倍,周末加班为日薪2倍,周末加班也可优先选择调休,管理人性化)。</p>
</div>
"""
new_html = re.sub(r'<.+?>',"",html)
print(new_html)


# split:根据规则分割字符串
text = "nihao zhongguo,hello world"
result = re.split(r' |,',text)
print(result)


# compile:编译正则表达式
text = "apple price is 34.56"
r = re.compile(r"""
\d+ # 整数部分
\.? # 小数点
\d* # 小数部分
""",re.VERBOSE)

result = re.search(r,text)
result = re.search(r"""
\d+ # 整数部分
\.? # 小数点
\d* # 小数部分
""",text,re.VERBOSE)
print(result.group())
# 如果想要在正则表达式中加注释,那么需要在正则表达式的函数最后加一个`re.VERBOSE`。

第四章:数据存储

JSON字符串处理:

什么是JSON字符串:

JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。更多解释请见:https://baike.baidu/item/JSON/2462549?fr=aladdin

JSON支持数据格式:
对象(字典):使用花括号。
数组(列表):使用方括号。
字符串类型:字符串必须要用双引号,不能用单引号)。
整形、浮点型、布尔类型还有null类型。

多个数据之间使用逗号分开。
注意:json本质上就是一个字符串。

将Python对象dump成JSON字符串:

  1. dumps:把Python对象转换成JSON格式的字符串。
  2. dump:把Python对象转换成JSON格式的字符串,并且还可以接收一个文件指针fp参数,可以写入到文件中。

这两个方法都有一个ensure_ascii参数,默认情况下这个参数的值是True,也就是说转换后的JSON字符串是只能存储ascii格式的,不能存储中文,如果想要存储成中文,那么可以将他设置为False。

将JSON字符串load成Python对象:

  1. loads:将JSON字符串转换成Python对象。
  2. load:将JSON字符串转换成Python对象,并且是直接从文件中获取JSON字符串。

CSV文件处理:

CSV文件读取的两种方式:

import csv

# 这种方式读取到的每一条数据是一个列表,所以需要通过下标的方式获取具体某一个值
with open("stock.csv",'r',encoding='gbk') as fp:
    reader = csv.reader(fp)
    for x in reader:
        print(x[3])

# 这种方式读取到的每一条数据是一个字典,所以可以通过列名获取数据
with open("stock.csv",'r',encoding='gbk') as fp:
    reader = csv.DictReader(fp)
    for x in reader:
        print(x['secShortName'])

CSV文件的写入的两种方式:

import csv

headers = ('name','age','height')
students = [
    ("张三",18,180),
    ("李四",19,190),
    ("王五",20,170)
     ]
students = [
    {"name":"张三","age":18,"height":180},
    {"name":"李四","age":19,"height":190},
    {"name":"王五","age":20,"height":170}
]

with open("students.csv",'w',encoding='utf-8',newline='') as fp:
    writer = csv.writer(fp)
    writer.writerow(headers)
    writer.writerows(students)


with open("students.csv",'w',encoding='utf-8',newline='') as fp:
    writer = csv.DictWriter(fp,headers)
    # 虽然DictWriter创建的时候有一个headers,但是想要写入数据进去,还是需要调用
    # writer.writeheader()方法,否则,表头数据写入不进去
    writer.writeheader()
    writer.writerows(students)

Excel文件处理:

Sheet相关的操作:

workbook = xlrd.open_workbook("成绩表.xlsx")

# 获取所有的sheet名字
print(workbook.sheet_names())

# 根据索引获取指定的sheet对象
sheet = workbook.sheet_by_index(1)
print(sheet.name)

# 根据名称获取指定的sheet对象
sheet = workbook.sheet_by_name("2班")
print(sheet.name)

# 获取所有的sheet对象
sheets = workbook.sheets()
for sheet in sheets:
    print(sheet.name)

# 获取指定sheet的行数和列数
sheet = workbook.sheet_by_index(0)
print({"rows":sheet.nrows,"cols":sheet.ncols})

Cell相关的操作:

from xlrd.sheet import Cell
sheet = workbook.sheet_by_index(0)
cell = sheet.cell(1,1)
print(cell.value)

cells = sheet.row_slice(1,1,4)
for cell in cells:
    print(cell.value)

cells = sheet.col_slice(0,1,sheet.nrows)
for cell in cells:
    print(cell.value)

cell_value = sheet.cell_value(0,1)
print(cell_value)

cell_values = sheet.col_values(1,1,sheet.nrows)
print(cell_values)

cell_values = sheet.row_values(1,1,sheet.ncols)
print(cell_values)

Cell中常用的数据类型:

sheet = workbook.sheet_by_index(0)
cell = sheet.cell(0,0)
print(cell.ctype)
print(xlrd.XL_CELL_TEXT)

cell = sheet.cell(1,1)
print(cell.ctype)
print(xlrd.XL_CELL_NUMBER)

cell = sheet.cell(19,0)
print(cell.ctype)
print(xlrd.XL_CELL_DATE)

cell = sheet.cell(19,0)
print(cell.ctype)
print(xlrd.XL_CELL_BOOLEAN)

cell = sheet.cell(1,1)
print(cell.ctype)
print(xlrd.XL_CELL_EMPTY)

写入Excel文件:

  1. 导入xlwt模块。
  2. 创建一个Workbook对象。
  3. 创建一个Sheet对象。
  4. 使用sheet.write方法把数据写入到Sheet下指定行和列中。如果想要在原来workbook对象上添加新的cell,那么需要调用put_cell来添加。
  5. 保存成Excel文件。

编辑Excel文件:

  1. 先读取原来的Excel文件。
  2. 然后在读取的sheet上面进行cell的修改,可以使用sheet.put_cell(row,col,ctype,value,None)方法实现。
  3. 再重新创建一个新的excel文件,然后把之前读取到的数据写入到新的excel文件中。

MySQL数据库操作

MySQL数据库安装:

  1. 下载地址:https://dev.mysql/downloads/windows/installer/5.7.0.html
  2. 如果提示没有.NET Framework框架。那么就在提示框中找到下载链接,下载一个就可以了。
  3. 如果提示没有Microsoft Virtual C++ x64(x86),那么百度或者谷歌这个软件安装即可。

Python连接MySQL数据库:

  1. pip install pymysql

  2. 连接代码:

    db = pymysql.connect(host="127.0.0.1",port=3306,user="root",password="root",database="csdn_crawler",charset='utf8')
    
    • host:以后在连接外网服务器的时候,就要改成外网服务器的ip地址。
    • port:在外网一般会更换端口号,不会为3306,这是为了安全考虑。
    • user:连接的用户,一般在生产环境中会单独分配一个账号给你,而不是使用root用户。
    • password:这个用户的密码。
    • database:要连接操作的数据库名。
    • charset:设置为utf8这样就能操作中文了。

插入数据:

title = '444'
content = '555'
sql = "insert into article(id,title,content) values(null,%s,%s)"
cursor.execute(sql,(title,content))

语法是:

insert into [表名(字段)] values(字段对应的值)

如果值是动态变化的,那么可以使用%s来先作为坑,后期在使用execute方法的时候,可以给一个元组把这些数据填进去。

查找数据:

sql = "select id,title from article where id>3"
cursor.execute(sql)

执行完sql语句后,可以使用以下三个方法来提取数据:

  1. fetcheone:提取第一条数据。
  2. fetchall:提取select语句获取到的所有数据。
  3. fetchmany:提取指定条数的数据。

删除数据:

语法:
delete from [表名] [条件]
示例:

sql = "delete from article where id>3"
cursor.execute(sql)

更新数据:

语法:
update [表名] [更新操作] [条件]
示例:

sql = "update article set title='钢铁是怎样练成的' where id=3"
cursor.execute(sql)

第五章:爬虫进阶

多线程:

什么是多线程:

  1. 理解:默认情况下,一个程序只有一个进程和一个线程,代码是依次线性执行的。而多线程则可以并发执行,一次性多个人做多件事,自然比单线程更快。
  2. 官方:https://baike.baidu/item/多线程/1190404?fr=aladdin

如何创建一个基本的多线程:

使用threading模块下的Thread类即可创建一个线程。这个类有一个target参数,需要指定一个函数,那么以后这个线程执行的时候,就会执行这个函数的代码。示例代码如下:

import time
import threading

def coding():
    for x in range(3):
        print("%d正在写代码..."%x)
        time.sleep(1)

def drawing():
    for x in range(3):
        print("%d正在画图..." % x)
        time.sleep(1)

def multi_thread():
    th1 = threading.Thread(target=coding)
    th2 = threading.Thread(target=drawing)

    th1.start()
    th2.start()

if __name__ == '__main__':
    # single_thread()
    multi_thread()

查看当前线程:

  1. threading.current_thread:在线程中执行这个函数,会返回当前线程的对象。
  2. threading.enumerate:获取整个程序中所有的线程。

继承自threading.Thread类:

  1. 我们自己写的类必须继承自threading.Thread类。
  2. 线程代码需要放在run方法中执行。
  3. 以后创建线程的时候,直接使用我们自己创建的类来创建线程就可以了。
  4. 为什么要使用类的方式创建线程呢?原因是因为类可以更加方便的管理我们的代码,可以让我们使用面向对象的方式进行编程。

全局变量共享的问题:

在多线程中,如果需要修改全局变量,那么需要在修改全局变量的地方使用锁锁起来,执行完成后再把锁释放掉。
使用锁的原则:

  1. 把尽量少的和不耗时的代码放到锁中执行。
  2. 代码执行完成后要记得释放锁。
    在Python中,可以使用threading.Lock来创建锁,lock.acquire()是上锁操作,lock.release()是释放锁的操作。

生产者和消费者模式:

生产者和消费者模式是多线程开发中经常见到的一种模式。生产者的线程专门用来生产一些数据,然后存放到一个中间的变量中。消费者再从这个中间的变量中取出数据进行消费。通过生产者和消费者模式,可以让代码达到高内聚低耦合的目标,程序分工更加明确,线程更加方便管理。

Lock版本的生产者和消费者模式:

import threading
import random

gMoney = 0
gLock = threading.Lock()
gTimes = 0


class Producer(threading.Thread):
    def run(self) -> None:
        global gMoney
        global gTimes
        while True:
            gLock.acquire()
            if gTimes >= 10:
                gLock.release()
                break
            money = random.randint(0, 100)
            gMoney += money
            gTimes += 1
            print("%s生产了%d元钱"%(threading.current_thread().name,money))
            gLock.release()


class Consumer(threading.Thread):
    def run(self) -> None:
        global gMoney
        while True:
            gLock.acquire()
            money = random.randint(0,100)
            if gMoney >= money:
                gMoney -= money
                print("%s消费了%d元钱"%(threading.current_thread().name,money))
            else:
                if gTimes >= 10:
                    gLock.release()
                    break
                print("%s想消费%d元钱,但是余额只有%d"%(threading.current_thread().name,money,gMoney))
            gLock.release()

def main():
    for x in range(5):
        th = Producer(name="生产者%d号"%x)
        th.start()

    for x in range(5):
        th = Consumer(name="消费者%d号"%x)
        th.start()

if __name__ == '__main__':
    main()

Condition版本的生产者和消费者模式:

Lock版本的生产者与消费者模式可以正常的运行。但是存在一个不足,在消费者中,总是通过while True死循环并且上锁的方式去判断钱够不够。上锁是一个很耗费CPU资源的行为。因此这种方式不是最好的。还有一种更好的方式便是使用threading.Condition来实现。threading.Condition可以在没有数据的时候处于阻塞等待状态。一旦有合适的数据了,还可以使用notify相关的函数来通知其他处于等待状态的线程。这样就可以不用做一些无用的上锁和解锁的操作。可以提高程序的性能。首先对threading.Condition相关的函数做个介绍,threading.Condition类似threading.Lock,可以在修改全局数据的时候进行上锁,也可以在修改完毕后进行解锁。以下将一些常用的函数做个简单的介绍:

  1. acquire:上锁。
  2. release:解锁。
  3. wait:将当前线程处于等待状态,并且会释放锁。可以被其他线程使用notify和notify_all函数唤醒。被唤醒后会继续等待上锁,上锁后继续执行下面的代码。
  4. notify:通知某个正在等待的线程,默认是第1个等待的线程。
  5. notify_all:通知所有正在等待的线程。notify和notify_all不会释放锁。并且需要在release之前调用。

代码如下:

import threading
import random
import time

gMoney = 0
gCondition = threading.Condition()
gTimes = 0


class Producer(threading.Thread):
    def run(self) -> None:
        global gMoney
        global gTimes
        while True:
            gCondition.acquire()
            if gTimes >= 10:
                gCondition.release()
                break
            money = random.randint(0, 100)
            gMoney += money
            gTimes += 1
            print("%s生产了%d元钱,剩余%d元钱"%(threading.current_thread().name,money,gMoney))
            gCondition.notify_all()
            gCondition.release()
            time.sleep(1)


class Consumer(threading.Thread):
    def run(self) -> None:
        global gMoney
        while True:
            gCondition.acquire()
            money = random.randint(0,100)
            while gMoney < money:
                if gTimes >= 10:
                    print("%s想消费%d元钱,但是余额只有%d元钱了,并且生产者已经不再生产了!"%(threading.current_thread().name,money,gMoney))
                    gCondition.release()
                    return
                print("%s想消费%d元钱,但是余额只有%d元钱了,消费失败!"%(threading.current_thread().name,money,gMoney))
                gCondition.wait()
            gMoney -= money
            print("%s消费了%d元钱,剩余%d元钱"%(threading.current_thread().name,money,gMoney))
            gCondition.release()
            time.sleep(1)


def main():
    for x in range(5):
        th = Producer(name="生产者%d号"%x)
        th.start()

    for x in range(5):
        th = Consumer(name="消费者%d号"%x)
        th.start()

if __name__ == '__main__':
    main()

线程安全的队列Queue:

在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫做queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。相关的函数如下:
初始化Queue(maxsize):创建一个先进先出的队列。

  1. qsize():返回队列的大小。
  2. empty():判断队列是否为空。
  3. full():判断队列是否满了。
  4. get():从队列中取最后一个数据。默认情况下是阻塞的,也就是说如果队列已经空了,那么再调用就会一直阻塞,直到有新的数据添加进来。也可以使用block=False,来关掉阻塞。如果关掉了阻塞,在队列为空的情况获取就会抛出异常。
  5. put():将一个数据放到队列中。跟get一样,在队列满了的时候也会一直阻塞,并且也可以通过block=False来关掉阻塞,同样也会抛出异常。

GIL:

什么是GIL:

Python自带的解释器是CPython。CPython解释器的多线程实际上是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核)。同一时刻只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPython解释器中有一个东西叫做GIL(Global Intepreter Lock),叫做全局解释器锁。这个解释器锁是有必要的。因为CPython解释器的内存管理不是线程安全的。当然除了CPython解释器,还有其他的解释器,有些解释器是没有GIL锁的,见下面:

  1. Jython:用Java实现的Python解释器。不存在GIL锁。更多详情请见:https://zh.wikipedia/wiki/Jython
  2. IronPython:用实现的Python解释器。不存在GIL锁。更多详情请见:https://zh.wikipedia/wiki/IronPython
  3. PyPy:用Python实现的Python解释器。存在GIL锁。更多详情请见:https://zh.wikipedia/wiki/PyPy
    GIL虽然是一个假的多线程。但是在处理一些IO操作(比如文件读写和网络请求)还是可以在很大程度上提高效率的。在IO操作上建议使用多线程提高效率。在一些CPU计算操作上不建议使用多线程,而建议使用多进程。

有了GIL,为什么还需要Lock:

GIL只是保证全局同一时刻只有一个线程在执行,但是他并不能保证执行代码的原子性。也就是说一个操作可能会被分成几个部分完成,这样就会导致数据有问题。所以需要使用Lock来保证操作的原子性。

动态网页爬虫

什么是动态网页爬虫和AJAX技术:

  1. 动态网页,是网站在不重新加载的情况下,通过ajax技术动态更新网站中的局部数据。比如拉勾网的职位页面,在换页的过程中,url是没有发生改变的,但是职位数据动态的更改了。
  2. AJAX(Asynchronouse JavaScript And XML)异步JavaScript和XML。前端与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用Ajax)如果需要更新内容,必须重载整个网页页面。因为传统的在传输数据格式方面,使用的是XML语法。因此叫做AJAX,其实现在数据交互基本上都是使用JSON。使用AJAX加载的数据,即使使用了JS,将数据渲染到了浏览器中,在右键->查看网页源代码还是不能看到通过ajax加载的数据,只能看到使用这个url加载的html代码。

动态网页爬虫的解决方案:

  1. 直接分析ajax调用的接口。然后通过代码请求这个接口。
  2. 使用Selenium+chromedriver模拟浏览器行为获取数据。

selenium和chromedriver:

使用selenium关闭浏览器:

  1. driver.close():关闭当前的页面。
  2. driver.quit():关闭整个浏览器。

selenium定位元素:

1**. find_element_by_id:根据id来查找某个元素。
2. find_element_by_class_name:根据类名查找元素。
3. find_element_by_name:根据name属性的值来查找元素。
4. find_element_by_tag_name:根据标签名来查找元素。
5. find_element_by_xpath:根据xpath语法来获取元素。
6. find_element_by_css_selector:根据css选择器选择元素。**

要注意,find_element是获取第一个满足条件的元素。find_elements是获取所有满足条件的元素。

selenium表单操作:

  1. webelement.send_keys:给输入框填充内容。
  2. webelement.click:点击。
  3. 操作select标签:需要首先用from selenium.webdriver.support.ui import Select来包装一下选中的对象,才能进行select选择:
    • select_by_index:按索引进行选择。
    • select_by_value:按值进行选择。
    • select_by_visible_text:按照可见文本进行选择。

selenium行为链:

有时候在页面中的操作可能要有很多步,那么这时候可以使用鼠标行为链类selenium.webdrivermon.action_chains.ActionChains来完成。比如现在要将鼠标移动到某个元素上并执行点击事件。那么示例代码如下:

inputTag = driver.find_element_by_id('kw')
submitTag = driver.find_element_by_id('su')
actions = ActionChains(driver)
actions.move_to_element(inputTag)
actions.send_keys_to_element(inputTag,'python')
actions.move_to_element(submitTag)
actions.click(submitTag)
actions.perform()

还有更多的鼠标相关的操作。
click_and_hold(element):点击但不松开鼠标。
context_click(element):右键点击。
double_click(element):双击。

更多方法请参考:http://selenium-python.readthedocs.io/api.html

为什么需要行为链条?
因为有些网站可能会在浏览器端做一些验证行为是否符合人类的行为来做反爬虫。这时候我们就可以使用行为链来模拟人的操作。行为链有更多的复杂操作,比如双击,右键等,在自动化测试中非常有用。

操作cookie:

  1. 获取所有的cookie:

    for cookie in driver.get_cookies():
        print(cookie)
    
  2. 根据cookie的key获取value:

    value = driver.get_cookie(key)
    
  3. 删除所有的cookie:

    driver.delete_all_cookies()
    
  4. 删除某个cookie:

    driver.delete_cookie(key)
    
  5. 添加cookie:

    driver.add_cookie({“name”:”username”,”value”:”abc”})
    

隐式等待和显式等待:

  1. 隐式等待:指定一个时间,在这个时间内一直会处于等待状态。隐式等待需要使用driver.implicitly_wait

  2. 显式等待:指定在某个时间内,如果某个条件满足了,那么就不会再等待,如果在指定的时间内条件都不满足,那么就不会再等待了。显式等待用的方法是from selenium.webdriver.support.ui import WebDriverWait。示例代码如下:

    driver.get("https://kyfw.12306/otn/leftTicket/init?linktypeid=dc")
    WebDriverWait(driver,100).until(
        EC.text_to_be_present_in_element_value((By.ID,"fromStationText"),"长沙")
    )
    WebDriverWait(driver,100).until(
        EC.text_to_be_present_in_element_value((By.ID,"toStationText"),"北京")
    )
    btn = driver.find_element_by_id("query_ticket")
    btn.click()
    

打开新窗口和切换页面:

  1. selenium中没有专门的打开新窗口的方法,是通过window.execute_script()来执行js脚本的形式来打开新窗口的。

    window.execute_script("window.open('https://www.douban/')")
    
  2. 打开新的窗口后driver当前的页面依然还是之前的,如果想要获取新的窗口的源代码,那么就必须先切换过去。示例代码如下:

    window.switch_to.window(driver.window_handlers[1])
    

设置代理:

设置代理通过ChromeOptions来设置,示例代码如下:

options = webdriver.ChromeOptions()
options.add_argument("--proxy-server=http://110.52.235.176:9999")
driver = webdriver.Chrome(executable_path="D:\ProgramApp\chromedriver\chromedriver73.exe",chrome_options=options)

driver.get("http://httpbin/ip")

补充:

  1. get_property:获取html标签中官方写好的属性。
  2. get_attribute:获取html标签中官方和非官方的属性。
  3. driver.save_screenshoot:获取当前页面的截图,有时候请求失败了,那么可以把当前网页的截图保存下来,方便后期进行分析。

第六章:Scrapy框架

Scrapy框架:

写一个爬虫,需要做很多的事情。比如:发送网络请求、数据解析、数据存储、反反爬虫机制(更换ip代理、设置请求头等)、异步请求等。这些工作如果每次都要自己从零开始写的话,比较浪费时间。因此Scrapy把一些基础的东西封装好了,在他上面写爬虫可以变的更加的高效(爬取效率和开发效率)。因此真正在公司里,一些上了量的爬虫,都是使用Scrapy框架来解决。

安装Scrapy框架:

  1. pip install scrapy。
  2. 可能会出现问题:
    • 在ubuntu下要先使用以下命令安装依赖包:sudo apt-get install python3-dev build-essential python3-pip libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev,安装完成后再安装scrapy
    • 在windows下安装可能会提示No module named win32api,这时候先使用命令:pip install pypiwin32,安装完成后再安装scrapy
    • 在windows下安装Scrapy可能会提示twisted安装失败,那么可以到这个页面下载twisted文件:https://www.lfd.uci.edu/~gohlke/pythonlibs/,下载的时候要根据自己的Python版本来选择不同的文件。下载完成后,通过pip install xxx.whl

Scrapy框架架构:

  1. Scrapy Engine(引擎):Scrapy框架的核心部分。负责在Spider和ItemPipeline、Downloader、Scheduler中间通信、传递数据等。
  2. Spider(爬虫):发送需要爬取的链接给引擎,最后引擎把其他模块请求回来的数据再发送给爬虫,爬虫就去解析想要的数据。这个部分是我们开发者自己写的,因为要爬取哪些链接,页面中的哪些数据是需要的,都是由程序员自己决定。
  3. Scheduler(调度器):负责接收引擎发送过来的请求,并按照一定的方式进行排列和整理,负责调度请求的顺序等。
  4. Downloader(下载器):负责接收引擎传过来的下载请求,然后去网络上下载对应的数据再交还给引擎。
  5. Item Pipeline(管道):负责将Spider(爬虫)传递过来的数据进行保存。具体保存在哪里,应该看开发者自己的需求。
  6. Downloader Middlewares(下载中间件):可以扩展下载器和引擎之间通信功能的中间件。
  7. Spider Middlewares(Spider中间件):可以扩展引擎和爬虫之间通信功能的中间件。

创建Scrapy项目:

  1. 创建项目:scrapy startproject [项目名称].
  2. 创建爬虫:cd到项目中->scrapy genspider [爬虫名称] [域名].

项目文件作用:

  1. settings.py:用来配置爬虫的。
  2. middlewares.py:用来定义中间件。
  3. items.py:用来提前定义好需要下载的数据字段。
  4. pipelines.py:用来保存数据。
  5. scrapy.cfg:用来配置项目的。

CrawlSpider爬虫:

  1. 作用:可以定义规则,让Scrapy自动的去爬取我们想要的链接。而不必跟Spider类一样,手动的yield Request。
  2. 创建:scrapy genspider -t crawl [爬虫名] [域名]
  3. 提取的两个类:
    • LinkExtrator:用来定义需要爬取的url规则。
    • Rule:用来定义这个url爬取后的处理方式,比如是否需要跟进,是否需要执行回调函数等。

Scrapy Shell:

在命令行中,进入到项目所在的路径。然后:
scrapy shell 链接
在这个里面,可以先去写提取的规则,没有问题后,就可以把代码拷贝到项目中。方便写代码。

使用twisted异步保存mysql数据:

  1. 使用twisted.enterprise.adbapi来创建一个连接对象:

    def __init__(self,mysql_config):
        self.dbpool = adbapi.ConnectionPool(
            mysql_config['DRIVER'],
            host=mysql_config['HOST'],
            port=mysql_config['PORT'],
            user=mysql_config['USER'],
            password=mysql_config['PASSWORD'],
            db=mysql_config['DATABASE'],
            charset='utf8'
        )
    
    @classmethod
    def from_crawler(cls,crawler):
        # 只要重写了from_crawler方法,那么以后创建对象的时候,就会调用这个方法来获取pipline对象
        mysql_config = crawler.settings['MYSQL_CONFIG']
        return cls(mysql_config)
    
  2. 在插入数据的函数中,使用runInteraction来运行真正执行sql语句的函数。示例代码如下:

    def process_item(self, item, spider):
        # runInteraction中除了传运行sql的函数,还可以传递参数给回调函数使用
        result = self.dbpool.runInteraction(self.insert_item,item)
        # 如果出现了错误,会执行self.insert_error函数
        result.addErrback(self.insert_error)
        return item
    
    def insert_item(self,cursor,item):
        sql = "insert into article(id,title,author,pub_time,content,origin) values(null,%s,%s,%s,%s,%s)"
        args = (item['title'],item['author'],item['pub_time'],item['content'],item['origin'])
        cursor.execute(sql,args)
    
    def insert_error(self,failure):
        print("="*30)
        print(failure)
        print("="*30)
    

Scrapy下载图片:

  1. 解析图片的链接。

  2. 定义一个item,上面有两个字段,一个是image_urls,一个是images。其中image_urls是用来存储图片的链接,由开发者把数据爬取下来后添加的。

  3. 使用scrapy.pipelines.images.ImagesPipeline来作为数据保存的pipeline。

  4. 在settings.py中设置IMAGES_SOTRE来定义图片下载的路径。

  5. 如果想要有更复杂的图片保存的路径需求,可以重写ImagePipeline的file_path方法,这个方法用来返回每个图片的保存路径。

  6. file_path方法没有item对象,所以我们还需要重写get_media_requests方法,来把item绑定到request上。示例代码如下:

    class ImagedownloadPipeline(ImagesPipeline):
    def get_media_requests(self, item, info):
        media_requests = super(ImagedownloadPipeline, self).get_media_requests(item,info)
        for media_request in media_requests:
            media_request.item = item
        return media_requests
    
     def file_path(self, request, response=None, info=None):
         origin_path = super(ImagedownloadPipeline, self).file_path(request,response,info)
         title = request.item['title']
         title = re.sub(r'[\\/:\*\?"<>\|]',"",title)
         save_path = os.path.join(settings.IMAGES_STORE,title)
         if not os.path.exists(save_path):
             os.mkdir(save_path)
         image_name = origin_path.replace("full/","")
         return os.path.join(save_path,image_name)
    
  7. 在创建文件夹的时候,要注意一些特殊字符是不允许作为文件夹的名字而存在的,那么我们就可以通过正则表达式来删掉。r'[\\/:\*\?"<>\|]'

下载器中间件:

下载器中间件是引擎和下载器之间通信的中间件。在这个中间件中我们可以设置代理、更换请求头等来达到反反爬虫的目的。要写下载器中间件,可以在下载器中实现两个方法。一个是process_request(self,request,spider),这个方法是在请求发送之前会执行,还有一个是process_response(self,request,response,spider),这个方法是数据下载到引擎之前执行。

  1. process_request(self,request,spider)方法:
    这个方法是下载器在发送请求之前会执行的。一般可以在这个里面设置随机代理ip等。

    1. 参数:
      • request:发送请求的request对象。
      • spider:发送请求的spider对象。
    2. 返回值:
      • 返回None:如果返回None,Scrapy将继续处理该request,执行其他中间件中的相应方法,直到合适的下载器处理函数被调用。
      • 返回Response对象:Scrapy将不会调用任何其他的process_request方法,将直接返回这个response对象。已经激活的中间件的process_response()方法则会在每个response返回时被调用。
      • 返回Request对象:不再使用之前的request对象去下载数据,而是根据现在返回的request对象返回数据。
      • 如果这个方法中抛出了异常,则会调用process_exception方法。
  2. process_response(self,request,response,spider)方法:
    这个是下载器下载的数据到引擎中间会执行的方法。

    1. 参数:
      • request:request对象。
      • response:被处理的response对象。
      • spider:spider对象。
    2. 返回值:
      • 返回Response对象:会将这个新的response对象传给其他中间件,最终传给爬虫。
      • 返回Request对象:下载器链被切断,返回的request会重新被下载器调度下载。
      • 如果抛出一个异常,那么调用request的errback方法,如果没有指定这个方法,那么会抛出一个异常。

Scrapy中设置代理:

  1. 设置普通代理:

    class IPProxyDownloadMiddleware(object):
        PROXIES = [
         "5.196.189.50:8080",
        ]
        def process_request(self,request,spider):
            proxy = random.choice(self.PROXIES)
            print('被选中的代理:%s' % proxy)
            request.meta['proxy'] = "http://" + proxy
    
  2. 设置独享代理:

    class IPProxyDownloadMiddleware(object):
        def process_request(self,request,spider):
            proxy = '121.199.6.124:16816'
            user_password = "970138074:rcdj35xx"
            request.meta['proxy'] = proxy
            # bytes
            b64_user_password = base64.b64encode(user_password.encode('utf-8'))
            request.headers['Proxy-Authorization'] = 'Basic ' + b64_user_password.decode('utf-8')
    
  3. 代理服务商:

    • 芝麻代理:http://http.zhimaruanjian/
    • 太阳代理:http://http.taiyangruanjian/
    • 快代理:http://www.kuaidaili/
    • 讯代理:http://www.xdaili/
    • 蚂蚁代理:http://www.mayidaili/
    • 极光代理:http://www.jiguangdaili/

分布式爬虫:

redis配置:

  1. 在ubuntu上安装redis:sudo apt install redis-server

  2. 连接reids服务器:redis-cli -h [ip地址] -p [端口号]

  3. 在其他电脑上连接本机的redis服务器:在/etc/redis/redis.conf中,修改bind,把redis服务器的ip地址加进去。示例如下:

    bind 192.168.175.129 127.0.0.1
    
  4. vim:有可能没有。那么通过sudo apt install vim就可以安装了。

  5. 虚拟机安装:vmware+ubuntu16.04.iso来安装。安装的时候,设置root用户的密码,用useradd命令来创建一个普通用户。后期方便通过xshell来连接。ubuntu不允许外面直接用root用户链接,那么我们可以先用普通用户连接,然后再切换到root用户。

  6. 修改代码:

    
    

爬虫部署:

  1. 在服务器上安装scrapyd:pip3 install scrapyd

  2. /usr/local/lib/python3.5/dist-packages/scrapyd下拷贝出default_scrapyd.conf放到/etc/scrapyd/scrapyd.conf

  3. 修改/etc/scrapyd/scrapyd.conf中的bind_address为自己的IP地址。

  4. 重新安装twisted

    pip uninstall twisted
    pip install twisted==18.9.0
    

    如果这一步不做,后期会出现intxxx的错误。

  5. 在开发机上(自己的window电脑上)安装pip install scrapyd-client

  6. 修改python/Script/scrapyd-deployscrapyd-deploy.py

  7. 在项目中,找到scrapy.cfg,然后配置如下:

    [settings]
    default = lianjia.settings
    
    [deploy]
    # 下面这个url要取消注释
    url = http://服务器的IP地址:6800/
    project = lianjia
    
  8. 在项目所在的路径执行命令生成版本号并上传爬虫代码:scrapyd-deploy。如果一次性想要把代码上传到多个服务器,那么可以修改scrapy.cfg为如下:

    [settings]
    default = lianjia.settings
    
    [deploy:服务器1]
    # 下面这个url要取消注释
    url = http://服务器1的IP地址:6800/
    project = lianjia
    
    [deploy:服务器2]
    # 下面这个url要取消注释
    url = http://服务器2的IP地址:6800/
    project = lianjia
    

    然后使用scrapyd-deploy -a就可以全部上传了。

  9. curl for windows下载地址:https://curl.haxx.se/windows/,解压后双击打开bin/curl.exe即可在cmd中使用了。

  10. 在cmd中使用命令运行爬虫:

    curl http://服务器IP地址:6800/schedule.json -d project=lianjia -d spider=house
    
  11. 如果后期修改了爬虫的代码,那么需要重新部署,然后服务器的scrapyd服务重新启动一下。

  12. 更多的API介绍:https://scrapyd.readthedocs.io/en/stable/api.html

更多推荐

Python网络爬虫识记