layout: posttitle: python爬虫笔记
subtitle: 基础知识、requests、asyncio/aiohttp异步爬虫
date: 2019-08-13
author: NSX
header-img: img/post-bg-ios9-web.jpg
catalog: true
tags:
- Python
- 教程
- 爬虫

python爬虫笔记

  1. 爬虫预备知识
  2. 网络请求模块 requests
  3. JSON数据提取
  4. re正则取数据
  5. aiohttp异步爬虫

    01. 爬虫预备知识

爬虫定义

网络爬虫(又被称为网页蜘蛛,网络机器人)就是模拟浏览器发送网络请求,接收请求响应,一种按照一定的规则,自动地抓取互联网信息的程序。

爬虫流程

  1. 向起始url发送请求,并获取响应
  2. 对响应进行提取
  3. 如果提取url,则继续发送请求获取响应
  4. 如果提取数据,则将数据进行保存

网络模型对应关系

  1. HTTP、RTSP、FTP ———-> 应用层
  2. TCP、UDP ———-> 传输层
  3. IP ———-> 网络层
  4. 数据链路 ———-> 数据链路层
  5. 物理介质 ———-> 物理层

url 地址格式

  1. scheme:协议(例如:http, https, ftp)
  2. host:服务器的 IP 地址或者域名
  3. port:服务器的端口(如果是走协议默认端口,缺省端口80)
  4. path:访问资源的路径
  5. query-string:参数,发送给 http 服务器的数据
  6. anchor:锚(跳转到网页的指定锚点位置)

HTTP 请求

  • 请求方式 | 请求方式 | 描述 | | —- | —- | | GET | 请求指定的页面信息,并返回实体主体。 | | HEAD | 类似于 get 请求,只不过返回的响应中没有具体的内容,用于获取报头 | | POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。 | | PUT | 从客户端向服务器传送的数据取代指定的文档的内容 | | DELETE | 请求服务器删除指定的页面。 | | CONNECT | HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。 | | OPTIONS | 允许客户端查看服务器的性能。 | | TRACE | 回显服务器收到的请求,主要用于测试或诊断。 |
  • 常见请求头 | 请求头 | 作用 | | —- | —- | | Cookie | Cookie | | User-Agent | 浏览器名称 | | Referer | 页面跳转处 | | Host | 主机和端口号 | | Connection | 链接类型 | | Upgrade-Insecure-Requests | 升级为 HTTPS 请求 | | Accept | 传输文件类型 | | Accept-Encoding | 文件编解码格式 | | x-requested-with : XMLHttpRequest | ajax 请求 |
  • 模拟请求时,先在headers中加入User-Agent,如果还不可以请求再尝试加入Referer,还无法访问,在尝试加入Cookie,在最后可以尝试加入Host.

HTTP 响应

HTTP 状态码

当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含 HTTP 状态码的信息头(server header)用以响应浏览器的请求。

HTTP 状态码的英文为 HTTP Status Code。HTTP 状态码共分为 5 种类型

分类 分类描述
1** 信息,服务器收到请求,需要请求者继续执行操作
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误

常见的 HTTP 状态码:

  • 200 - 请求成功
  • 301 - 资源(网页等)被永久转移到其它 URL
  • 404 - 请求的资源(网页等)不存在
  • 500 - 内部服务器错误

02. 网络请求模块requests

requests 模块是可以模仿浏览器发送请求获取响应,能够自动帮助我们解压网页内容。

requests模块能够自动帮助我们解压网页内容

基本使用

  1. import requests
  2. def fetch(url):
  3. '''
  4. url: 要请求的网站url
  5. '''
  6. headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'} # 请求时携带的header
  7. resp = requests.get(url, headers=headers, verify=False) # verify忽略https的ssl验证
  8. resp.encoding = resp.apparent_encoding # 猜测编码,防乱码
  9. return resp.text
  10. fetch('https://www.baidu.com')

response 常用属性:

  • response.text 返回响应内容,响应内容为 str 类型
  • respones.content 返回响应内容,响应内容为 bytes 类型
  • response.status_code 返回响应状态码
  • response.headers 返回响应头
  • response.cookies 返回响应的 RequestsCookieJar 对象

发送 GET 请求

  1. # 导入模块
  2. import requests
  3. # 定义请求地址
  4. url = 'http://www.baidu.com/s'
  5. # 定义自定义请求头
  6. headers = {
  7. "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
  8. }
  9. # 定义 GET 请求参数
  10. params = {
  11. "kw":"hello"
  12. }
  13. # 使用 GET 请求参数发送请求
  14. response = requests.get(url,headers=headers,params=params)
  15. # 获取响应的 html 内容
  16. html = response.text

发送请求时 params 参数作为 GET 请求参数

发送 POST 请求

  1. # 导入模块
  2. import requests
  3. # 定义请求地址
  4. url = 'http://www.baidu.com'
  5. # 定义自定义请求头
  6. headers = {
  7. "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
  8. }
  9. # 定义post请求参数
  10. data = {
  11. "kw":"hello"
  12. }
  13. # 使用 POST 请求参数发送请求
  14. response = requests.post(url,headers=headers,data=data)
  15. # 获取响应的 html 内容
  16. html = response.text

发送请求时 data 参数作为 POST 请求参数

使用代理服务器

作用:

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

代理分类:

  • 透明代理(Transparent Proxy):透明代理虽然可以直接“隐藏”你的IP地址,但是还是可以查到你是谁。
  • 匿名代理(Anonymous Proxy):匿名代理比透明代理进步了一点:别人只能知道你用了代理,无法知道你是谁。
  • 混淆代理(Distorting Proxies):与匿名代理相同,如果使用了混淆代理,别人还是能知道你在用代理,但是会得到一个假的IP地址,伪装的更逼真
  • 高匿代理(Elite proxy或High Anonymity Proxy):可以看出来,高匿代理让别人根本无法发现你是在用代理,所以是最好的选择。

在使用的使用,毫无疑问使用高匿代理效果最好

使用方式

  1. # 导入模块
  2. import requests
  3. # 定义请求地址
  4. url = 'http://www.baidu.com'
  5. # 定义自定义请求头
  6. headers = {
  7. "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
  8. }
  9. # 定义 代理服务器
  10. proxies = {
  11. "http":"http://IP地址:端口号",
  12. "https":"https://IP地址:端口号"
  13. }
  14. # 使用 POST 请求参数发送请求
  15. response = requests.get(url,headers=headers,proxies=proxies)
  16. # 获取响应的 html 内容
  17. html = response.text

发送请求携带 Cookies

  1. # 导入模块
  2. import requests
  3. # 定义请求地址
  4. url = 'http://www.baidu.com'
  5. # 定义自定义请求头
  6. headers = {
  7. "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
  8. # 方式一:直接在请求头中携带Cookie内容
  9. "Cookie": "Cookie值"
  10. }
  11. # 方式二:定义 cookies 值
  12. cookies = {
  13. "xx":"yy"
  14. }
  15. # 使用 POST 请求参数发送请求
  16. response = requests.get(url,headers=headers,cookies=cookies)
  17. # 获取响应的 html 内容
  18. html = response.text

03. JSON数据提取

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

JSON相关的方法:

  • json.loads json字符串 转 Python数据类型
  • json.dumps Python数据类型 转 json字符串
  • json.load json文件 转 Python数据类型
  • json.dump Python数据类型 转 json文件

JSON数据提取代码示例:

  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. # 导入模块
  4. import json
  5. # json.loads json字符串 转 Python数据类型
  6. json_string = '''
  7. {
  8. "name": "crise",
  9. "age": 18,
  10. "parents": {
  11. "monther": "妈妈",
  12. "father": "爸爸"
  13. }
  14. }
  15. '''
  16. print("json_string数据类型:",type(json_string))
  17. data = json.loads(json_string)
  18. print("data数据类型:",type(data))
  19. # json.dumps Python数据类型 转 json字符串
  20. data = {
  21. "name": "crise",
  22. "age": 18,
  23. "parents": {
  24. "monther": "妈妈",
  25. "father": "爸爸"
  26. }
  27. }
  28. print("data数据类型:",type(data))
  29. json_string = json.dumps(data)
  30. print("json_string数据类型:",type(json_string))

04. re 正则表达式提取数据 - 正则表达式 提取数据.md)

正则表达式就是记录文本规则的代码

re 模块常用方法:

  • match 开头匹配,只匹配一次

  • search 全局匹配,只匹配一次

  • findall 匹配所有符号条件的数据,返回是 结果列表
    优点:使用简单,缺点:必须把所有数据搜索返回再返回(如果数据量小)

  • finditer 迭代对象,迭代 Match 对象
    优点:找到就返回,可以边找边返回(如果数据量大)

05. aiohttp异步爬虫

异步爬虫不同于多进程爬虫,它使用单线程(即仅创建一个事件循环,然后把所有任务添加到事件循环中)就能并发处理多任务。在轮询到某个任务后,当遇到耗时操作(如请求URL)时,挂起该任务并进行下一个任务,当之前被挂起的任务更新了状态(如获得了网页响应),则被唤醒,程序继续从上次挂起的地方运行下去。极大的减少了中间不必要的等待时间。

参考1 参考2

aiohttp 基本介绍

aiohttp 可以用来实现异步网页请求以及作为异步服务器。aiohttp 是一个利用asyncio的库,性能会比同步的模块提高不少。

aiohttp改写 requests 爬虫代码:

  1. import asyncio
  2. import aiohttp
  3. async def fetch(url):
  4. headers = {'content-type': 'application/json'}
  5. async with aiohttp.ClientSession() as session:
  6. async with session.get(url, headers=headers, verify_ssl=False) as resp:
  7. return await resp.text(errors='ignore')
  8. if __name__ == "__main__":
  9. start_time = time.time()
  10. loop = asyncio.get_event_loop()
  11. get_future = asyncio.ensure_future(fetch('https://www.baidu.com')) # ?
  12. loop.run_until_complete(get_future)
  13. print(get_future.result())
  14. loop.close()

批量url爬取

有大量url时,每个都开一个会话显然是不好的,于是会写成这样:

  1. import asyncio
  2. import aiohttp
  3. async def fetch(session, id_, url):
  4. headers = {'content-type': 'application/json'}
  5. data = json.dumps({"XX": "YY"})
  6. try:
  7. async with session.post(url, data=data, headers=headers, verify_ssl=False) as resp:
  8. assert resp.status == 200
  9. print(resp.charset)
  10. print(await resp.text())
  11. print(await resp.read())
  12. # print(await resp.json()) # aiohttp.client_exceptions.ContentTypeError: 0, message='Attempt to decode JSON with unexpected mimetype: text/plain;charset=utf-8'
  13. # The correct way of use is as follows:
  14. res = await resp.json(content_type=None)
  15. return res
  16. except Exception:
  17. print(f"{id_}, url: {url} error happened:")
  18. print(traceback.format_exc())
  19. async def fetch_all(urls):
  20. '''
  21. urls: list[(id_, url)]
  22. '''
  23. async with aiohttp.ClientSession() as session:
  24. datas = await asyncio.gather(*[fetch(session, id_, url) for id_, url in urls], return_exceptions=True)
  25. for ind, data in enumerate(urls):
  26. id_, url = data
  27. if isinstance(datas[ind], Exception):
  28. print(f"{id_}, {url}: ERROR")
  29. return datas
  30. urls = [(i, f'https://www.baidu.com/?tn={i}') for i in range(100)]
  31. loop = asyncio.new_event_loop()
  32. # loop = asyncio.get_event_loop()
  33. task = loop.create_task(fetch_all(urls))
  34. loop.run_until_complete(task)
  35. print(task.result()) # save data
  36. loop.close()

错误处理

如果url太多,可能会报错ValueError: too many file descriptors in select(),根据stackoverflow所述,aiohttp默认设置中一次可以打开100个连接,而Windows一次最多只能打开64个socket,所以可以在fetch_all中添加一行:

  1. connector = aiohttp.TCPConnector(limit=60) # 60小于64。也可以改成其他数
  2. async with aiohttp.ClientSession(connector=connector) as session:
  3. ...

此外,为防止爬的过客,可以使用信号量来控制:

  1. import time
  2. from lxml import etree
  3. import aiohttp
  4. import asyncio
  5. sem = asyncio.Semaphore(10) # 信号量,控制协程数,防止爬的过快
  6. async def get_title(url):
  7. with(await sem):
  8. async with aiohttp.ClientSession() as session:
  9. async with session.request('GET', url) as resp: # 提出请求
  10. html = await resp.read()

参考

从零到一 Python 爬虫

Python实战异步爬虫(协程)+分布式爬虫(多进程)

4.2 加速爬虫: 异步加载 Asyncio

用aiohttp写爬虫