request爬虫入门

🌸request爬虫入门

从网页的基本结构开始讲述, 慢慢使用一些简单的工具, 做一些简单的爬虫. 还会有一些小练习, 让你爬爬真正的互联网. 下载美图, 逛逛百度百科, 全网爬取等等. 当你懂得了爬虫的概念, 我们在深入一些, 谈谈如何加速你那和蠕虫(爬的慢)一样的爬虫, 把它升级为一只小飞虫(多进程,异步爬取). 当然这些内容都不会特别深入, 重点是把你带入门

简单的网页结构

在 HTML 中, 基本上所有的实体内容, 都会有个 tag 来框住它. 而这个被 tag 住的内容, 就可以被展示成不同的形式, 或有不同的功能. 主体的 tag 分成两部分, headerbody. 在 header 中, 存放这一些网页的网页的元信息, 比如说 title, 这些信息是不会被显示到你看到的网页中的. 这些信息大多数时候是给浏览器看, 或者是给搜索引擎的爬虫看

<head>
    <meta charset="UTF-8">
    <title>云澈の博客</title>
    <link rel="icon" href="https://liudufu.github.io/">
</head>

HTML 的第二大块是 body, 这个部分才是你看到的网页信息. 网页中的 heading, 视频, 图片和文字等都存放在这里. 这里的 <h1></h1> tag 就是主标题, 我们看到呈现出来的效果就是大一号的文字. <p></p> 里面的文字就是一个段落. <a></a>里面都是一些链接. 所以很多情况, 东西都是放在这些 tag 中的.

用 Python 登录网页

好了, 对网页结构和 HTML 有了一些基本认识以后, 我们就能用 Python 来爬取这个网页的一些基本信息. 首先要做的, 是使用 Python 来登录这个网页, 并打印出这个网页 HTML 的 source code. 注意, 因为网页中存在中文, 为了正常显示中文, read() 完以后, 我们要对读出来的文字进行转换, decode() 成可以正常显示中文的形式.

from urllib.request import urlopen

# if has Chinese, apply decode()
html = urlopen(
    "https://liudufu.github.io/"
).read().decode('utf-8')
print(html)

我们能够成功读取这个网页的所有信息了. 但我们还没有对网页的信息进行汇总和利用. 我们发现, 想要提取一些形式的信息, 合理的利用 tag 的名字十分重要.

匹配网页内容

这里我们使用 Python 的正则表达式 RegEx 进行匹配文字, 筛选信息的工作. 如果是初级的网页匹配, 我们使用正则完全就可以了, 高级一点或者比较繁琐的匹配, 我还是推荐使用 BeautifulSoup. 不急不急, 我知道你想偷懒, 我之后马上就会教 beautiful soup 了. 但是现在我们还是使用正则来做几个简单的例子, 让你熟悉一下套路.

如果我们想用代码找到这个网页的 title, 我们就能这样写. 选好要使用的 tag 名称 <title>. 使用正则匹配.

import re
res = re.findall(r"<title>(.+?)</title>", html)
print("\nPage title is: ", res[0])

如果想要找到中间的那个段落 <p>, 我们使用下面方法, 因为这个段落在 HTML 中还夹杂着 tab, new line, 所以我们给一个 flags=re.DOTALL 来对这些 tab, new line 不敏感.

res = re.findall(r"<p>(.*?)</p>", html, flags=re.DOTALL)    # re.DOTALL if multi line
print("\nPage paragraph is: ", res[0])


最后一个练习是找一找所有的链接, 这个比较有用, 有时候你想找到网页里的链接, 然后下载一些内容到电脑里, 就靠这样的途径

res = re.findall(r'href="(.*?)"', html)
print("\nAll links: ", res)

BeautifulSoup基础

我们总结一下爬网页的流程, 让你对 BeautifulSoup 有一个更好的定位.

  1. 选着要爬的网址 (url)
  2. 使用 python 登录上这个网址 (urlopen等)
  3. 读取网页信息 (read() 出来)
  4. 将读取的信息放入 BeautifulSoup
  5. 使用 BeautifulSoup 选取 tag 信息等 (代替正则表达式)

初学的时候总是搞不懂这些包是干什么的, 现在你就能理解这个 BeautifulSoup 到底是干什么的了.

BeautifulSoup 使用起来非常简单, 我们先按常规读取网页

from bs4 import BeautifulSoup
from urllib.request import urlopen

# if has Chinese, apply decode()
html = urlopen("url").read().decode('utf-8')
print(html)

每张网页中, 都有两大块, 一个是 <head>, 一个是 <body>, 我们等会用 BeautifulSoup 来找到 body 中的段落 <p> 和所有链接 <a>.

读取这个网页信息, 我们将要加载进 BeautifulSoup, 以 lxml 的这种形式加载. 除了 lxml, 其实还有很多形式的解析器, 不过大家都推荐使用 lxml 的形式. 然后 soup 里面就有着这个 HTML 的所有信息. 如果你要输出 <h1> 标题, 可以就直接 soup.h1.

soup = BeautifulSoup(html, features='lxml')
print(soup.h1)

print('\n', soup.p)

如果网页中有多个同样的 tag, 比如链接 <a>, 我们可以使用 find_all() 来找到所有的选项. 因为我们真正的 link 不是在 <a> 中间 </a>, 而是在 <a href="link"> 里面, 也可以看做是 <a> 的一个属性. 我们能用像 Python 字典的形式, 用 key 来读取 l["href"].

all_href = soup.find_all('a')
all_href = [l['href'] for l in all_href]
print('\n', all_href)

懂得这些还是远远不够的, 真实情况往往比这些复杂. BeautifulSoup 还有很多其他的选择增强器. 接下来, 我们来了解一些 CSS 的概念, 用 BeautifulSoup 加上 CSS 来选择内容.

CSS

CSS 主要用途就是装饰你 骨感 HTML 页面. 如果将 HTML 比喻成没穿衣服的人, 那 CSS 就是五颜六色的衣服. 穿在人身上让人有了气质. CSS 的规则很多, 好在如果你只是需要爬网页, 你并不需要学习 CSS 的这些用法或规则, (如果你想, 你可以看到这里), 你只需要注意 CSS 的一条规则就能玩转爬虫了.

CSS 的 Class

这条规则就是 CSS 的 Class, CSS 在装饰每一个网页部件的时候, 都会给它一个名字. 而且一个类型的部件, 名字都可以一样. 比如每个网页 里面的字体/背景颜色, 字体大小, 都是由 CSS 来掌控的.

而 CSS 的代码, 可能就会放在这个网页的 <head> 中. 我们先使用 Python 读取这个页面.

from bs4 import BeautifulSoup
from urllib.request import urlopen

# if has Chinese, apply decode()
html = urlopen("URL").read().decode('utf-8')
print(html)

<head> 中, 你会发现有这样一些东西被放在 <style> 里面, 这些东西都是某些 class 的 CSS 代码. 比如 jan 就是一个 class. jan 这个类掌控了这个类型的背景颜色. 所以在 <ul class="jan"> 这里, 这个 ul 的背景颜色就是黄色的. 而如果是 month 这个类, 它们的字体颜色就是红色.

按 Class 匹配

Class 匹配很简单. 比如我要找所有 class=month 的信息. 并打印出它们的 tag 内文字.

soup = BeautifulSoup(html, features='lxml')

# use class to narrow search
month = soup.find_all('li', {"class": "month"})
for m in month:
    print(m.get_text())

或者找到 class=jan 的信息. 然后在 <ul> 下面继续找 <ul> 内部的 <li> 信息. 这样一层层嵌套的信息, 非常容易找到.

jan = soup.find('ul', {"class": 'jan'})
d_jan = jan.find_all('li')              # use jan as a parent
for d in d_jan:
    print(d.get_text())

如果想要找到一些有着一定格式的信息, 比如使用正则表达来寻找相类似的信息, 我们在 BeautifulSoup 中也能嵌入正则表达式

嵌入正则

比如你想下载页面的图片, 我们就可以将图片形式的 url 个匹配出来. 之后再下载就简单多了

正则匹配

我们先读取这个网页. 导入正则模块 re.

from bs4 import BeautifulSoup
from urllib.request import urlopen
import re

# if has Chinese, apply decode()
html = urlopen("URL").read().decode('utf-8')

如果是图片, 它们都藏在这样一个 tag 中:

<td>
    <img src="tf.jpg">
</td>

所以, 我们可以用 soup 将这些 <img> tag 全部找出来, 但是每一个 img 的链接(src)都可能不同. 或者每一个图片有的可能是 jpg 有的是 png, 如果我们只想挑选 jpg 形式的图片, 我们就可以用这样一个正则 r'.*?\.jpg' 来选取. 把正则的 compile 形式放到 BeautifulSoup 的功能中, 就能选到符合要求的图片链接了.

soup = BeautifulSoup(html, features='lxml')

img_links = soup.find_all("img", {"src": re.compile('.*?\.jpg')})
for link in img_links:
    print(link['src'])

又或者我们发现, 我想选一些课程的链接, 而这些链接都有统一的形式, 就是开头都会有 https://., 那我就将这个定为一个正则的规则, 让 BeautifulSoup 帮我找到符合这个规则的链接.

course_links = soup.find_all('a', {'href': re.compile('/tutorials/.*')})
for link in course_links:
    print(link['href'])


我们接下来就来做一个小实战, 让我们的爬虫在百度百科上自由爬行, 在各个百科网页上跳来跳去

爬百度百科

爬一爬百度百科, 让我们的爬虫从 网络爬虫 这一页开始爬, 然后在页面中寻找其他页面的信息, 然后爬去其他页面, 然后循环这么做, 看看最后我们的爬虫到底爬去了哪

百度百科

百度百科中有很多名词的解释信息, 我们今天从 网页爬虫 的词条开始爬, 然后在页面中任意寻找下一个词条, 爬过去, 再寻找词条, 继续爬. 看看最后我们爬到的词条和 网页爬虫 差别有多大.

观看规律

20+行代码. 但是却能让它游走在百度百科的知识的海洋中. 首先我们需要定义一个起始网页, 我选择了 网页爬虫. 我们发现, 页面中有一些链接, 指向百度百科中的另外一些词条, 比如说下面这样.

<a target="_blank" href="/item/%E8%9C%98%E8%9B%9B/8135707" data-lemmaid="8135707">蜘蛛</a>
<a target="_blank" href="/item/%E8%A0%95%E8%99%AB">蠕虫</a>
<a target="_blank" href="/item/%E9%80%9A%E7%94%A8%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E">通用搜索引擎</a>

通过观察, 我们发现, 链接有些共通之处. 它们都是 /item/ 开头, 夹杂着一些 %E9 这样的东西. 但是仔细搜索一下, 发现还有一些以 /item/ 开头的, 却不是词条. 比如

<a href="/item/史记·2016?fr=navbar" target="_blank">史记·2016</a>

我们需要对这些链接做一些筛选, 之前提到 的用 BeautifulSoup 和 正则表达式来筛选应该用得上. 有了些思路, 我们开始写代码吧.

制作爬虫

导入一些模块, 设置起始页. 并将 /item/... 的网页都放在 his 中, 做一个备案, 记录我们浏览过的网页.

from bs4 import BeautifulSoup
from urllib.request import urlopen
import re
import random


base_url = "https://baike.baidu.com"
his = ["/item/%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB/5162711"]

接着我们先不用循环, 对一个网页进行处理, 走一遍流程, 然后加上循环, 让我们的爬虫能在很多网页中爬取. 下面做的事情, 是为了在屏幕上打印出来我们现在正在哪张网页上, 网页的名字叫什么.

url = base_url + his[-1]

html = urlopen(url).read().decode('utf-8')
soup = BeautifulSoup(html, features='lxml')
print(soup.find('h1').get_text(), '    url: ', his[-1])

接下来我们开始在这个网页上找所有符合要求的 /item/ 网址. 使用一个正则表达式(正则教程) 过滤掉不想要的网址形式. 这样我们找到的网址都是 /item/%xx%xx%xx... 这样的格式了. 之后我们在这些过滤后的网页中随机选一个, 当做下一个要爬的网页. 不过有时候很不幸, 在 sub_urls 中并不能找到合适的网页, 我们就往回跳一个网页, 回到之前的网页中再随机抽一个网页做同样的事.

# find valid urls
sub_urls = soup.find_all("a", {"target": "_blank", "href": re.compile("/item/(%.{2})+$")})

if len(sub_urls) != 0:
    his.append(random.sample(sub_urls, 1)[0]['href'])
else:
    # no valid sub link found
    his.pop()
print(his)

有了这套体系, 我们就能把它放在一个 for loop 中, 让它在各种不同的网页中跳来跳去

his = ["/item/%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB/5162711"]

for i in range(20):
    url = base_url + his[-1]

    html = urlopen(url).read().decode('utf-8')
    soup = BeautifulSoup(html, features='lxml')
    print(i, soup.find('h1').get_text(), '    url: ', his[-1])

    # find valid urls
    sub_urls = soup.find_all("a", {"target": "_blank", "href": re.compile("/item/(%.{2})+$")})

    if len(sub_urls) != 0:
        his.append(random.sample(sub_urls, 1)[0]['href'])
    else:
        # no valid sub link found
        his.pop()

requests

获取网页的方式

其实在加载网页的时候, 有几种类型, 而这几种类型就是你打开网页的关键. 最重要的类型 (method) 就是 getpost (当然还有其他的, 比如 head, delete). 刚接触网页构架的朋友可能又会觉得有点懵逼了. 这些请求的方式到底有什么不同? 他们又有什么作用?

我们就来说两个重要的, get, post, 95% 的时间, 你都是在使用这两个来请求一个网页.

  • post
    • 账号登录
    • 搜索内容
    • 上传图片
    • 上传文件
    • 往服务器传数据 等
  • get
    • 正常打开网页
    • 往服务器传数据

这样看来, 很多网页使用 get 就可以了, 比如 百度百科 里的所有页面, 都是只是 get 发送请求. 而 post, 我们则是给服务器发送个性化请求, 比如将你的账号密码传给服务器, 让它给你返回一个含有你个人信息的 HTML.

从主动和被动的角度来说, post 中文是发送, 比较主动, 你控制了服务器返回的内容. 而 get 中文是取得, 是被动的, 你没有发送给服务器个性化的信息, 它不会根据你个性化的信息返回不一样的 HTML.

requests get 请求

有了 requests, 我们可以发送个中 method 的请求. 比如 get. 我们想模拟一下百度的搜索. 首先我们需要观看一下百度搜索的规律. 在百度搜索框中写上CSDN 我们发现它弹出了一串很长长的网址.

https://www.baidu.com/s?word=CSDN&tn=25017023_2_dg&ch=5&ie=utf-8

但是仔细一看, 和 CSDN 有关的信息, 只有前面一小段 (&word=CSDN), 其他的对我们来说都是无用的信息. 所以我们现在来尝试一下如果的无用 url 都去掉会怎样? Duang! 我们还是能搜到 CSDN.

所以&word=CSDN 这就是我们搜索需要的关键信息. 我们就能用 get 来搭配一些自定义的搜索关键词来用 python 个性化搜索. 首先, 我们固定不动的网址部分是 http://www.baidu.com/s, ? 后面的东西都是一些参数 (parameters), 所以我们将这些 parameters 用 python 的字典代替, 然后传入 requests.get() 功能. 然后我们还能用 python (webbrowser模块) 打开一个你的默认浏览器, 观看你是否在百度的搜索页面.

import requests
import webbrowser
param = {"wd": "CSDN"}  # 搜索的信息
r = requests.get('http://www.baidu.com/s', params=param)
print(r.url)
webbrowser.open(r.url)

# http://www.baidu.com/s?wd=%E8%8E%AB%E7%83%A6Python

requests post 请求

post 又怎么用呢? 我们举个小例子, 假设 我们有一个提交信息的窗口, 如果我提交上去这个信息, 那边的服务器会更加这个提交的信息返回出另一个网页. 这就是网页怎么样使用你 post 过去的信息了.

比如我在一个表单填上自己的姓名, 当我点 submit 的时候, 这个姓名 就会被提交给服务器, 然后它会根据提交的姓名返回这个网页.

这样咋看起来好像和上面讲的 get 百度搜索没区别呀? 都是提交一些信息, 返回一个界面. 但是, 重点来了. 你看看网址栏. 你 post 上去的个人信息, 有没有显示在 url 里? 你愿意将你的私密信息显示在 url 里吗? 你 post 过去的信息是交给服务器内部处理的. 不是用来显示在网址上的.

懂了这些, 我们就来看使用 python 和 requests 怎么做 post 这个操作吧.

首先我们调出浏览器的 inspect 然后发现我们填入姓名的地方原来是在一个 <form> 里面.

这个 <form> 里面有一些 <input> 个 tag, 我们仔细看到 <input> 里面的这个值 name="firstname"name="lastname", 这两个就是我们要 post 提交上去的关键信息了. 我们填好姓名, 为了记录点击 submit 后, 浏览器究竟发生了什么翻天覆地的变化, 我们在 inspect 窗口, 选择 Network, 勾选 Preserve log, 再点击 submit, 你就能看到服务器返回给你定制化后的页面时, 你使用的方法和数据.

这些数据包括了:

  • Request URL (post 要用的 URL)
  • Request Method (post)
  • Form Data (post 去的信息)

有了这些记录, 我们就能开始写 Python 来模拟这一次提交 post 了.

data = {'firstname': 'xx', 'lastname': 'xx'}
r = requests.post('url', data=data)
print(r.text)
上传图片

传照片也是 post 的一种, 我们得将本地的照片文件传送到服务器. 我们使用这个网页来模拟一次传照片的过程

如果你留意观察 url, 你会发现, 传送完照片以后的 url 有变动. 我们使用同样的步骤再次检查, 发现, choose file 按键链接的 <input> 是一个叫 uploadFile 的名字. 我们将这个名字记下, 放入 python 的字典当一个 key.

接着在字典中, 使用 open 打开一个图片文件, 当做要上传的文件. 把这个字典放入你的 post 里面的 files 参数. 就能上传你的图片了, 网页会返回一个页面, 将你的图片名显示在上面.

file = {'uploadFile': open('./image.png', 'rb')}
r = requests.post('url', files=file)
print(r.text)

登录

post 还有一个重要的, 就是模拟登录. 再登录的时候发生了什么事情呢?

我们总结一下, 登录账号, 我们的浏览器做了什么.

  1. 使用 post 方法登录了第一个红框的 url
  2. post 的时候, 使用了 Form data 中的用户名和密码
  3. 生成了一些 cookies

第三点我们是从来没有提到过的. cookie, 听起来很熟呀! 每当游览器出现问题的时候, 网上的解决方法是不是都有什么清除 cookie 之类的, 那 cookie 实际上是什么呢? 这里给出了和全面的介绍.

简单来说, 因为打开网页时, 每一个页面都是不连续的, 没有关联的, cookies 就是用来衔接一个页面和另一个页面的关系. 比如说当我登录以后, 浏览器为了保存我的登录信息, 将这些信息存放在了 cookie 中. 然后我访问第二个页面的时候, 保存的 cookie 被调用, 服务器知道我之前做了什么, 浏览了些什么. 像你在网上看到的广告, 为什么都可能是你感兴趣的商品? 你登录淘宝, 给你推荐的为什么都和你买过的类似? 都是 cookies 的功劳, 让服务器知道你的个性化需求.

所以大部分时候, 每次你登录, 你就会有一个 cookies, 里面会提到你已经是登录状态了. 所以 cookie 在这时候很重要. cookies 的传递也特别重要, 比如我用 requests.post + payload 的用户信息发给网页, 返回的 r 里面会有生成的 cookies 信息. 接着我请求去登录后的页面时, 使用 request.get, 并将之前的 cookies 传入到 get 请求. 这样就能已登录的名义访问 get 的页面了.

payload = {'username': 'Morvan', 'password': 'password'}
r = requests.post('/pages/cookies/welcome.php', data=payload)
print(r.cookies.get_dict())

# {'username': 'Morvan', 'loggedin': '1'}


r = requests.get('/pages/cookies/profile.php', cookies=r.cookies)
print(r.text)

# Hey Morvan! Looks like you're still logged into the site!


使用 Session 登录

不过每次都要传递 cookies 是很麻烦的, 好在 requests 有个很 handy 的功能, 那就是 Session. 在一次会话中, 我们的 cookies 信息都是相连通的, 它自动帮我们传递这些 cookies 信息.

同样是执行上面的登录操作, 下面就是使用 session 的版本. 创建完一个 session 过后, 我们直接只用 session 来 postget. 而且这次 get 的时候, 我们并没有传入 cookies. 但是实际上 session 内部就已经有了之前的 cookies 了.

session = requests.Session()
payload = {'username': 'Morvan', 'password': 'password'}
r = session.post('/pages/cookies/welcome.php', data=payload)
print(r.cookies.get_dict())

# {'username': 'Morvan', 'loggedin': '1'}


r = session.get("http://pythonscraping.com/pages/cookies/profile.php")
print(r.text)

# Hey Morvan! Looks like you're still logged into the site!

这就是我们这次的教学, 想了解更多 requests 使用的朋友看到这里.

下载文件

背景

在下载之前, 我们的弄清楚怎么样下载. 想下一张图, 我们首先要到这张图所在的网页. 在这个网页中找到这张图的位置, 并右键 inspect, 找到它在 HTML 中的信息.

发现原图被存放在这个网页, 注意这个地址开头是 /, 并不是完整的网址, 这种形式代表着, 它是在主域名下面的网址. 所以我们还要将其补全, 才能在网址栏中找到这个图片地址.

找到了这个网址, 我们就能开始下载了. 为了下载到一个特定的文件夹, 我们先建立一个文件夹吧. 并且规定这个图片下载地址.

import os
os.makedirs('./img/', exist_ok=True)

IMAGE_URL = "url"

使用 urlretrieve

在 urllib 模块中, 提供了我们一个下载功能 urlretrieve. 使用起来很简单. 输入下载地址 IMAGE_URL 和要存放的位置. 图片就会被自动下载过去了.

from urllib.request import urlretrieve
urlretrieve(IMAGE_URL, './img/image1.png')
使用 request

而在 requests模块, 也能拿来下东西. 下面的代码实现了和上面一样的功能, 但是稍微长了点. 但我们为什么要提到 requests 的下载呢? 因为使用它的另一种方法, 我们可以更加有效率的下载大文件.

import requests
r = requests.get(IMAGE_URL)
with open('./img/image2.png', 'wb') as f:
    f.write(r.content)

所以说, 如果你要下载的是大文件, 比如视频等. requests 能让你下一点, 保存一点, 而不是要全部下载完才能保存去另外的地方. 这就是一个 chunk 一个 chunk 的下载. 使用 r.iter_content(chunk_size) 来控制每个 chunk 的大小, 然后在文件中写入这个 chunk 大小的数据.

r = requests.get(IMAGE_URL, stream=True)    # stream loading

with open('./img/image3.png', 'wb') as f:
    for chunk in r.iter_content(chunk_size=32):
        f.write(chunk)

有了这些知识的积累, 我们就能开始做一个小的实战练习

下载美国地图

找到图片位置

说白了, 每次的爬虫, 都是先分析一下这个网页要找的东西的位置, 然后怎么索引上这个位置, 最后用 python 找到它. 这次也是这个逻辑. 我们看看今天要爬的这个图片网址. 定位到最新图片的位置,

找到这张图片的所在位置, 对比这类型的图片, 找到一种手段来筛选这些图片. 而图片地址都是在 <img> 中.现在我们有了思路, 先找带有 Image__Wrapper Image__Wrapper--relative 的这种 <div>, 然后在 <div> 里面找 <img>.

下载图片
from bs4 import BeautifulSoup
import requests

URL = "https://www.nationalgeographic.com/travel/article/ethical-souvenirs-crafts-shop-australia-india/"

用 BeautifulSoup 找到带有 Image__Wrapper Image__Wrapper--relative 的这种 <div>,

html = requests.get(URL).text
soup = BeautifulSoup(html, 'lxml')
img_ul = soup.find_all('div', {"class": "Image__Wrapper Image__Wrapper--relative"})

从 div中找到所有的 <img>, 然后提取 <img>src 属性, 里面的就是图片的网址啦. 接着, 就用之前在 requests 下载里提到的一段段下载.

for ul in img_ul:
    imgs = ul.find_all('img')
    for img in imgs:
        url = img['src']
        r = requests.get(url, stream=True)
        image_name = url.split('/')[-1]
        with open('./img/%s' % image_name, 'wb') as f:
            for chunk in r.iter_content(chunk_size=128):
                f.write(chunk)
        print('Saved %s' % image_name)

如果你只是偶尔爬一爬网页, 学到目前为止, 你已经入门了, 但是如果你想要继续深入, 你开始对爬虫的效率担忧, 觉得自己爬得太慢, 想要大规模爬取网页, 那么接下来的内容就再适合你不过了. 接下来我们就会提到爬虫的提效方法. 而且现在我们爬取的都是静态网页 , 如果你遇到 JavaScript 很多的动态加载网页 (淘宝等), 就需要selenium .

加速爬虫:多进程分布式

什么是分布式爬虫

分布式爬虫主要是为了非常有效率的抓取网页, 我们的程序一般是单线程跑的, 指令也是一条条处理的, 每执行完一条指令才能跳到下一条. 那么在爬虫的世界里, 这里存在着一个问题.

如果你已经顺利地执行过了前几节的爬虫代码, 你会发现, 有时候代码运行的时间大部分都花在了下载网页上. 有时候不到一秒能下载好一张网页的 HTML, 有时候却要几十秒. 而且非要等到 HTML 下载好了以后, 才能执行网页分析等步骤. 这非常浪费时间.

如果我们能合理利用计算资源, 在下载一部分网页的时候就已经开始分析另一部分网页了. 这将会大大节省整个程序的运行时间. 又或者, 我们能同时下载多个网页, 同时分析多个网页, 这样就有种事倍功半的效用. 分布式爬虫的体系有很多种, 处理优化的问题也是多样的. 这里有一篇博客可以当做扩展阅读, 来了解当今比较流行的分布式爬虫框架.

我们的分布式爬虫

而今天我们想搭建的这一个爬虫, 就是同时下载, 同时分析的这一种类型的分布式爬虫. 虽然算不上特别优化的框架, 但是概念理解起来比较容易. 我有尝试过徒手写高级一点的分布式爬虫, 但是写起来非常麻烦. 我琢磨了一下, 打算给大家介绍的这种分布式爬虫代码也较好写, 而且效率比普通爬虫快了3.5倍.

主要来说, 我们最开始有一个网页, 然后首页中有很多 url, 我们使用多进程 同时开始下载这些 url, 得到这些 url 的 HTML 以后, 同时开始解析 (比如 BeautifulSoup) 网页内容. 在网页中寻找这个网站还没有爬过的链接. 最终爬完整个 网站所有页面.

有了这种思路, 我们就可以开始写代码了. 你可以在我的 Github 一次性观看全部代码.

首先 import 全部要用的模块, 并规定一个主页.

import multiprocessing as mp
import time
from urllib.request import urlopen, urljoin
from bs4 import BeautifulSoup
import re

# base_url = "http://127.0.0.1:4000/"
base_url = 'https://xxx.com/'

我们定义两个功能, 一个是用来爬取网页的(crawl), 一个是解析网页的(parse). 有了前几节内容的铺垫, 你应该能一言看懂下面的代码. crawl() 用 urlopen 来打开网页, 我用的内网测试, 所以为了体现下载网页的延迟, 添加了一个 time.sleep(0.1) 的下载延迟. 返回原始的 HTML 页面, parse() 就是在这个 HTML 页面中找到需要的信息, 我们用 BeautifulSoup 返回找到的信息.

def crawl(url):
    response = urlopen(url)
    # time.sleep(0.1)             # slightly delay for downloading
    return response.read().decode()


def parse(html):
    soup = BeautifulSoup(html, 'lxml')
    urls = soup.find_all('a', {"href": re.compile('^/.+?/$')})
    title = soup.find('h1').get_text().strip()
    page_urls = set([urljoin(base_url, url['href']) for url in urls])   # 去重
    url = soup.find('meta', {'property': "og:url"})['content']
    return title, page_urls, url

网页中爬取中, 肯定会爬到重复的网址, 为了去除掉这些重复, 我们使用 python 的 set 功能. 定义两个 set, 用来搜集爬过的网页和没爬过的.

unseen = set([base_url,])
seen = set()

测试普通爬法

为了对比效果, 我们将在下面对比普通的爬虫和这种分布式的效果. 如果是普通爬虫, 我简化了一下接下来的代码, 将一些不影响的代码去除掉了. 我们用循环一个个 crawl unseen 里面的 url, 爬出来的 HTML 放到 parse 里面去分析得到结果. 接着就是更新 seenunseen 这两个集合了.

特别注意: 任何网站都是有一个服务器压力的, 如果你爬的过于频繁, 特别是使用多进程爬取或异步爬取, 一次性提交请求给服务器太多次, 这将可能会使得服务器瘫痪, 所以为了安全起见, 我限制了爬取数量(restricted_crawl=True). 因为我测试使用的是内网 http://127.0.0.1:4000/ 所以不会有这种压力. 你在以后的爬网页中, 会经常遇到这样的爬取次数的限制 (甚至被封号). 我以前爬 github 时就被限制成一小时只能爬60页.

# DON'T OVER CRAWL THE WEBSITE OR YOU MAY NEVER VISIT AGAIN
if base_url != "http://127.0.0.1:4000/":
    restricted_crawl = True
else:
    restricted_crawl = False

while len(unseen) != 0:                 # still get some url to visit
    if restricted_crawl and len(seen) >= 20:
        break
    htmls = [crawl(url) for url in unseen]
    results = [parse(html) for html in htmls]

    seen.update(unseen)         # seen the crawled
    unseen.clear()              # nothing unseen

    for title, page_urls, url in results:
        unseen.update(page_urls - seen)     # get new url to crawl

使用这种单线程的方法, 在我的内网上面爬, 爬完整个 网站, 一共消耗 52.3秒. 接着我们把它改成多进程分布式.

测试分布式爬法

还是上一个 while 循环, 首先我们创建一个进程池(Pool)… 然后我们修改得到 htmlsresults 的两句代码. 其他都不变, 只将这两个功能给并行了. 我在这里写的都是简化代码,

pool = mp.Pool(4)
while len(unseen) != 0:
    # htmls = [crawl(url) for url in unseen]
    # --->
    crawl_jobs = [pool.apply_async(crawl, args=(url,)) for url in unseen]
    htmls = [j.get() for j in crawl_jobs]

    # results = [parse(html) for html in htmls]
    # --->
    parse_jobs = [pool.apply_async(parse, args=(html,)) for html in htmls]
    results = [j.get() for j in parse_jobs]

    ...

还是在内网测试, 只用了 16.3秒!! 这可比上面的单线程爬虫快了3.5倍. 而且我还不是在外网测试的. 如果在外网, 爬取一张网页的时间更长, 使用多进程会更加有效率, 节省的时间更多.

看到这里, 一定觉得多线程是爬虫的救星. 其实不然

代码全


加速爬虫:异步加载

Python 还提供了一个有力的工具, 叫做 asyncio. 这是一个仅仅使用单线程, 就能达到多线程/进程的效果的工具. 它的原理, 简单说就是: 在单线程里使用异步计算, 下载网页的时候和处理网页的时候是不连续的, 更有效利用了等待下载的这段时间.

传统的单线程下载处理网页可能就像此图(来源)左边蓝色那样, 计算机执行一些代码, 然后等待下载网页, 下好以后, 再执行一些代码… 或者在等待的时候, 用另外一个线程执行其他的代码, 这是多线程的手段. 那么 asyncio 就像右边, 只使用一个线程, 但是将这些等待时间统统掐掉, 下载应该都调到了后台, 这个时间里, 执行其他异步的功能, 下载好了之后, 再调回来接着往下执行.

今天就来尝试使用 asyncio 来替换掉 multiprocessing 或者 threading, 看看效果如何.

Asyncio 库

Asyncio 库是 Python 的原装库, 但是是在 Python 3 的时候提出来的, Python 2 和 Python 3.3- 是没有的. 而且 Python 3.5 之后, 和 Python 3.4 前在语法上还是有些不同, 比如 await 和 yield 的使用, 下面的教程都是基于 Python 3.5+, 使用 Python3.4 的可能会执行有点问题. 调整一下就好.

在 3.5+ 版本中, asyncio 有两样语法非常重要, async, await. 弄懂了它们是如何协同工作的, 我们就完全能发挥出这个库的功能了. 剧透一下, 等会使用单线程爬网页的 asyncio 和之前多进程写的爬网页效果差不多, 而且当并行的进程数少的时候, asyncio 效果还会比多进程快.

基本用法

接着我们来举例介绍 asyncio, 像之前画的图那样, 我们要时刻记住, asyncio 不是多进程, 也不是多线程, 单单是一个线程, 但是是在 Python 的功能间切换着执行. 切换的点用 await 来标记, 能够异步的功能用 async 标记, 比如 async def function():. 首先我们看一下, 不使用 async 完成的一份代码, 然后我们将这份代码改成 async 版的.

# 不是异步的
import time


def job(t):
    print('Start job ', t)
    time.sleep(t)               # wait for "t" seconds
    print('Job ', t, ' takes ', t, ' s')


def main():
    [job(t) for t in range(1, 3)]


t1 = time.time()
main()
print("NO async total time : ", time.time() - t1)

"""
Start job  1
Job  1  takes  1  s
Start job  2
Job  2  takes  2  s
NO async total time :  3.008603096008301
"""

从上面可以看出, 我们的 job 是按顺序执行的, 必须执行完 job 1 才能开始执行 job 2, 而且 job 1 需要1秒的执行时间, 而 job 2 需要2秒. 所以总时间是 3 秒多. 而如果我们使用 asyncio 的形式, job 1 在等待 time.sleep(t) 结束的时候, 比如是等待一个网页的下载成功, 在这个地方是可以切换给 job 2, 让它开始执行.

import asyncio


async def job(t):                   # async 形式的功能
    print('Start job ', t)
    await asyncio.sleep(t)          # 等待 "t" 秒, 期间切换其他任务
    print('Job ', t, ' takes ', t, ' s')


async def main(loop):                       # async 形式的功能
    tasks = [
    loop.create_task(job(t)) for t in range(1, 3)
    ]                                       # 创建任务, 但是不执行
    await asyncio.wait(tasks)               # 执行并等待所有任务完成

t1 = time.time()
loop = asyncio.get_event_loop()             # 建立 loop
loop.run_until_complete(main(loop))         # 执行 loop
loop.close()                                # 关闭 loop
print("Async total time : ", time.time() - t1)

"""
Start job  1
Start job  2
Job  1  takes  1  s
Job  2  takes  2  s
Async total time :  2.001495838165283
"""

从结果可以看出, 我们没有等待 job 1 的结束才开始 job 2, 而是 job 1 触发了 await 的时候就切换到了 job 2 了. 这时, job 1job 2 同时在等待 await asyncio.sleep(t), 所以最终的程序完成时间, 取决于等待最长的 t, 也就是 2秒. 这和上面用普通形式的代码相比(3秒), 的确快了很多.

aiohttp

有了对 asyncio 的基本了解, 我们就来看怎么把它用在爬虫. 这个功能对于爬虫非常的理想, 原因很简单, 我们在等待一个网页下载的时候, 完全可以切换到其它代码, 事半功倍. 但是 asycio 自己还是没办法完成这项任务的, 我们还需要安装另一个牛逼的模块将 requests 模块代替成一个异步的 requests, 这个牛逼的模块叫作 aiohttp (官网在这). 下载安装特别简单. 直接在你的 terminal 或者 cmd 里面输入 pip3 install aiohttp.

接着我们来看看我们怎么用最一般的 requests 模块爬网页, 和我们怎么将 requests 替换成 aiohttp.

import requests

URL = 'https://xxx.com/'


def normal():
    for i in range(2):
        r = requests.get(URL)
        url = r.url
        print(url)

t1 = time.time()
normal()
print("Normal total time:", time.time()-t1)



然后我们在用 aiohttp 来实现一样的功能

import aiohttp


async def job(session):
    response = await session.get(URL)       # 等待并切换
    return str(response.url)


async def main(loop):
    async with aiohttp.ClientSession() as session:      # 官网推荐建立 Session 的形式
        tasks = [loop.create_task(job(session)) for _ in range(2)]
        finished, unfinished = await asyncio.wait(tasks)
        all_results = [r.result() for r in finished]    # 获取所有结果
        print(all_results)

t1 = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))
loop.close()
print("Async total time:", time.time() - t1)



我们刚刚创建了一个 Session, 这是官网推荐的方式, 但是我觉得也可以直接用 request 形式, 细节请参考官方说明. 如果要获取网页返回的结果, 我们可以在 job() 中 return 个结果出来, 然后再在 finished, unfinished = await asyncio.wait(tasks) 收集完成的结果, 这里它会返回完成的和没完成的, 我们关心的都是完成的, 而且 await 也确实是等待都完成了才返回. 真正的结果被存放在了 result() 里面.

和多进程分布式爬虫对比

有了这些基础, 我们就可以来玩点高级的了, 之前我们用 multiprocessing 写过了一个简单的分布式爬虫, 现在我们就来拿过来 PK 一下 asyncio 的方法. 首先我们对比一下这次写的结构和上次写的简单分布式爬虫的区别. 分布式我们完全依赖的是 multiprocessing 这个模块. 使用 python 强大的并行处理运算来下载我们要处理的 urls, 然后解析网页也是一件耗时的事, 特别是网页量多的时候. 所以我们也将网页解析给并行了. 这样大大节省了下载和运算时间. 再看右边的这个 asyncio 的例子, 我们解析网页还是用的和 multiprocessing 那边一样的并行处理, 因为 asyncio 好像不支持解析网页的异步, 毕竟是计算密集型工序. 然后不一样的地方是, 我们在下载网页时, 不用 multiprocessing, 改用 asyncio, 用一个单线程的东西挑战多进程.

我们发现, 如果 Pool(n) 里面的这个 n 越大, 多进程才能越快, 但是 asyncio 却不会特别受进程数的影响. 一个单线程的东西居然战胜了多进程. 可见异步 asyncio 下载网页的重要性.

上面介绍的还只是 asyncio 的一小部分功能, 如果想了解更多有关于 asyncio 的使用方法, 请看到 Python 的官方介绍.

高级爬虫:让 Selenium 控制你的浏览器帮你爬

Selenium 是为了测试而出生的. 但是没想到到了爬虫的年代, 它摇身一变, 变成了爬虫的好工具. 让我试着用一句话来概括 Seleninm: 它能控制你的浏览器, 有模有样地学人类看网页.

安装

因为 Selenium 需要操控你的浏览器, 所以安装起来比传统的 Python 模块要多几步. 先在 terminal 或者 cmd 用 pip 安装 selenium.

要操控浏览器, 你就要有浏览器的 driver. Selenium 针对几个主流的浏览器都有 driver. 针对 Linux 和 MacOS.

Linux 和 MacOS 用户下载好之后, 请将下载好的geckodriver文件放在你的计算机的 /usr/bin 或 /usr/local/bin 目录. 并赋予执行权限, 不会放的, 请使用这条语句.

sudo cp 你的geckodriver位置 /usr/local/bin
sudo chmod +x /usr/local/bin/geckodriver

对于 Windows 用户, 官网上的说法, 好像没有提到要具体怎么操作, 我想, 应该是把 geckodriver 这个文件的位置加在 Windows 的环境变量中(PATH).

如果你安装有任何的问题, 请在它们的官网上查询解决方案.

偷懒的火狐浏览器插件

在这教你用火狐浏览器偷懒的一招, 因为暂时只有火狐上有这个插件. 插件 Katalon Recorder 下载的网址在这

这个插件能让你记录你使用浏览器的操作. 我以前玩网游, 为了偷懒, 用过一个叫按键精灵的东西, 帮我做了很多重复性的工作, 拯救了我的鼠标和键盘, 当然还有我的手指! 看着别人一直在点鼠标, 我心中暗爽~ 这个 Katalon Recorder 插件 + Selenium 就和按键精灵是一个意思. 记录你的操作, 然后你可以让电脑重复上千遍.

安装好火狐上的这个插件后, 打开它.

找到插件上的 record, 点它. 然后用火狐登录上 某个网站, 开始你的各种点击工作,

每当你点击的时候, 插件就会记录下你这些点击, 形成一些log. 最后神奇的事情将要发生. 你可以点击 Export 按钮, 观看到帮你生成的浏览记录代码!

虽然这个代码输出只有 Python2 版本的, 不过不影响. 我们直接将这些圈起来的代码复制. 这将会是 python 帮你执行的行为代码.

Python 控制浏览器

好了, 有了这些代码, 我们就能回到 Python. 开始写 Python 的代码了. 这里十分简单! 我将 selenium 绑定到 Chrome 上 webdriver.Chrome(). 你可以绑其它的浏览器.

from selenium import webdriver

driver = webdriver.Chrome()     # 打开 Chrome 浏览器

# 将刚刚复制的帖在这
driver.get("https://xxx.com/")
driver.find_element_by_xpath(u"//img[@alt='强化学习 (Reinforcement Learning)']").click()
driver.find_element_by_link_text("About").click()
driver.find_element_by_link_text(u"赞助").click()
driver.find_element_by_link_text(u"教程 ▾").click()
driver.find_element_by_link_text(u"数据处理 ▾").click()
driver.find_element_by_link_text(u"网页爬虫").click()

# 得到网页 html, 还能截图
html = driver.page_source       # get html
driver.get_screenshot_as_file("./img/sreenshot1.png")
driver.close()

我们能得到页面的 html code (driver.page_source), 就能基于这个 code 来爬取数据了.

不过每次都要看着浏览器执行这些操作, 有时候有点不方便. 我们可以让 selenium 不弹出浏览器窗口, 让它安静地执行操作. 在创建 driver 之前定义几个参数就能摆脱浏览器的身体了.

from selenium.webdriver.chrome.options import Options

chrome_options = Options()
chrome_options.add_argument("--headless")       # define headless

driver = webdriver.Chrome(chrome_options=chrome_options)
...

Selenium 能做的事还有很多, 比如填 Form 表单, 超控键盘等等. 这个教程不会细说了, 只是个入门, 如果你还想继续深入了解, 欢迎点进去他们的 Python 教学官网.

最后, Selenium 的优点我们都看出来了, 可以很方便的帮你模拟你的操作, 添加其它操作也是非常容易的, 但是也是有缺点的, 不是任何时候 selenium 都很好. 因为要打开浏览器, 加载更多东西, 它的执行速度肯定没有其它模块快. 所以如果你需要速度, 能不用 Selenium, 就不用吧.

高级爬虫: 高效无忧的 Scrapy 爬虫库

如果你想更高效的开发, 爬取网页, 记录数据库, Scrapy 是值得一推的. 它是一个爬虫的框架, 而不是一个简单的爬虫. 它整合了爬取, 处理数据, 存储数据的一条龙服务. 如果你只需要偶尔的一两次爬爬网页, 前面的教程已经够了, 如果你需要每天靠爬虫吃饭, Scrapy 还是有必要了解的

这里你写出一个 Scrapy 形式的爬虫, 带你入门 Scrapy, 但是 Scrapy 不仅仅只有爬虫, 你需要学习更多. 那学习 Scrapy 的地方, 当然是他们自家网站咯.

Scrapy 的优势

Scrapy 是一个整合了的爬虫框架, 有着非常健全的管理系统. 而且它也是分布式爬虫, 但是比我们之前写的那个分布式爬虫高级多了. 这里是 Scrapy 的框架示意图(图源). 它的管理体系非常复杂. 但是特别高效. 让你又刷网页, 又下载, 同时能处理数据. 简直千手观音呀.

而且做 Scrapy 的项目, 绝对不是只需要写一个脚本就能解决的. 为了把你带入门, 这次我们只写一个脚本, 只涉及里面的爬虫(spider)部分. 其他的部分你可以在这里深入学习.

Scrapy 爬虫

首先你得安装 Scrapy. 在 terminal 或者 cmd 使用 pip 安装就好.

如果安装遇到任何问题, 它们家的网站是个好去处.

我们导入 scrapy 模块, 并创建一个 spider 的 class. 并继承 scrapy.Spider, 一定还要给这个 spider 一个名字, 我就用 mofan 好了, 因为是爬 莫烦Python 的. 给定一些初始爬取的网页, 写在 start_urls 里. 这里特别要提的是: 之前我们用 python 的 set 来去除重复的 url, 在 scrapy 中, 这是不需要的, 因为它自动帮你去重.

import scrapy

class MofanSpider(scrapy.Spider):
    name = "mofan"
    start_urls = [
        'https://xxx.com/',
    ]
    # unseen = set()
    # seen = set()      # 我们不在需要 set 了, 它自动去重

接着我们还要定义这个 class 中的一个功能就能完事了. 我们使用 python 的 yield 来返回搜集到的数据 (为什么是yield? 因为在 scrapy 中也有异步处理, 加速整体效率). 这些 title 和 url 的数据, 我们都是用 scrapy 中抓取信息的方式.

class MofanSpider(scrapy.Spider):
    ...
    def parse(self, response):
        yield {     # return some results
            'title': response.css('h1::text').extract_first(default='Missing').strip().replace('"', ""),
            'url': response.url,
        }

        urls = response.css('a::attr(href)').re(r'^/.+?/$')     # find all sub urls
        for url in urls:
            yield response.follow(url, callback=self.parse)     # it will filter duplication automatically

然后在这个response网页中筛选 urls, 这里我们也不需要使用 urljoin() 这种功能给 url 改变形式. 它在 follow() 这一步会自动检测 url 的格式. (真是省心啊~), 然后对于每个找到的 url, 然后 yield 重新使用 self.parse() 来爬取, 这里又是自动去重! Scrapy 仿佛知道你最不想做什么, 它自动帮你都做好了. 开心~

最后需要运行的时候有点不同, 你需要在 terminal 或 cmd 中运行这个爬虫. 而且还能帮你保存刚刚 yield 的 {title:, url:} 的结果. runspider 5-2-scrapy.py 就是选择你要跑的这个 Python 文件.

$ scrapy runspider 5-2-scrapy.py -o res.json

-o res.json 这个 -o 就是输出的指令, 你可以在那个文件夹中找到一个名字叫 res.json 的文件, 里面存有所有找到的 {title:, url:}.

其实我们只做了 scrapy 中的爬虫, 一个正常的 scrapy 项目还包括有很多其他的内容(见下面). 这个教程就不会细说了, 因为学好 scrapy 还是比较麻烦的. 你可以在上面推荐给你的链接中, 继续深入学习 scrapy.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值