Python67-爬虫

爬虫的基础知识

爬虫的定义

只要是浏览器可以做的事情,原则上,爬虫都可以帮助我们做,即:浏览器不能够做到的,爬虫也不能做

网络爬虫:又叫网络蜘蛛(spider),网络机器人,就是模拟客户端发送网络请求,接受请求响应,一种按照一定的规则,自动地抓取互联网信息的程序

爬虫的分类

  • 通用爬虫

    通常指搜索引擎的爬虫(面对整个互联网)

  • 聚焦爬虫:

    针对特定网站的爬虫

  • 流程:


    爬虫的工作流程

ROBOTS协议

网站通过robots协议告诉搜索引擎那些页面可以抓取,那些页面不能抓取

例如:https://www.taobao.com/robots.txt(通常是网站后面加/robots.txt即可以看到,就是一个文本文件)

部分内容如下:

User-agent:  Baiduspider    #用户代理,可以理解为浏览器的身份标识,通过这个字段可以告诉服务器是什么样的程序在请求网站,Baiduspider即百度的搜索引擎
Allow:  /article    #表示允许爬的内容
Allow:  /oshtml
Allow:  /ershou
Allow: /$
Disallow:  /product/    #表示不允许该用户代理爬的内容
Disallow:  /

但是robots只是道德层面的约束

http和https

为了拿到和浏览器一模一样的数据,就必须要知道http和https


在这里插入图片描述
  • http:

    超文本传输协议,明文方式传输,默认端口80

  • https:

    http+ssl(安全套接字层),会对数据进行加密,默认端口443

https更安全,但是性能更低(耗时更长)

浏览器发送http请求的过程

在这里插入图片描述

ps:爬虫在爬取数据的时候,不会主动的请求css、图片、js等资源,就算自己爬取了js的内容,也只是字符串,而不会执行,故,浏览器渲染出来的内容和爬虫请求的页面并不一样

爬虫要根据当前url地址对应的响应为准,当前url地址的elements的内容和url的响应不一样,特别要注意tbody,经常在elements中有而响应中无

  • 页面上的数据在哪里?

    • 当前url地址对应的相应中
    • 其他url地址对应的相应中,如ajax请求
    • js生成:1、部分数据在响应中,2、全部由js生成

url的格式

host:服务器的ip地址或者是域名
port:服务器的端口
path:访问资源的路径
query-string:参数,发送给http服务器的数据
anchor:锚点(跳转到网页的制定锚点位置,anchor也有主播的意思)

例:http://item.jd.com/11936238.html#product-detail,就是一个带锚点的url,会自动跳转到商品详情,但是要注意,一个页面带锚点和不带锚点的响应是一样的(写爬虫的时候,就可以直接删掉锚点的部分)

http请求格式

在这里插入图片描述

如,在访问百度时,查看request headers的source时,就可以看到如下内容

GET http://www.baidu.com/ HTTP/1.1  
#请求方法:get;url:http:xxxx.com/;协议版本:http1.1,然后换行
Host: www.baidu.com
#请求头部:host;值:www.baidu.com;换行,以下类似
Proxy-Connection: keep-alive    #keep-alive表示支持长链接。为什么要用长连接:不用频繁握手挥手,提高效率
Upgrade-Insecure-Requests: 1    #升级不安全的请求:把http请求转换为https的请求
DNT: 1  #Do not track
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)    Chrome/76.0.3809.100 Safari/537.36  #浏览器的标识。名字/版本号。如果有模拟手机版的请求,改user agent即可,不同的user agent访问相同的url,可能会得到不同的内容
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3  #浏览器告诉服务器自己可以接受什么样的数据
Referer: http://baidu.com/
Accept-Encoding: gzip, deflate  #告诉服务器自己可以接受什么样的压缩方式
Accept-Language: en-US,en;q=0.9 #告诉服务器自己可以接受什么样语言的数据,q:权重,更愿意接受哪种语言
Cookie: BAIDUID=B8BE58B25611B7BBA38ECFE9CE75841F:FG=1; BIDUPSID=B8BE58B25611B7BBA38ECFE9CE75841F; PSTM=1565080210; BD_UPN=12314753; delPer=0; BD_HOME=0;    H_PS_PSSID=26522_1453_21118_29523_29521_29098_29568_28830_29221_26350_22159; BD_CK_SAM=1; PSINO=7; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; COOKIE_SESSION=218_0_3_0_0_7_1_1_1_3_76_2_0_0_0_0_0_0_1565599571%7C3%230_0_1565599571%7C1; rsv_jmp_slow=1565599826516; H_PS_645EC=2c80At1Is237xdMOfC3ju2q0qlWJ%2FFlbD5N50IQeTrCHyIEsZN6yQYBgLHI; B64_BOT=1   #cookie:保存用户的个人信息。ps:cookie和session的区别:cookie保存在浏览器本地,不安全,存储量有上限,session保存在服务器,更安全,往往没有上限。cookie又分为request cookie和reponse cookie,在浏览器中可以查看

除了以上字段,可能还有referer字段,表示当前url是从哪个url过来的;x-request-with字段,表示是ajax异步请求

以上字段中,主要是user agent(模拟浏览器),cookie(反反爬虫)

常见的请求方式

  • get:除了post,基本都用get,更常用

  • post:常用于提交表单的时候(安全),传输大文件的时候(美观)

响应状态码(status code)

  • 200:成功
  • 302/307:临时转移至新的url
  • 404:not found
  • 500:服务器内部错误

字符串知识复习

  • str类型和bytes类型

    • bytes:二进制类型,互联网上数据都是以二进制的方式传输的
  • str:unicode的呈现形式

ps:ascii码是一个字节,unicode编码通常是2个字节,utf-8是unicode的实现方式之一,是一变长的编码方式,可以是1、2、3个字节

编码和解码的方式必须一致,否则会乱码

爬虫部分重要的是理解,而不是记忆


Request模块使用入门

Q:为什么要学习requests,而不是urllib?

  1. requests的底层实现是就urllib,urllib能做的事情,requests都可以做;
  2. requests在python2和python3中通用,方法完全一样;
  3. requests简单易用;
  4. request能够自动帮我们解压(gzip等)网页内容

中文文档api:http://docs.python-requests.org/zh_CN/latest/index.html

基础使用

"""基础入门"""
import requests

r = requests.get('http://www.baidu.com')    #r即为获得的响应。所请求的所有东西都在r中
# 必须要包含协议(http或https);还有dlelete,post等方法

print(r)

print(r.text)   #text是一个属性,其实可以通过他的意思判断,text是一个名字,所以是属性,如果是方法,常为动词
#会自动根据推测的编码方式解码为str

print(r.encoding)   #根据头部推测编码方式,如果猜错了,我们解码的时候就必须自己指定解码的方式

print(r.content)    #也是属性,是一个bytes类型的数据。
print(r.content.decode())   #将bytes类型转换为str类型。默认的解码方式为utf-8
print(r.status_code)    #获取状态码
assert r.status_code == 200   #断言状态码为200,如果断言失败,会报错:assertionerror
#可以用此方法判断请求是否成功

print(r.headers)    #响应头,我们主要关注其中的set-cookie(在本地设置cookie)字段
print(r.request)    #关于对应相应的请求部分,是一个对象
print(r.request.url)    #请求的url地址
print(r.url)    #响应地址,请求的url地址和响应的url地址可能会不一样(因为可能和重定向)
print(r.request.headers)    #请求头,如果不设置,默认的user-agent是python-requests/x.xx.x

with open('baidu_r.txt','w') as f:    #测试:查看默认的user-agent访问时返回的内容
    f.write(r.content.decode())

"""
requests中解编码的方法:
1. r.content.decode()   #content的类型为bytes,必须再次解码
2. r.content.decode('gbk')
3. r.text       #text是按照推测的编码方式进行解码后的数据,他的类型为str
"""

发送带header的请求

具体的header到浏览器中进行查看

"""
为什么请求需要带上header?
    模拟浏览器,欺骗服务器,获取和浏览器一致的内容

header的形式:字典,形式:{request headers冒号前面的值:request headers冒号后面的值},大部分情况,我们带上user-agent即可,少数情况需要cookie
用法:requests.get(url,headers=headers) 
"""

import requests

headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0'}

response = requests.get('https://www.baidu.com',headers=headers)

print(response.content.decode())    #会发现响应中的数据比不带header多许多

发送带参数的请求

#在url中带参数的形式
#例如:在我们百度搜索某东西时,就会带上一大堆参数,但是大部分可能是没有用的,我们可以尝试删除,然后我们在爬虫中带的参数只需要为其中不能删除的部分即可
"""
参数的形式:字典
kw={'wd':'长城'}
用法:requests.get(url,params=kw)
"""
import requests

headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0'}

params = {'wd':'这里是中文'}
#如果参数含中文,其就会被自动编码,编码的后的形式含百分号,我们可以使用url解码来查看原来的内容

r = requests.get('https://www.baidu.com',params=params,headers=headers)
print(r.status_code)
print(r.request.url)
print(r.url)
print(r.content.decode())

#当然,我们也可以直接把参数拼接到url中去,而不单独传参(也不需要手动编码),eg:r = requests.get('https://www.baidu.com/s?wd={}'.formate('传智播客'))

小练习:爬贴吧前1000页

import requests
kw = input('请输入您要爬取的贴吧名:')
url = 'https://tieba.baidu.com/f?kw=%{kw}8&pn='.format(kw=kw)
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0'}


for i in range(1000):
    url = urlr.formate(str(i*50))
    r = requests.get(url=url, headers=headers)
    with open('./tieba_pages/{}-第{}页.html'.format(kw,i), 'w', encoding='utf-8') as f:
        # 为什么是utf-8,因为r.content.decode()为utf-8的格式
        f.write(r.content.decode())

扁平胜于嵌套:比如,多用列表推倒式替代某些循环


Request深入

发送post请求

用法:

response = requests.post('https://www.baidu.com',data=data,headers=headers)
post时不仅需要地址,还需要我们post的数据,该数据就放在data中
data的形式:字典

使用代理

在这里插入图片描述

正向代理与反向代理
在这里插入图片描述

反向代理:浏览器不知道服务器的地址,比如以上的图例中,浏览器知道的只是nginx服务器,因此,及时有攻击,也只能攻击nginx,不能攻击到我们的服务器

正向代理:浏览器知道服务器的地址

爬虫为什么要使用代理

  • 让服务器以为不是同一个客户端在请求
  • 防止我们的真实地址被泄漏,防止被追究

使用代理

用法:requests.get('http://www.baidu.com',proxies=proxies)
proxies的形式是字典

proxies={
   'htttp':'http://12.34.56.78:8888', #如果请求的是http
   'https':'https://12.34.56.78:8888' #如果请求的是https的地址
}

免费代理的网址:https://proxy.mimvp.com/free.php

代理一般可以分为3种:

  • 透明代理
  • 普匿代理,透明以及普匿,对方是可以追查到我们的真实ip的
  • 高匿代理

要注意,不是所有的ip都支持发送https的请求,有些也不支持发送post请求

代码示例:

"""
0. 准备大量ip地址,组成ip池,随机选择一个ip地址来使用
    - 如何随机选择ip
        - {'ip':ip,'times':0}
        - [{},{},...{}],对这个ip的列表按照使用次数进行排序
        选择使用次数较少的几个ip,从中随机选择一个
1. 检查代理的可用性
    - 使用request添加超时参数,判断ip的质量
    - 在线代理ip质量检测的网站
"""
import requests

proxies = {"http":'http://123.56.74.13:8080'}

headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'
}

r = requests.get('http://www.baidu.com', proxies=proxies,headers=headers)

print(r.status_code)
print(r.request.url)

session和cookie的使用与处理

cookie和session的区别

  • cookie存储在客户的浏览器上,session存储在服务器上
  • cookie的安全性不如session,别人可以分析存放在本地的cokie并进行cookie欺骗
  • session会在一定时间内保存在服务器上,当访问增多,会比较比较占用服务器的性能
  • cookie保存的数据容量有限(通常是4k),很多浏览器限制一个站点最多保存20个cookie

爬虫处理cookie和session

  • 带上 cookie和session的好处:

    能够请求到登录之后的页面

  • 带上cookie和session的弊端:

    一套cookie和session往往和一个用户对应,请求太快、次数太多,容易被服务器识别为爬虫

不需要cookie的时候尽量不去使用cookie

但是为了获取登录之后的页面,我们必须发送带有cookies的请求

携带cookie请求:

  • 携带一堆cookie进行请求,把cookie组成cookie池

如何使用

requests提供了一个叫做session的类,来实现客户端和服务器端的会话保持

  1. 实例化一个session对象:session = requests.session()
  2. 让session发送get或post请求:r = sessioon.get(url=url,data=post_data, headers=headers)

请求登录之后的网站的思路:

  • 实例化session
  • 先使用session发送请求,登陆对应网站,把cookie保存在session中,这里请求时,url应是表单的action的值,如果没有action项,就尝试抓包,看看当我们提交的时候,究竟给哪个网址发送了post请求;post_data是表单中的要提交的数据,其键为name
  • 再使用session请求登录之后才能访问的网站,session能够自动的携带登录成功时保存在其中的cookie,进行请求

案例:访问淘宝的登录后的页面

import requests

sesssion = requests.session()

headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0'}
#注意:在copy User-Agent时,一定要复制全,不能直接在查看的时候copy,容易带上省略号
post_url = 'https://login.m.taobao.com/login.htm'
#post的url一般是在源码中表单的action中找

post_data = {
    'TPL_username':'xxxx',
    'TPL_password2':'xxxx'
}#表单中要填写的项

sesssion.post(url=post_url, data=post_data, headers=headers)

r = sesssion.get('https://h5.m.taobao.com/mlapp/mytaobao.html',headers=headers)

with open('taobao.html', 'w', encoding='utf-8') as f:
    f.write(r.content.decode()) #会发现taobao.html中的代码与我们登录淘宝后的https://h5.m.taobao.com/mlapp/mytaobao.html的代码一样,即成功访问了登录淘宝后的页面

不发送post请求,使用cookie获取登录后的页面

即:直接将cookie加在headers里面,而不必使用session进行post

如:

import requests

headers = {
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36',
    'Cookie':'xxxx'
}
url = 'https://i.taobao.com/my_taobao.htm'

r = requests.get(url=url, headers=headers)

print(r.content.decode())

也可以对cookies以参数形式传递,cookies为字典

r = requests.get('http://xxxx',headers=headers, cookies=cookies)
  • cookie过期时间很长的
  • 在cookie过期之前能够拿到所有的数据,比较麻烦
  • 配合其他程序一起使用,其他程序专门获取cookie,当前程序专门请求页面

寻找登录的post地址

  • 在form表单中查找actiond的url地址

    • post的数据是input标签中的name的值作为键,真正的用户名密码作为值的字典,post的url地址就是action对应的url地址
  • 抓包,看看当我们提交的时候,究竟给哪个网址发送了post请求

    • 勾选perserve log按钮,防止页面跳转找不到url

    • 寻找post数据,确定参数

      • 参数不会变:(如何确定参数会不会变?多请求几次),直接用,比如密码不是动态加密的时候

      • 参数会变

        • 参数在当前的响应中
        • 通过js生成:定位到对应的js查看

定位想要的js

  • 法一:对于Chrome浏览器

    • 选择登录按钮(或任意绑定了js事件的对象)
    • Eventlistener
    • 勾选Framework listeners
    • 查看对应的js
    • 找到登录按钮对应的函数
    • (如果遇到某个元素(如:$('.password').value)是干嘛的,可以copy到console中去进行查看;也可以直接对js添加断点)
  • 法二:对于Chrome浏览器

    • 直接通过Chrome中的search all file的搜索url中的关键字
  • 法三

    添加断点的方式来查看js的操作,通过python进行同样的操作,就可以得到js生成的数据

Requests的小技巧

cookie对象与字典的相互转化与url编解码

"""1. 把cookie对象(cookiejar)转化为字典"""
import requests
r = requests.get('http://www.baidu.com')
print(r.cookies)
ret = requests.utils.dict_from_cookiejar(r.cookies)
print(ret)  #输出:{'BDORZ': '27315'}
"""将字典转化为cookiejar"""
print(requests.utils.cookiejar_from_dict(ret))

"""2. url地址的编解码"""
url = 'http://www.baidu.com'
print(requests.utils.quote(url))    #输出:http%3A//www.baidu.com
print(requests.utils.unquote(requests.utils.quote(url)))    #输出:http://www.baidu.com

"""输出结果如下:
<RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>]>
{'BDORZ': '27315'}
<RequestsCookieJar[<Cookie BDORZ=27315 for />]>
http%3A//www.baidu.com
http://www.baidu.com
"""

请求SSL证书验证与超时设置

如果某网站使用的是https,但是又没有购买ssl证书,等浏览器访问时就会提示不安全,而当我们使用爬虫爬取的就会报错,此时,我们可以用verify=False来解决

import requests
r = requests.get('https://www.12306.cn/mormhweb/',verify=False, timeout=10) #如果超时,会报错,因此要结合try使用
"""注意:此时不会报错,但是会warning"""

配合状态码判断是否请求成功

assert response.status_code == 200  #如果断言失败,会报错,因此应该结合try使用

重新请求

使用retrying模块,通过装饰器的方式使用

"""重新请求"""
import requests
from retrying import retry

headers= {
            'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'
        }
@retry(stop_max_attempt_number=3)   #即下面的方法最多可以报错3次,如果3次都报错,则程序报错
def _parse_url(url):
    r = requests.get(url,headers=headers,timeout=0.01)
    assert r.status_code == 200
    return r.content.decode()

def parse_url(url):
    try:
        html_str = _parse_url(url)
    except:
        html_str = None
    return html_str
if __name__ == "__main__":
    url = 'http://www.baidu.com'
    print(parse_url(url))

ps:安装第三方模块的方法

  • pip install
  • 下载源码文件,进入解压后的目录:python setup.py install
  • xxx.whl文件,安装方法:pip install xxx.whl

数据提取方法

基础知识

什么是数据提取

从响应中获取我们想要的数据

数据的分类

  • 非结构话数据:html等

    • 处理方法:正则表达式、xpath
  • 结构化数据:json、xml等

    • 处理方法:转化为python数据类型

主要是看结构清不清晰

数据提取之JSON

由于把json数据转化为python内奸数据类型很简单,所以爬虫缀,我们常使用能够返回json数据的url

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它使得人们很容易进行阅读和编写 。同时也方便了机器进行解析和生成,适用于进行数据交换的场景,比如网站前后台间的数据交换

Q:哪里能够找到返回json的url呢?

  • 使用chrome切换到手机页面
  • 抓包手机app的软件

在这里插入图片描述

json.loads与json.dumps

"""
1. json.loads   能够把json字符串转换成python类型
2. json.dumps   能够把python类型转换为json字符串,当我们把数据保存问文本的时候常常需要这么做,如果要使其显示中文,可以使用参数:ensure_ascii=False;还使用使用参数:indent=2,使下一级相对上一级有两个空格的缩进
"""

使用json的注意点:

  • json中的引号都是双引号;

    • 如果不是双引号

      • eval:能实现简单的字符串和python类型的转化
      • replace:把单引号替换为双引号
  • 往一个文件中写如多个json串,不再是一个json串

    • 可以一行写一个json串,按照行来读取

json.load与json.dump

类文件对象:具有read和write方法的对象就是类文件对象,比如:f = open('a.txt','r'),f就是类文件对象(fp)

"""
1. json.load(类文件对象) #类型为dict
2. json.dump(python类型, 类文件对象)   #把python类型放入类文件对象中,也可以使用ensure_ascii和indent参数
"""

json在数据交换中起到了一个载体的作用,承载着相互传递的数据


在这里插入图片描述

案例:爬取豆瓣

import requests
from pprint import pprint   #pprint:pretty print,实现美化输出
import json

from retrying import retry

url = 'https://m.douban.com/rexxar/api/v2/skynet/playlists?from_rexxar=true&for_mobile=1'

headers = {
    'User-Agent': 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Mobile Safari/537.36',
    # 'Sec-Fetch-Mode': 'cors'
    'Referer': 'https://m.douban.com/movie/beta'
    #在本次爬取过程中,必须加上Referer才行
}
@retry(stop_max_attempt_number=3)
def parse_url(url):
    r = requests.get(url,headers=headers, timeout=10)
    assert r.status_code == 200
    return r.content.decode()
    
resp_html = parse_url(url)
p_resp = json.loads(resp_html)
pprint(p_resp)
with open('douban.json','w', encoding='utf-8') as f:
    f.write(json.dumps(p_resp, indent=2, ensure_ascii=False))

douban.json中的部分内容如下:


在这里插入图片描述

案例:爬取36kr

"""爬取36kr"""
import requests,json
from pprint import pprint
import re
url = 'https://36kr.com/'

headers = {
    'User-Agent':'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Mobile Safari/537.36'
}
r = requests.get(url=url, headers=headers,timeout=3)
html_str = r.content.decode()
reg = '<span class="item-title weight-bold ellipsis-2">(.*?)</span>'    #新闻的标题是直接在html中的
ret = re.findall(reg, html_str)
pprint(ret)

部分输出结果如下:


在这里插入图片描述

爬虫思路总结

  • 通常,我们访问某个网站时,得到的是其主页的url
  • 得到了主页的url后,观察我们所需要的数据是否在主页对应的响应中,如果在,直接利用主页的url爬取
  • 如果不在主页的url中,查找我们需要的数据,得到其对应url,用该url进行数据的爬取
  • 如果相应数据不是在html中,而是json中,用json.loads对数据进行处理

正则表达式复习

所谓正则表达式,即:事先定义好一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤

常用的正则表达式的方法有

  • re.compile:编译
  • pattern.math:从头找一个
  • pattern.search:找一个
  • pattern.findall:找所以
  • pattern.sub:替换
在这里插入图片描述

说明:

  • .的Dotall即模式即是在匹配时加上re.Dotall参数,或者re.S,使.能够匹配任意字符

  • 记忆:d:digit;s:space

  • sub的使用,re.sub(reg, new_str, old_str),将匹配到的内容替换为new_str

  • re.findall('a(.*)b', 'str'),能够返回括号中的内容,括号前后的内容起到定位和过滤的效果

  • r'a\nb' 可以匹配'a\nb';r'a\nb'而不能匹配'a\nb',r可以忽略转义符号带来的影响,待匹配的字符串里面有 几个\,正则表达式里面也写几个\即可

  • compile的作用

    • 将对应正则表达式能够匹配到的内容放到内存中去,加快匹配的速度
    • 使用方法:re.compile(reg)
  • compile和sub的结合使用

    b = hello1world2
    p = re.compile('\d')
    p.findall(b)
    p.sub('_',b)    #将b中的所有数字替换为下划线
    

    ps:如果是对.进行编译,若想使其能够匹配换行符等,则re.S需要加在编译的使用,而不是匹配的时候

贪婪模式与非贪婪模式

  • 非贪婪模式:.*?或者.+?
  • 贪婪模式:.*或者.+

XPATH和lXML

基础知识

lxml是一款高性能的python HTML/XML解析器,利用xpath,我们可以快速的定位特定元素以及获取节点信息

什么是xpath

xpath(XML Path Language)是一门在HTML/XML文档中查找信息的语言(既然是一种语言,就有自己的语法),克用来在html/xml中对元素和属性进行遍历

W3School官方文档:http://www.w3school.com.cn/xpath/index.asp

xml与html对比

在这里插入图片描述

节点的概念:每个xml标签都称之为节点,比如下图中的<book><title>

在这里插入图片描述

book节点是title等节点的父节点,title和author等是兄弟节点,此外,还有祖先节点等概念

常用节点选择工具

  • Chrome插件XPATH Helper
  • 开源的XPATH表达式编辑工具:XMLQuire(XML格式文件可用)
  • FireFox插件 XPath Checker

XPATH语法

XPATH使用路径表达式来选取xml文档中的节点或者节点集。这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常类似

注意:我们写xpath的时候,看的是请求页的响应,而不是elements

在这里插入图片描述

使用Chrome插件的时候,当我们选中标签,该标签会添加属性class='xh-highlight'

/html   即表示从根节点开始选中html标签
/html/head  选中html标签下的head标签
/html/head/link 选中html标签下的head标签中的所有link标签

xpath学习重点

  • 使用xpath helper或者是Chrome浏览器中的copy xpath都是从element中提取的数据,但是爬虫获取的是url对应的响应,往往和elements不一样

  • 获取属性

    • /html/head/link/@href 选择html标签下的head标签下的(所有)link标签中的href属性的值
  • 获取文本

    • /html/head/link/text() 即选取标签中的内容,innerHtml
    • /html//text() 获取html下所有的标签文本
    • //a[text()='下一页']选择文本为下一页三个字的a标签
  • 从当前节点往下开始选择与使用@进行元素的定位

    • //li 选中文档中所有的li标签
    • //li//a 文档中的所有li中的所有a标签
    • //ul[@id='detail-list']/li 选中文档中的id为'detail-list'的ul标签下的li标签;如果没有id,也可以@class等

选择特定节点

在这里插入图片描述

上图中的部分说明:

  • /bookstore/book[price>35.00]book用的是子节点中的price标签进行的修饰,此处price的形式为:<price>35.00</price>

选择未知节点

在这里插入图片描述

选择若干路径(或的运用)

在这里插入图片描述

lXML库

使用入门

"""
1. 导入lxml的etree库:from lxml import etree,注意,如果是在pycharm中,可能会报红,但是不影响使用
2. 利用etree.HTML,将字符串转化为Element对象
3. Element对象具有xpath的方法:html = etree.HTML(text)    html.xpath('字符串格式的xpath语法')
"""

应用举例:

from lxml import etree
from pprint import pprint

text = """
             <tr>                                                                                                                                                     
                <td class="opr-toplist1-right">586万<i class="opr-toplist1-st c-icon c-icon-up"></i></td>
            </tr>
            <tr>
                <td class="opr-toplist1-right">539万<i class="opr-toplist1-st c-icon c-icon-up"></i></td>
            </tr>
            <tr>
                <td class="opr-toplist1-right">444万<i class="opr-toplist1-st c-icon c-icon-up"></i></td>
            </tr>
            <tr>
                <td class="opr-toplist1-right">395万<i class="opr-toplist1-st c-icon "></i></td>
"""
html = etree.HTML(text)
#html为一个Element对象
pprint(html)
#查看element对象中包含的字符串(bytes类型)
pprint(etree.tostring(html).decode())   #会发现把缺少的标签进行了补全,包括html和body标签
print(html.xpath('//td/text()'))   #这里的html是上面etree.HTML(text)获得的对象,结果为列表
#只要是element对象,就可以使用xpath进行数据的提取

lxml注意点

  • lxml可以自动修正html代码(但是不一定能正确修正,也可能改错了)

    • 使用etree.tostring查看修改后的样子,根据修改之后的html字符串写xpath
  • 提取页面数据的思路

    • 先分组,渠道:一个包含分组标签的列表
    • 遍历:取其中每一组进行数据的提取,不会造成数据对应错乱

xpath的包含

  • //div[contains(@class='li')]获取包含有li样式类的标签的标签

爬虫的思路总结

  1. 准备url

    1. 准备start_url

      • url地址规律不明显,总数不确定

      • 通过代码提取下一页url

        • xpath:url在当前的响应里面
        • 寻找url地址,部分参数在当前的响应中,比如当前页面数和总的页码数(eg:通过js生成)
    2. 准备url_list

      • 页码总数明确
      • url地址规律明显
  2. 发送请求,获取响应

    • 添加随机的User-Agent:反反爬虫

    • 添加随机的代理ip:反反爬虫

    • 在对方判断出我们是爬虫之后,添加更多的header字段,包括cookie

      • cookie的处理可以通过session来解决

      • 准备一堆能用的cookie,组成cookie池

        • 如果不登录,准备当开始能够请求对方网站的cookie,即接受对方网站设置在response的cookie

          • 下一次请求的时候,使用之前的列表中的cookie来请求
          • 即:专门用一个小程序来获取cookie,爬取数据再用另一个程序
        • 如果登录

          • 准备一堆账号
          • 使用程序获取每个账号的cookie
          • 之后请求登录之后才能访问的网站随机的选择cookie
  3. 提取数据

    • 确定数据的位置

      • 如果数据在当前的url地址中

        • 提取的是列表页的数据

          • 直接请求列表页的url地址,不用进入详情页
        • 提取的是详情页的数据

          • 确定url地址
          • 发送请求
          • 提取数据
          • 返回
      • 如果数据不在当前的url地址中

        • 在其他的响应中,寻找数据的位置
        • 使用chrome的过滤条件,选择除了js,css,img之外的按钮(但是可能出错)
        • 使用chrome的search all file,搜索数字和英文(有时候不支持搜索中文)
    • 数据的提取

      • xpath,从html中提取整块数据,先分组,之后没一组再提取
      • json
      • re,提取max_time,price,html中的json字符串
  4. 保存

    • 保存在本地,text、json、csv
    • 保存在数据库

CSV

逗号分隔值,一种文件后缀,以纯文本的形式存储表格数据

其文件中的一行对应表格的一行,以逗号分隔列


多线程爬虫

在这里插入图片描述

动态HTML技术

在这里插入图片描述

Selenium和PhantomJS

  • Selenium

    Selenium是一个Web的自动化测试工具,可以控制一些浏览器(比如phantomJS),可以接受指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏

  • PhantomJS

    phantomJS是一个基于Webkit的“无界面”浏览器,它会把网站加载到内存并执行页面上的javascript

下载地址:http://phantomjs.org/download.html

入门

"""1. 加载网页"""
from selenium import webdriver
driver = webdriver.PhantomJS("xxxx/phantom.exe")
"""除了PhantomJS,还有Chrome,FireFox等"""
driver.get("http://www.baiud.com/")
driver.save_screenshot("长城.pnh")

"""2. 定位和操作"""
driver.find_element_byid("kw").send_keys("长城")
drvier.finde_element_by_id("su").click()

"""3. 查看和请求信息"""
driver.page_source()
driver.get_cookies()
driver.current_url()

"""4. 退出"""
driver.close()  #退出当前页面
driver.quit()   #退出浏览器

"""5. 其他"""
#ps:无论是使用PhantomJS还是Chrome或是FireFox,driver的操作是一样的

基础使用示例

ps:chromedriver的下载地址(注意:版本一定要和你安装的Chrome浏览器的版本号一致):http://npm.taobao.org/mirrors/chromedriver/

from selenium import webdriver
"""
selenium请求的速度很慢,因为是使用浏览器,会请求js、css等
"""

phantom_path = r"D:\Green\phantomjs-2.1.1-windows\bin\phantomjs.exe"
"""在使用phatnomjs时,报了unexpected exit, status code 0的错误,尚未找到原因"""
chrome_path = r"C:\Users\25371\Desktop\chromedriver_win32\chromedriver.exe"
"""注意:这里一定要是chromedriver.exe,而不是chrome.exe"""

driver = webdriver.Chrome(executable_path=chrome_path) #实例化对象
# driver.maximize_window()    #最大化窗口
driver.set_window_size(width=1920,height=1080)  #设置窗口大小
driver.get("http://www.baidu.com")
driver.find_element_by_id('kw').send_keys("python")
#kw是百度的输入框的表单的id;send_keys就是往一个input标签里面输入内容
#以上的一行代码就可以时Chrome自己百度搜索python
driver.find_element_by_id('su').click()
#su是百度一下的按钮的id
#click实现对按钮的点击

"""获取当前的url"""
print(driver.current_url)   #注意:因为已经click了,所以是click后的地址

"""截屏"""
driver.save_screenshot("./baidu_python.png")
"""在本次截屏中,由于截屏太快而网页加载太慢,截屏的图中未能截到百度出来的结果"""

"""driver获取cookie"""
cookies = driver.get_cookies()
print(cookies)
cookies = {i['name']:i['value'] for i in cookies}  #使用字典推导式重新生成requests模块能用的cookies
print(cookies)

"""获取html字符串"""
"""即elements"""
print(driver.page_source)   #page_source是一个属性,获得html字符串后,就可以直接交给xpath

"""退出当前页面"""
driver.close()  #如果只有一个窗口,close就是退出浏览器
"""退出浏览器"""
driver.quit()

示例二

from selenium import webdriver
from time import sleep
chrome_path = r"C:\Users\25371\Desktop\chromedriver_win32\chromedriver.exe"
driver = webdriver.Chrome(executable_path=chrome_path)
driver.get('https://www.qiushibaike.com/text/page/1/')
ret = driver.find_elements_by_xpath(".//div[@id='content-left']/div")
for r in ret:
    print(r.find_element_by_xpath("./a[1]/div[@class='content']/span").text)
    """通过text属性获取文本"""
    print(r.find_element_by_xpath("./a[1]").get_attribute("href"))
    """通过get_attribute获取属性"""

driver.quit()

元素的定位方法

  • find_element_by_id 返回一个
  • find_elements_by_id 返回一个列表
  • find_elements_by_link_text
  • find_elements_by_partial_link_text
  • find_elements_by_tag_name
  • find_element_by_class_name
  • find_elements_by_class_name
  • find_elements_by_css_selector

注意:

  • 获取文本或属性时,需要先定位到对应元素,再使用text属性或者get_attribute方法
  • element返回一个,elements返回列表
  • link_text和partial_link_text的区别:全部文本和包含的某个文本,即partial可以只写部分文本,而link_text需要写完整
  • by_css_selector的用法:#food span.dairy.aged
  • by_xpath中获取属性和文本需要使用get_attribute()和.text
  • selenium使用class定位标签的时候,只需要其中的一个class样式名即可,而xpath必须要写所有的class样式类名

示例:

from selenium import webdriver
from time import sleep
chrome_path = r"C:\Users\25371\Desktop\chromedriver_win32\chromedriver.exe"

driver = webdriver.Chrome(executable_path=chrome_path)

driver.get('https://www.qiushibaike.com/text/page/1/')

ret = driver.find_elements_by_xpath(".//div[@id='content-left']/div")
for r in ret:
    print(r.find_element_by_xpath("./a[1]/div[@class='content']/span").text)
    """通过text属性获取文本"""
    print(r.find_element_by_xpath("./a[1]").get_attribute("href"))
    """通过get_attribut获取属性"""

print('-'*50)
"""find_element_by_link_text"""
"""根据标签里面的文字获取元素"""
print(driver.find_element_by_link_text("下一页").get_attribute('href'))
"""partial_link_text"""
print(driver.find_element_by_partial_link_text("下一").get_attribute('href'))
"""以上两行代码获得的东西相同"""
driver.quit()

深入

iframe

iframe或frame里面的html代码和外面的html代码实质上是两个不同的页面,因此,有时候我们在定位元素时,明明elements里面有,但是会定位失败

解决办法:使用driver.switch_to.frame或driver.switch_to_frame(已经被弃用)方法切换到对应frame中去

driver.switch_to.frame的使用说明:

def frame(self, frame_reference):
"""
        Switches focus to the specified frame, by index, name, or webelement.

        :Args:
         - frame_reference: The name of the window to switch to, an integer representing the index,
                            or a webelement that is an (i)frame to switch to.

        :Usage:
            driver.switch_to.frame('frame_name')
            driver.switch_to.frame(1)
            driver.switch_to.frame(driver.find_elements_by_tag_name("iframe")[0])
        """
        源码

代码示例:豆瓣登录

from selenium import webdriver
import time
chrome_path = r"C:\Users\25371\Desktop\chromedriver_win32\chromedriver.exe"
driver = webdriver.Chrome(executable_path=chrome_path)

driver.get("https://douban.com/")
login_frame = driver.find_element_by_xpath("//iframe[1]")
driver.switch_to.frame(login_frame)
driver.find_element_by_class_name("account-tab-account").click()
driver.find_element_by_id("username").send_keys("784542623@qq.com")
driver.find_element_by_id("password").send_keys("zhoudawei123")

driver.find_element_by_class_name("btn-account").click()
time.sleep(10)   #暂停以手动进行验证

"""获取cookies"""
cookies = {i['name']:i['value'] for i in driver.get_cookies()}
print(cookies)

time.sleep(3)
driver.quit()

注意

  • selenium获取的页面数据 是浏览器中elements的内容
  • selenium请求第一页的时候,会等待页面加载完了之后再获取数据,但是在点击翻页之后,会立马获取数据,此时可能由于页面还没有加载完而报错

其他

  • cookies相关用法

    • {cookie['name']:cookie['value'] for cookie in driverr.get_cookies()} 获取字典形式的cookie
    • driver.delete_cookie('cookiename')
    • driver.delete_all_cookies()
  • 页面等待

    页面等待的原因:如果网站采用了动态html技术,那么页面上的部分元素出现时间便不能确定,这个时候就需要一个等待时间

    • 强制等待:time.sleep(10)
    • 显示等待(了解)
    • 隐式等待(了解)

Tesseract的使用

  1. tesseract是一个将图像翻译成文字的OCR库,ocr:optical character recognition

  2. 在python中安装tesseract模块:pip install pytesseract,使用方法如下:

    import pytesseract
    from PIL import Image
    image = Image.open(jpg) #jpg为图片的地址
    pytesseract.image_to_string(image)
    

使用tesseract和PIL,我们就可以使程序能够识别验证码(当然,也可以通过打码平台进行验证码的识别)

示例:对如下图片进行识别

在这里插入图片描述
import pytesseract
from PIL import Image
img_url = "verify_code.jpg"
pytesseract.pytesseract.tesseract_cmd = r'D:\Tesseract-OCR\tesseract.exe'
image = Image.open(img_url)

print(pytesseract.image_to_string(image))

"""
使用过程中的问题:
    1. 电脑上必须先安装tesseract客户端,然后才能结合pytesseract使用
    2. 将tesseract加入环境变量
    3. 在环境变量中新建项:名字:TESSDATA_PREFIX,值:你的tesseract的安装目录(tessdata的父级目录)
    4. 在代码中加入:pytesseract.pytesseract.tesseract_cmd = r"tesseract的安装路径\tesseract.exe"
    5. 默认只识别英语,如果要识别其他语言,需要下载相关语言的.traneddata文件到Tesseract的安装目录下的tessdata路径下:https://github.com/tesseract-ocr/tesseract/wiki/Data-Files
    
"""
"""识别结果如下:
Happy
Birthday
"""

Mongodb

注意:以下用到的集合大多为stu(学生信息),少部分为products

基础入门

Mongodb是一种NoSQL数据库

mysql的扩展性差,大数据下IO压力大,表结构更改困难;而nosql易扩展,大数据量高性能,灵活的数据模型,高可用

下载地址:https://www.mongodb.com/download-center/community

mongodb的使用

  • 在终端运行MongoDB\bin\mongod.exe --dbpath D:\MongoDB\data,其中,D:\MongoDB是安装路径,(注意:下图中我在安装时把MongoDB写成了MonggoDB

    在这里插入图片描述
    • 因为在安装时我们默认安装了MongoDB Compass Community,我们打开该软件,直接连接即可,不用对其做任何更改,成功后如图所示:
    在这里插入图片描述
    • 如果我们要使用数据库,还需要将安装目录下小bin目录加入系统的环境变量,然后在终端输入mongo即可

ps:在mongo的交互环境中,可以通过tab键进行命令的补全

database的基础命令

  • 查看当前数据库:db
  • 查看所有的数据库:show dbs或show databases
  • 切换数据库:use db_name
  • 删除当前数据库:db.dropDatabase()

在mongodb里面,是没有表的概念的,数据是存储在集合中

向不存在的集合中第一次插入数据时,集合会被创建出来

手动创建集合:

  • db.createCollection(name,options)
  • options是一个字典,例如:{size:10, capped:true} #表示存储的上限为10条数据,capped为true表示当数据达到上限时,会覆盖之前的数据
  • 查看集合:show collections
  • 删除集合:db.collection_name.drop()

mongodb中的数据类型

  • ObjectID:文档id,所谓文档,即我们即将存储到数据库中的一个个的字典
  • String
  • Boolean:必须是小写,true或false
  • Integer
  • Double
  • Arrays
  • Object:用于嵌入式的文档,即一个值为一个文档
  • Null
  • Timestamp:时间戳
  • Date:创建日期的格式:new Date("2019-02-01")

注意点:

  • 每个文档的都有一个属性,为_id,保证文档的唯一性
  • 可以自己设置_id插入文档,如果没有提供,自动生成,类型为Object_id
  • objecID是一个12字节的16进制数,4:时间戳,3:机器id,2:mongodb的服务进程id,3:简单的增量值

数据的操作

  • 用insert插入:先use数据库,db.集合名.insert({"name":"zhang3", "age":23}),实质上插入的数据不是字典,而是json,因此键可以不用引号。insert的时候如果文档id已经存在,会报错
  • 查看表中的数据:db.表名.find()
  • 用save进行数据的插入:db.集合名.save(要插入的数据),如果文档id(对应我们要插入的数据)已经存在,就是修改,否则新增
  • 查看集合中的数据:db.集合名称.find()
  • 更新:db.集合名称.update(<query>,<update>,{multi:<boolean>}),用法如下:
    db.stu.upate({name:'hr'}, {name:'mnc'})#更新一条的全部
    db.stu.update({name:'hr'}, {$set:{name:'mnc'}}) #更新一条中的对应键值
    """这种更改用得更多"""
    db.stu.update({},{$set:{gender:0}}, {multi:true})   #更新全部
    """注意:multi这个参数必须和$符号一起使用才有效果"""
    
  • 使用remove删除数据:db.集合名.remove({name:"zhang3"},{justOne:true}),表示只删除一条名字为zhang3的数据,如果不指定justOne,就是删除全部符合的数据

高级查询

find

"""find"""
db.stu.find()   #查询所有的数据
db.stu.find({age:23})   #查询满足条件的数据
db.stu.findOne({age:23})    #查询满足条件的一个数据
db.stu.find().pretty()  #对数据进行美化

比较运算符

1. 等于:默认是等于判断,没有运算符
2. 小于:$lt(less than)
3. 大于:$gt(greater than)
4. 小于等于:$lte(less than equal)
5. 大于等于:$gte(greater than equal)
6. 不等于:$ne(not equal)

使用举例:

db.stu.find({age:{$le(18)}})    #查询年龄小于18的

范围运算符

1. $in:在某个范围
2. $nin:不在某个范围

用法举例:

db.stu.find({age:{$in[18,28,38]}})  #查询年龄为18或28或38的

逻辑运算符

and:直接写多个条件即可,例:db.stu.find({age:{$gte:18},gender:true})
or:使用$or,值为数组,数组中的每个元素为json,例:查询年龄大于18或性别为false的数据:db.stu.find({$or:[{age:{$gt:18}},{gender:{false}}]})

正则表达式

1. db.products.find({sku:/^abc/})   #查询以abc开头的sku
2. db.products.find({sku:{$regex:"789$"}})  #查询以789结尾的sku

limit和skip

1. db.stu.find().limit(2)   #查询两个学生的信息
2. db.stu.find().skip(2)    #跳过前两个学生的信息
3. db.stu.find().skip(2).limit(4)   #先跳过2个,再查找4个

自定义查询

db.stu.find({$where:function(){
    return this.age > 30;
}}) #查询年龄大于30的学生

投影

即返回满足条件的数据中的部分内容,而不是返回整条数据

db.stu.find({$where:function(){
    return this.age > 30, {name:1,hometown:1};
}}) #查询年龄大于30的学生,并返回其名字和hometown,其中,this是指从前到后的每一条数据;如果省略{name:xxx}就会返回该条数据的全部内容

db.stu.find({},{_id:0,name:1,hometown:1})   #显示所有数据的name和hometown,不显示_id,但是要注意,只有_id可以使用0;一般对其他字段来说,要显示的写1,不显示的不写即可,_id默认是会显示的

排序

db.stu.find().sort({age:-1})    #按年龄的降序排列,如果是{age:1}就是按按铃升序排序
db.stu.find().sort({age:1,gender:-1})   #按年龄的升序排列,如果年龄相同,按gender的降序排列

count方法

db.stu.find({条件}).count()   #查看满足条件的数据有多少条
db.stu.count({条件})

消除重复

db.stu.distinct("去重字段",{条件})
db.stu.distinct("hometown",{age:{$gt:18}})  #查看年龄大于18的人都来自哪几个地方

聚合aggregate

聚合(aggregate)是基于数据处理的聚合管道,每个文档通过一个由多个阶段(stage)组成的管道,可以对每个阶段的管道进行分组、过滤等功能,然后经过一系列的处理,输出相应的结果。
db.集合名称.aggregate({管道:{表达式}})

所谓管道,即把上一次的输出结果作为下一次的输入数据

常用管道如下

  • $group: 将集合中的⽂档分组, 可⽤于统计结果
  • $match: 过滤数据, 只输出符合条件的⽂档,match:匹配
  • $project: 修改输⼊⽂档的结构, 如重命名、 增加、 删除字段、 创建计算结果
  • $sort: 将输⼊⽂档排序后输出
  • $limit: 限制聚合管道返回的⽂档数
  • $skip: 跳过指定数量的⽂档, 并返回余下的⽂档
  • $unwind: 将数组类型的字段进⾏拆分,即展开的意思

表达式

语法:表达式:'$列名'
常⽤表达式:

  • sum: 计算总和,sum:1 表示以⼀倍计数
  • $avg: 计算平均值
  • $min: 获取最⼩值
  • $max: 获取最⼤值
  • $push: 在结果⽂档中插⼊值到⼀个数组中
  • $first: 根据资源⽂档的排序获取第⼀个⽂档数据
  • $last: 根据资源⽂档的排序获取最后⼀个⽂档数据

用法示例

"""group的使用"""
db.Temp.aggregate(
    {$group:{_id:"$gender"}}
)   #按性别分组
"""输出结果:
{ "_id" : 1 }
{ "_id" : 0 }
"""
db.Temp.aggregate( 
{$group:{_id:"$gender",count:{$sum:1}}} 
)   #按性别分组并计数,sum:1是指每条数据作为1
"""输出结果如下:
{ "_id" : 1, "count" : 7 }
{ "_id" : 0, "count" : 1 }
"""
"""注意:_id和count的键不能变"""
db.Temp.aggregate( 
    {$group:{_id:"$gender",
    count:{$sum:1},
    avg_age:{$avg:"$age"}}} 
)   #按年龄分组并计数,再分别计算其年龄的平均值
"""结果如下:
{ "_id" : 1, "count" : 7, "avg_age" : 22.857142857142858 }
{ "_id" : 0, "count" : 1, "avg_age" : 32 }
"""
"""注意:如果分组时_id:null,则会将整个文档作为一个分组"""



"""管道的使用"""
db.Temp.aggregate(
    {$group:{_id:"$gender",count:{$sum:1},avg_age:{$avg:"$age"}}},
    {$project:{gender:"$_id",count:"$count",avg_age:"$avg_age"}}
)   #将group的输出再作为project的输入,因为前面已经有了_id,count,avg_age等输出键,所以在后面的管道中可以直接使用(此例中用了_id和avg_age),也可以使用1使其显示,0使其不显示
"""输出结果如下
{ "count" : 7, "gender" : 1, "avg_age" : 22.857142857142858 }
{ "count" : 1, "gender" : 0, "avg_age" : 32 }
"""


"""match管道的使用"""
#为什么使用match过滤而不是find的过滤?match可以将其数据交给下一个管道处理,而find不行
db.Temp.aggregate(
    {$match:{age:{$gt:20}}},
    {$group:{_id:"$gender",count:{$sum:1}}},
    {$project:{_id:0,gender:"$_id",count:1}}
)   #先选择年龄大于20的数据;然后将其交给group管道处理,按照性别分组,对每组数据进行计数;然后再将其数据交给project处理,让_id字段显示为性别,不显示_id字段,显示count字段

"""sort管道的使用"""
db.Temp.aggregate(
    {$group:{_id:"$gender",count:{$sum:1}}},
    {$sort:{count:-1}}
)   #将第一个管道的数据按照其count字段的逆序排列,和find中的排序使用方式一样
"""结果如下:
{ "_id" : 1, "count" : 7 }
{ "_id" : 0, "count" : 1 }
"""

"""skip和limit的用法示例:
{$limit:2}
{$skip:5}
"""

"""unwind使用使例:"""
eg:假设某条数据的size字段为:['S','M','L'],要将其拆分
db.Temp.aggregate(
    {$match:{size:["S","M","L"]}},  #先找到该数据
    {$unwind:"$size"}
)
"""结果如下:
{ "_id" : ObjectId("5d57cada783675188ccd7ee0"), "size" : "S" }
{ "_id" : ObjectId("5d57cada783675188ccd7ee0"), "size" : "M" }
{ "_id" : ObjectId("5d57cada783675188ccd7ee0"), "size" : "L" }
"""


小练习:

"""数据和需求:
{ "country" : "china", "province" : "sh", "userid" : "a" }  
{  "country" : "china", "province" : "sh", "userid" : "b" }  
{  "country" : "china", "province" : "sh", "userid" : "a" }  
{  "country" : "china", "province" : "sh", "userid" : "c" }  
{  "country" : "china", "province" : "bj", "userid" : "da" }  
{  "country" : "china", "province" : "bj", "userid" : "fa" }  
需求:统计出每个country/province下的userid的数量(同一个userid只统计一次)
"""
db.Exci.aggregate(
    {$group:{_id:{userid:"$userid",province:"$province",country:"$country"}}},  #先按照三个字段分组(去重)
    {$group:{_id:{country:"$_id.country",province:"$_id.province"},count:{$sum:1}}},
    {$project:{_id:0,country:"$_id.country",province:"$_id.province",count:"$count"}}
)
"""注意:取字典里面的元素用(.)操作符;group的_id可以为字典"""
"""三个管道处理过后的数据分别如下:
#第一次group
{ "_id" : { "userid" : "a", "province" : "sh", "country" : "china" } }
{ "_id" : { "userid" : "b", "province" : "sh", "country" : "china" } }
{ "_id" : { "userid" : "c", "province" : "sh", "country" : "china" } }
{ "_id" : { "userid" : "da", "province" : "bj", "country" : "china" } }
{ "_id" : { "userid" : "fa", "province" : "bj", "country" : "china" } }

#第二次group
{ "_id" : { "country" : "china", "province" : "bj" }, "count" : 2 }
{ "_id" : { "country" : "china", "province" : "sh" }, "count" : 3 }

#最终结果
{ "country" : "china", "province" : "sh", "count" : 3 }
{ "country" : "china", "province" : "bj", "count" : 2 }
"""

"""也可以写成如下形式:"""
db.Temp.aggregate(
    {$match:{size:["S","M","L"]}},
    {$unwind:{path:"$size",preserveNullAndEmptyArrays:true}}
)   #path字段是要拆分的字段,参数表示保存Null和EmptyArrays,因为如果是原数据中有某字段为null或[],那么在拆分一个数据后,表中原来含nul或[]的那几条数据会消失

索引

作用:提升查询速度

db.t1.find({查询条件})
db.t1.find({查询条件}).explain('executionStats') 可以通过其中的"executionTimeMillisEstimate"字段查看查询所花费的时间

建立索引

  • 语法:db.集合.ensureIndex({属性1:1}, {unique:true}) 其中,1表示升序,-1表示降序,一般来说,升序或降序的影响不大;unique字段可以省略,加上后,保证索引唯一,即:如果我们用name作为索引,那么在集合中就不能有name值相同的数据;

  • 联合索引,即ensureIndex的参数为{属性1:1或-1, 属性2:-1或1},联合索引是为了保证数据的唯一性

  • 唯一索引的作用:比如,当我们爬取数据时,如果使用了唯一索引,那么,当我们爬到重复数据时,就不会存储到数据库中

  • 默认有一个index:_id

  • 查看索引:db.集合.getIndex()

  • 删除索引:db.集合.dropIndex("索引名称") #索引名称即我们创建索引时传入的字典{属性:1或-1},可以通过getIndex()查看时的key项

爬虫数据去重,实现增量式爬虫

  • 使用数据库建立唯一索引进行去重

  • url地址去重

    • 使用场景

      • 如果url对应的数据不会变,url地址能够唯一的判别一条数据的情况
    • 思路

      • url地址存在redis中
      • 拿到url地址,判断url地址在url的集合中是否存在
      • 存在:不再请求
      • 不存在:请求,并将该url地址存储到redis数据库中
    • 布隆过滤器

      • 使用加密算法加密url地址,得到多个值
      • 往对应值的位置把结果设置为1
      • 新来一个url地址,一样通过加密算法生成多个值
      • 如果对应位置的值全为1,说明这个url已经被抓过
      • 否则没有被抓过,就把对应位置的值设置为1
  • 根据数据本身去重

    • 选择特定字段,使用加密算法(md5,shal)将字段进行加密,生成字符串,存入redis集合中
    • 如果新来一条数据,同样的方法进行加密,如果得到的数据在redis中存在,说明数据存在,要么插入,要么更新,否则不存在,直接插入

数据的备份与恢复

备份的语法
mongodump -h dbhost -d dbname -o dbdirectory
-h: 服务器地址, 也可以指定端⼝号,如果是本机上,就可以省略
-d: 需要备份的数据库名称
-o: 备份的数据存放位置, 此⽬录中存放着备份出来的数据

备份的数据中,一个json和一个bson表示一个集合

恢复的语法
mongorestore -h dbhost -d dbname --dir dbdirectory
-h: 服务器地址
-d: 需要恢复的数据库实例,即数据库的名字
--dir: 备份数据所在位置


pymongo的使用

pip install pymongo
from pymongo import MongoClient

用法示例

from pymongo import MongoClient

client = MongoClient(host='127.0.0.1',port=27017)
#实例化client,即和数据库建立连接
collection = client['admin']['Temp']  #使用[]选择数据库和集合

collection.insert_one({"name":"laowang","age":33})  #插入一条数据
it_data = [{"name":"laoli","age":23},{"name":"laozhao","age":43}]
collection.insert_many(it_data) #插入多条数据

print(collection.find_one({"name":"laoli"}))
print(collection.find())    #是一个Cursor(游标)对象,一个Cursor对象只能进行一次遍历
for ret in collection.find():
    print(ret)  #遍历查看cursor对象
print(list(collection.find()))  #强制转化为list

collection.delete_one({"name":"laoli"}) #删除一个
collection.delete_many({"age":33})  #删除所有age为33的
# mongodb不需要我们手动断开连接

scrapy

scrapy简介

为什么要使用scrapy:使我们的爬虫更快更强

scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架

scrapy使用了Twisted异步网络框架,可以加快我们的下载

scrapy的工作流程

在这里插入图片描述

scheduler里面实际上存放的并不是url地址,而是request对象

在spiders处,url要先组装成request对象再交到scheduler调度器

scrapy引擎的作用:scheduler将request交给scrapy engine,engine再交给下载器,response也是先由下载器交给scrapy engine,然后再由engine交给spiders,url类似,先交给scrapy engine,再交给scheduler

engine实现了程序的解耦,response和request在经过scrapy后,还要经过各自的middleware,再交到目的地,因此我们就可以定义自己的中间件,对reponse和request进行一些额外的处理

爬虫中间件不会对爬虫提取的数据进行数据(实际上可以,但是因为有专门的部分进行这项工作,所以我们通常不这么做)

在这里插入图片描述

scrapy入门

  • 创建一 个scrapy项目:scrapy startproject 项目名(eg:myspider)


    在这里插入图片描述
  • 生成一个爬虫:scrapy genspider 爬虫名字 "允许爬取的域名"

  • 提取数据

    • 完善spider,使用xpath等方法
  • 保存数据

    • pipline中保存数据

scrapy具体流程及spider和pipline讲解

此处的项目名为mySpider,创建的爬虫名字为itcast

  • 新建一个python项目

  • 在Terminal中:scrapy startproject mySpider

  • 在Terminal中,根据提示:cd mySpider

  • 在Terminal中:scrapy genspider itcast "itcast.cn",此时,如果创建成功,就会在spiders目录中有了itcast.py;在里面,我们写上如下代码段内容:

    • 在项目文件夹中使用scrapy新建的爬虫都在spider文件夹中,每个spider文件即对应上面流程图中的spiders,其中有几个默认字段:

      • name:爬虫的名字,默认有
      • allowed_domains:默认有(在使用scrapy新建spider的时候通常会指定)
      • start_urls:默认有,但是通常需要我们自己修改,其值为我们最开始请求的url地址
      • parse方法:处理start_url对应的响应地址,通过yield将我们提取到的数据传递到pipline
    import scrapy
    
    
    class ItcastSpider(scrapy.Spider):
        name = 'itcast' #爬虫名
        allowed_domains = ['itcast.cn'] #允许爬取的范围
        start_urls = ['http://www.itcast.cn/channel/teacher.shtml']  #最开始请求的url地址
    
        def parse(self, response):
            """处理start_url对应的响应"""
            # ret1 = respnse.xpath("//div[@class='tea_con']//h3/text()").extract()
            # #提取数据
            # #extract()方法可以提取其中的文字
            # print(ret1)
            li_list = response.xpath("//div[@class='tea_con']//li")
            for li in li_list:
                item = {}
                item['name'] = li.xpath(".//h3/text()").extract_first() #提取第一个字符串
                #使用extract_first,如果是没有拿到元素,会返回none,而不是报错(extract()[0]在拿不到元素的情况下会报错)
                item['title'] = li.xpath(".//h4/text()").extract_first()
                yield item  #将item传给piplines
    
  • 在Terminal中,进入项目文件夹下:scrapy crawl itcast,就会自动开始爬取;然后在terminal中输出一些结果和一些日志,我们可以在settings.py中对日志的级别进行设置,比如添加:LOG_LEVEL = "WARNING",比warning等级小的日志都不会输出

  • pipline的使用

    • pipline对应流程图中的item pipline

    • 要使用pipline,需要在项目的settings.py文件中取消对pipline的注释,使其可用,即

      # Configure item pipelines
      # See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
      ITEM_PIPELINES = {
         'mySpider.pipelines.MyspiderPipeline': 300,
         #意为:mySpider项目下的piplines模块中的MyspiderPipline类,后面的数字(300)表示距离引擎的远近,越近,数据就会越先经过该pipline
         #所谓管道,就是把前面管道处理后的数据再交给后面的管道处理,这里ITEM_PIPLINES的格式为字典:管道类的位置:距离引擎的远近
         #把spiders提取的数据由近及远交给各个管道处理
         #亦即:我们可以在piplines中定义自己的多个管道,然后在这里进行注册,使其可用,如下
         'mySpider.pipelines.MyspiderPipeline1': 301,
      }
      
  • 执行爬虫爬取数据:scrapy crawl 爬虫的名字(类下面的name的值)

  • 在piplines.py中,我们定义了如下的两个管道

    • 为了让数据能够在各个管道间进行传递,每个管道必须return item,这里的item即为spider中传递过来的数据
    • process_item方法是必须的,专门用于对数据的处理,只有它可以接受传递过来的item
    • spider就是爬虫在传item的时候,把自己也传递过来了,即:这里的参数spider就是我们在spiders目录下的爬虫名的py文件中定义的spider类
    class MyspiderPipeline(object):
        def process_item(self, item, spider):
            print(item)
            return item
    
    class MyspiderPipeline1(object):
        def process_item(self, item, spider):
            print(item.items())
            return item
    
  • 为什么需要有多个pipline

    • 一个项目通常有多个爬虫,而对于爬取的数据,我们通常要进行的处理不相同,因此就需要使用不同的pipline
      • 此时需要对传过来的item进行判别,比如可以使用在item中添加某字段以判别(或者使用spider进行判别,比如:if spider.name == xxx),如果是我们要处理的数据才进行处理,否则传递给其他pipline
    • 一个spider的内容可能要做不同的操作,比如存入不同的数据库中,我们就可以使用多个pipline分多步进行

logging模块的使用

在settings.py文件中,可以添加字段:LOG_LEVEL="log等级",以控制当前爬虫的输出的log等级(大于)

在spider中输出log的两种常用方法:

  • import logging,然后使用logging.warning(要输出的信息),无法显示log来自哪个文件

  • impot logging,然后logger = logging.getLogger(__name__),使用logger.warning(要输出的数据),此种方法可以输出日志来自哪个文件

    • ps:我们实例化了一个logger之后,在其他的文件中如果要使用log,不必单独再去实例化一个对象,直接导入现有的logger即可

如果我们要想使log输出到文件中,而非terminal,则需要在settings.py中添加字段:LOG_FILE = "保存log的文件路径"

如果要自定义log的格式,在使用logging前:logging.basicConfig(xxx),其中的xxx即我们要自定义的log格式
logging.basicConfig()示例:

import logging
 
logging.basicConfig(level=logging.DEBUG,
                format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
                datefmt='%a, %d %b %Y %H:%M:%S',
                filemode='w')
"""basicConfig参数"""
logging.basicConfig函数各参数:
filename: 指定日志文件名
filemode: 和file函数意义相同,指定日志文件的打开模式,'w'或'a'
format: 指定输出的格式和内容,format可以输出很多有用信息,如上例所示:
 %(levelno)s: 打印日志级别的数值
 %(levelname)s: 打印日志级别名称
 %(pathname)s: 打印当前执行程序的路径,其实就是sys.argv[0]
 %(filename)s: 打印当前执行程序名
 %(funcName)s: 打印日志的当前函数
 %(lineno)d: 打印日志的当前行号
 %(asctime)s: 打印日志的时间
 %(thread)d: 打印线程ID
 %(threadName)s: 打印线程名称
 %(process)d: 打印进程ID
 %(message)s: 打印日志信息
datefmt: 指定时间格式,同time.strftime()
level: 设置日志级别,默认为logging.WARNING
stream: 指定将日志的输出流,可以指定输出到sys.stderr,sys.stdout或者文件,默认输出到sys.stderr,当stream和filename同时指定时,stream被忽略

翻页请求

  • 在爬虫中,首先,获得下一页的url:next_url = response.xpath("//a[text()='下一页']/@href").extract()以获得下一页的url
  • 然后使用scrapy.Request构造一个request,同时指定回调函数
  • 在回调函数中,对于要pipline处理的数据,同样要yield
    while next_url:
        yield scrapy.Request(next_url, callback=self.parse)
        #如果下一页数据的处理和当前页相同,那么回调函数就直接指定当前函数即可,如果处理方式,则另外定义一个函数进行回调即可;这里实际上也是实例化了一个request对象交给引擎
    

scrapy.request的其他知识点

在这里插入图片描述
  • 如图所示,在crapy中,cookies就不能再放到headers中去
  • 所谓解析函数,可以简单理解为回调函数,meta的格式为字典,在解析函数中获取该数据时:response.meta["键"]

设置user-agent

  • user-agent的使用:在项目的设置文件中找到对应项进行设置即可

案例(结合下面的item)

爬取阳光热线问政平台:

# -*- coding: utf-8 -*-
import scrapy
from yangguang.items import YangguangItem

class YgSpider(scrapy.Spider):
    name = 'yg'
    allowed_domains = ['sun0769.com']
    start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=0']

    def parse(self, response):
        tr_list = response.xpath("//div[@class='greyframe']/table[2]/tr/td/table/tr")
        for tr in tr_list:
            item = YangguangItem()
            item['title'] = tr.xpath("./td[2]/a[@class='news14']/@title").extract_first()
            item['href'] = tr.xpath("./td[2]/a[@class='news14']/@href").extract_first()
            item['publish_data'] = tr.xpath("./td[last()]/text()").extract_first()

            yield scrapy.Request(
                item['href'],
                callback= self.parse_detail,
                meta={"item":item}
            )
            next_url = response.xpath("//a[text()='>']/@href").extract_first()
            if next_url:
                yield scrapy.Request(
                    next_url,
                    callback=self.parse
                )


    def parse_detail(self,response):
        """处理详情页"""
        item = response.meta['item']
        item['content'] = response.xpath("//td[@class='txt16_3']//text()").extract_first()
        item['content_img'] = response.xpath("//td[@class='txt16_3']//img/@src").extract()
        yield  item

scrapy深入

Items

使用流程:

  • 先在items.py中写好我们要使用的字段,如图:


    在这里插入图片描述
    • 说明:就相当于我们定义了一个字典类,规定好了里面可以有的键,以后如果使用这个字典的实例时,发现想往里面存入未定义的键,程序就会报错
  • 使用时,在spider中导入该类(这里是Myspideritem),然后用其实例化一个对象(当作字典使用即可)

  • 然后通常是用这个字典存储我们提取的数据,然后把其在各个piplines间传递

    在这里插入图片描述
  • 注意:item对象不能直接插入mongodb(只是像字典,毕竟不是字典),可以强制将其转化为字典,然后存入即可

  • ps:对不同爬虫爬取的数据,我们可以定义多个item类,然后在pipline中处理时,可以用isinstance判断是否为某个item类的实例,如果是,我们才处理

debug信息

在这里插入图片描述

scrapy shell

Scrapy shell是一个交互终端,我们可以在未启动spider的情况下尝试及调试代码,也可以用来测试XPath表达式

使用方法:
scrapy shell http://www.itcast.cn/channel/teacher.shtml

就会自动进入shell,在shell中进行一些操作,会自动提示
然后就能得到response

response:

  • response.url:当前响应的url地址
  • response.request.url:当前响应对应的请求的url地址
  • response.headers:响应头
  • response.body:响应体,也就是html代码,默认是byte类型
  • response.requests.headers:当前响应的请求头
  • response.xpath()

spider:

  • spider.name
  • spider.log(log信息)

settings

settings中的字段

默认已有字段:

  • bot_name:项目名
  • spider_modules:爬虫位置
  • newspider_module:新建爬虫的位置
  • user-agent:用户代理
  • robotstxt_obey:是否遵守robot协议
  • CONCURRENT_REQUESTS:并发请求的最大数量
  • DOWNLOAD_DELAY:下载延迟
  • CONCURRENT_REQUESTS_PER_DOMAIN:每个域名的最大并发请求数
  • CONCURRENT_REQUESTS_PER_IP:每个代理ip的最大并发请求数
  • COOKIES_ENABLED:是否开启cookies
  • TELNETCONSOLE_ENABLED:是否启用teleconsole插件
  • DEFAULT_REQUEST_HEADERS:默认请求头
  • spider_midddleware:爬虫中间件
  • downlowd_middleware:下载中间件
  • EXTENSIONS:插件
  • ITEM_PIPELINES:管道,其格式为:管道的位置:权重
  • AUTOTHROTTLE_ENABLED:自动限速
  • 缓存的配置项

可自己添加字段:

  • LOG_LEVEL

在其他位置中要使用配置中的数据:

  1. 法一:直接导入settings模块使用
  2. 如果是在spider中:可以直接用self.settings.get()或是self.settings[]以字典的形式存取相关数据
  3. 如果是在pipline中,由于传过来了spider,就以spider.settings.get()或spider.settings[]存取

piplines

import json
class myPipline(object):
    def open_spider(self,spider):
    """在爬虫开启的时候执行一次"""
    #比如实例化MongoClient
        pass

    def close_spider(self,spider):
    """在爬虫关闭的时候执行一次"""
        pass

    def process_item(self,item,spider):
    """对spider yield过来的数据进行处理"""
        pass
        return item
        """如果不return item,其他的pipline就无法获得该数据"""

CrawlSpider

之前爬虫的思路:
1、从response中提取所有的a标签对应的url地址
2、自动的构造自己requests请求,发送给引擎

改进:

满足某个条件的url地址,我们才发送给引擎,同时能够指定callback函数

如何生成crawlspider:
eg:
scrapy genspider –t crawl csdn “csdn.com"

crawlspider示例:

from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
import re

class CfSpider(CrawlSpider):    #继承的父类不再是scrapy.spider
    name = 'cf'
    allowed_domains = ['circ.gov.cn']
    start_urls = ['http://circ.gov.cn/web/site0/tab5240/module14430/page1.htm']

    """定义提取url规则的地方"""
    """每个Rule是一个元组"""
    """注意:每个url提取出来后被构造成一个请求,他们没有先后顺序"""
    rules = (
        #Rule是一个类,LinkExtractor: 链接提取器,其参数是一个正则表达式,提取到了link,就交给parse函数进行请求
        #所以我们在crawlspider中不能自己定义parse函数
        #url请求的响应数据会交给callback处理,如果不用提取该url中的数据,就可以不指定callback
        #follow,当前url地址的相应是否重新进入rules来提取url地址(会挨个按规则提取,如果被前面的正则表达式匹配到了,就不会再被后面的进行匹配提取,所以写正则表达式的时候应该尽可能明确)
        #注意:crawlspider能够帮助我们自动把链接补充完整,所以我们下面的allow中并没有手动补全链接
        Rule(LinkExtractor(allow=r'/web/site0/tab5240/info\d+.htm'), callback='parse_item', follow=False),
        Rule(LinkExtractor(allow=r'/web/site0/tab5240/module14430/page\d+.htm'), follow=True),
    )
    """parse函数不见了,因为其有特殊功能,不能定义"""
    def parse_item(self, response):
        """解析函数"""
        item = {}
        #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
        #item['name'] = response.xpath('//div[@id="name"]').get()
        #item['description'] = response.xpath('//div[@id="description"]').get()
        item['title'] = re.findall(r"<!--TitleStart-->(.*?)<!--TitleEnd-->",response.body.decode())[0]
        item['date'] = re.findall(r"发布时间:(\d{4}-\d{2}-\d{2})",response.body.decode())
        print(item)
    #   也可以自己再yiel scrapy.Request
    #     yield scrapy.Request(
    #         url,
    #         callback=self.parse_detail,
    #         meta={"item":item}
    #     )
    #
    # def parse_detail(self,response):
    #     item = response.meta['item']
    #     pass
    #     yield item

LinkExtractor和Rule的更多知识点

在这里插入图片描述

中间件

下载中间件

下载中间件是我们要经常操作的一个中间件,因为我们常常需要在下载中间件中对请求和响应做一些自定义的处理

如果我们要使用中间件,需要在settings中开启,其格式也是:位置:权重(或者说是距离引擎的位置,越小越先经过)

Downloader Middlewares默认的方法

  • process_request(self, request, spider):
    当每个request通过下载中间件时,该方法被调用。
  • process_response(self, request, response, spider):
    当下载器完成http请求,传递响应给引擎的时候调用

案例:使用随机user-agent:

在settings.py中定义USER_AGENTS_LIST:

USER_AGENTS_LIST = ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36',
                    'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0',
                    ]

在middlewares.py中定义下载中间件:

import random
class RandomUserAgentMiddleware:
    """自定义一个下载中间件"""
    def process_request(self,request,spider):
        ua = random.choice(spider.settings.get("USER_AGENTS_LIST"))
        request.headers["User-Agent"] = ua
        #request.meta['proxy'] = "你的proxy"  #也可以通过此种方法来使用代理
        # return request,process不能返回request

class CheckUserAgentMiddleware:
    def process_response(selfs,request,response,spider):
        print(request.headers["User-Agent"])    #查看是否实现了随机用户代理
        return response
        # process_response必须返回reponse

并在settings.py中对自己的中间件进行注册:

DOWNLOADER_MIDDLEWARES = {
   # 'circ.middlewares.CircDownloaderMiddleware': 543,
    'circ.middlewares.RandomUserAgentMiddleware': 544,
    'circ.middlewares.CheckUserAgentMiddleware': 545,   #这里的权重并不重要,因为他们一个是处理请求,一个是处理响应的
}

spiders内容省略

scrapy模拟登录

对于scrapy来说,有两个方法模拟登陆:
1、直接携带cookie
2、找到发送post请求的url地址,带上信息,发送请求

直接携带cookie

案例:爬取人人网登录后的信息

import scrapy
import re

class RenrenSpider(scrapy.Spider):
    name = 'renren'
    allowed_domains = ['renren.com']
    start_urls = ['http://www.renren.com/971962231/profile']    #人人网的个人主页

    def start_requests(self):
        """覆盖父类的start_request方法,从而使其携带我们自己的cookies"""
        cookies = "我的cookies"
        cookies = {i.split("=")[0]:i.split("=")[1] for i in cookies.split(";")}
        yield scrapy.Request(
            self.start_urls[0],
            callback=self.parse,    #表示当前请求的响应会发送到parse
            cookies=cookies
        )   #因为cookies默认是enable的,所以下次请求会自动拿到上次的cookies

    def parse(self, response):
        print(re.findall(r"假装",response.body.decode())) #验证是否请求成功
        yield scrapy.Request(
            "http://www.renren.com/971962231/profile?v=info_timeline",
            callback=self.parse_data,
        )

    def parse_data(self,response):
        """"访问个人资料页,验证cookie的传递"""
        print(re.findall(r"学校信息",response.body.decode()))

此外,为了查看cookies的传递过程,可以在settings中加上字段:COOKIES_DEBUG = True

scrapy发送post请求

案例:爬取github

spider:

import scrapy
import re

class GitSpider(scrapy.Spider):
    name = 'git'
    allowed_domains = ['github.com']
    start_urls = ['https://github.com/login']

    def parse(self, response):
        authenticity_token = response.xpath("//input[@name='authenticity_token']/@value").extract_first()
        utf8 = response.xpath("//input[@name='utf8']/@value").extract_first()
        webauthn_support = response.xpath("//input[@name='webauthn-support']/@value").extract_first()
        post_data = {
            "login":"你的邮箱",
            "password":"密码",
            "webauthn-support":webauthn_support,
            "authenticity_token":authenticity_token,
            "utf8":utf8
        }
        print(post_data)
        yield scrapy.FormRequest(
            "https://github.com/session",   #数据提交到的地址
            formdata=post_data,
            callback=self.after_login
            #无论这个post请求成功没有,响应都会交给after_login
        )

    def after_login(self,response):
        print(response)
        print(re.findall(r"Trial_Repo", response.body.decode()))    #匹配我的某个仓库名,以验证是否成功

settings中的修改字段:

USER_AGENT = 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Mobile Safari/537.36'
ROBOTSTXT_OBEY = False

scrapy还可以帮我们自动从表单中提取action的地址

爬取github登录后的页面:
spider:

# -*- coding: utf-8 -*-
import scrapy
import re

class Github2Spider(scrapy.Spider):
    name = 'github2'
    allowed_domains = ['github.com']
    start_urls = ['https://github.com/login']

    def parse(self, response):
        post_data = {
            "login":"2537119279@qq.com",
            "password":"A1d9b961017#"
        }
        yield scrapy.FormRequest.from_response(
            #自动从response中寻找form表单,然后将formdata提交到对应的action的url地址
            #如果有多个form表单,可以通过其他参数对表单进行定位,其他参数见源码
            response,
            formdata=post_data,
            callback=self.after_login,
        )

    def after_login(self,response):
        print(re.findall(r"Trial_Repo", response.body.decode()))

另:无需在settings中设置任何东西


scrapy_redis

  • scrapy_redis的作用:reqeust去重,爬虫持久化,和轻松实现分布式

scrapy_redis爬虫的流程

在这里插入图片描述

上图中的指纹是指request指纹,指纹能够唯一标识一个request,从而避免重复的reqeust请求

所以redis中常存储待爬的request对象和已爬取的request的指纹

redis基础

在这里插入图片描述

scrapy_redis的地址:https://github.com/rmax/scrapy-redis

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260