转自https://morvanzhou.github.io/tutorials/data-manipulation/scraping/

了解网页结构

学习爬虫, 首先要懂的是网页. 支撑起各种光鲜亮丽的网页的不是别的, 全都是一些代码. 这种代码我们称之为 HTML, HTML 是一种浏览器(Chrome, Safari, IE, Firefox等)看得懂的语言, 浏览器能将这种语言转换成我们用肉眼看到的网页. 所以 HTML 里面必定存在着很多规律, 我们的爬虫就能按照这样的规律来爬取你需要的信息.

其实除了 HTML, 一同构建多彩/多功能网页的组件还有 CSS 和 JavaScript. 但是这个简单的爬虫教程, 大部分时间会将会使用 HTML. CSS 和 JavaScript 会在后期简单介绍一下. 因为爬网页的时候多多少少还是要和 CSS JavaScript 打交道的.

虽然莫烦Python主打的是机器学习的教程. 但是这个爬虫教程适用于任何想学爬虫的朋友们. 从机器学习的角度看, 机器学习中的大量数据, 也是可以从这些网页中来, 使用爬虫来爬取各种网页上面的信息, 然后再放入各种机器学习的方法, 这样的应用途径正在越来越多被采用. 所以如果你的数据也是分散在各个网页中, 爬虫是你减少人力劳动的必修课.

网页基本组成部分

在真正进入爬虫之前, 我们先来做一下热身运动, 弄明白网页的基础, HTML 有哪些组成部分, 是怎么样运作的. 如果你已经非常熟悉网页的构造了, 欢迎直接跳过这一节, 进入下面的学习.

我制作了一个非常简易的网页, 给大家呈现以下最骨感的 HTML 结构. 如果你点开它, 呈现在你眼前的, 就是下面这张图的上半部分. 而下半部分就是我们网页背后的 HTML code.

想问我是如何看到 HTML 的 source code 的? 其实很简单, 在你的浏览器中 (我用的是 Google Chrome), 显示网页的地方, 点击鼠标右键, 大多数浏览器都会有类似这样一个选项 “View Page Source”. 点击它就能看到页面的源码了.


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

<head>
    <meta charset="UTF-8">
    <title>Scraping tutorial 1 | 莫烦Python</title>
    <link rel="icon" href="https://morvanzhou.github.io/static/img/description/tab_icon.png">
</head>

HTML 的第二大块是 body, 这个部分才是你看到的网页信息. 网页中的 heading, 视频, 图片和文字等都存放在这里. 这里的

tag 就是主标题, 我们看到呈现出来的效果就是大一号的文字. 里面的文字就是一个段落. 里面都是一些链接. 所以很多情况, 东西都是放在这些 tag 中的.
<body>
    <h1>爬虫测试1</h1>
    <p>
        这是一个在 <a href="https://morvanzhou.github.io/">莫烦Python</a>
        <a href="https://morvanzhou.github.io/tutorials/scraping">爬虫教程</a> 中的简单测试.
    </p>
</body>

爬虫想要做的就是根据这些 tag 来找到合适的信息.

用 Python 登录网页

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

from urllib.request import urlopen

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

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

<!DOCTYPE html>
<html lang="cn">
<head>
    <meta charset="UTF-8">
    <title>Scraping tutorial 1 | 莫烦Python</title>
    <link rel="icon" href="https://morvanzhou.github.io/static/img/description/tab_icon.png">
</head>
<body>
    <h1>爬虫测试1</h1>
    <p>
        这是一个在 <a href="https://morvanzhou.github.io/">莫烦Python</a>
        <a href="https://morvanzhou.github.io/tutorials/scraping">爬虫教程</a> 中的简单测试.
    </p>

</body>
</html>

匹配网页内容

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

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

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

# Page title is: Scraping tutorial 1 | 莫烦Python

如果想要找到中间的那个段落

, 我们使用下面方法, 因为这个段落在 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])

# Page paragraph is:
# 这是一个在 <a href="https://morvanzhou.github.io/">莫烦Python</a>
# <a href="https://morvanzhou.github.io/tutorials/scraping">爬虫教程</a> 中的简单测试.

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

res = re.findall(r'href="(.*?)"', html)
print("\nAll links: ", res)
# All links:
['https://morvanzhou.github.io/static/img/description/tab_icon.png',
'https://morvanzhou.github.io/',
'https://morvanzhou.github.io/tutorials/scraping']

BeautifulSoup 解析网页: 基础

我们了解了网页 (html) 的基本构架, 知道了爬网页就是在这个构架中找到需要的信息. 那么找到需要的信息时, BeautifulSoup 就是一个找信息好帮手. 它能帮你又快有准地找到信息. 大大简化了使用难度.
我们总结一下爬网页的流程, 让你对 BeautifulSoup 有一个更好的定位.

  1. 选着要爬的网址 (url)
  2. 使用 python 登录上这个网址 (urlopen等)
  3. 读取网页信息 (read() 出来)
  4. 将读取的信息放入 BeautifulSoup
  5. 使用 BeautifulSoup 选取 tag 信息等 (代替正则表达式)
    初学的时候总是搞不懂这些包是干什么的, 现在你就能理解这个 BeautifulSoup 到底是干什么的了.

安装

等什么, 知道 BeautifulSoup 这么方便, 我们就赶紧装一个吧. 安装的步骤很简单, 用 pip 就能轻松安装.

# Python 2+
pip install beautifulsoup4

# Python 3+
pip3 install beautifulsoup4

注意在名字后面还有个 “4”, 可能是代表第4版吧. 如果你在安装的时候遇到任何问题, 请参考他们官网上的解决方案.

简单实用方法

这次我们还是爬一爬上次爬的那个基本网页. BeautifulSoup 使用起来非常简单, 我们先按常规读取网页.

from bs4 import BeautifulSoup
from urllib.request import urlopen

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

回顾一下, 每张网页中, 都有两大块, 一个是 , 一个是 , 我们等会用 BeautifulSoup 来找到 body 中的段落

和所有链接 .

<!DOCTYPE html>
<html lang="cn">
<head>
    <meta charset="UTF-8">
    <title>Scraping tutorial 1 | 莫烦Python</title>
    <link rel="icon" href="https://morvanzhou.github.io/static/img/description/tab_icon.png">
</head>
<body>
    <h1>爬虫测试1</h1>
    <p>
        这是一个在 <a href="https://morvanzhou.github.io/">莫烦Python</a>
        <a href="https://morvanzhou.github.io/tutorials/scraping">爬虫教程</a> 中的简单测试.
    </p>

</body>
</html>

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

标题, 可以就直接 soup.h1.

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

""" <h1>爬虫测试1</h1> """

print('\n', soup.p)

""" <p> 这是一个在 <a href="https://morvanzhou.github.io/">莫烦Python</a> <a href="https://morvanzhou.github.io/tutorials/scraping">爬虫教程</a> 中的简单测试. </p> """

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

""" <a href="https://morvanzhou.github.io/tutorials/scraping">爬虫教程</a> """

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

# ['https://morvanzhou.github.io/', 'https://morvanzhou.github.io/tutorials/scraping']

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

BeautifulSoup 解析网页: CSS

BeautifulSoup 十分好用, 能快速定位到你需要的网页信息. 上次 我们学着使用了 BeautifulSoup, 这次我们将会了解它更强大的功能, 使用 CSS 的 Class 来选择内容.

什么是 CSS

提到这个, 我们肯定需要知道什么是 CSS. HTML 和 CSS 是一对好搭档, 他们共同组成了当今的众多网页. 如果这个世界上没有 CSS, 你看到的所有网页可能都长得像这样. 特别”骨感”!

如果有 CSS, 你的网页就变得丰富多彩起来. 文字有了颜色, 字***置也多样了, 图片也有规则了.

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

CSS 的 Class

这条规则就是 CSS 的 Class, CSS 在装饰每一个网页部件的时候, 都会给它一个名字. 而且一个类型的部件, 名字都可以一样. 比如我们这个练习网页. 里面的字体/背景颜色, 字体大小, 都是由 CSS 来掌控的.
而 CSS 的代码, 可能就会放在这个网页的 中. 我们先使用 Python 读取这个页面.

from bs4 import BeautifulSoup
from urllib.request import urlopen

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

在 中, 你会发现有这样一些东西被放在

<head>
    ...
    <style>
    .jan {
        background-color: yellow;
    }
    ...
    .month {
        color: red;
    }
    </style>
</head>

<body>
...
<ul>
    <li class="month">一月</li>
    <ul class="jan">
        <li>一月一号</li>
        <li>一月二号</li>
        <li>一月三号</li>
    </ul>
    ...
</ul>
</body>

这样, 我们就知道, 有时候, 网页中, 这种 class 归类一些组件还是很有用的. 比如我就想找 jan 下面的这些

  • . 我就能通过寻找 class=”jan” 找到它们. BeautifulSoup 就能这么干.
  • 按 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 的信息. 然后在

    • 下面继续找
      • 内部的
      • 信息. 这样一层层嵌套的信息, 非常容易找到.
    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 中也能嵌入正则表达式, 让 BeautifulSoup 更为强大. 怎么用, 我们就接着往下看啦.

    BeautifulSoup 解析网页: 正则表达

    正则表达式, 是处理文本信息的重要工具, 除了 Python, 在其他的程序语言中, 也有十分重要的地位. 如果将正则表达式 + BeautifulSoup, 岂不是完美中的完美, 哈哈. 我们今天就来看看, 在 BeautifulSoup 中如何使用正则表达式, 获取更有难度的信息.

    正则表达式

    正则表达式很厉害, 它能用简单的规则匹配到多样化的文本信息. 在做爬虫教程之前, 我特地做了一个正则表达式的教程, 为爬虫做铺垫. 所以有兴趣了解使用正则表达式的朋友, 都可以看看这个非常全的正则教程.

    这次的教程有一些表格形式的 HTML, 在表格中, 有一些信息的格式类似, 我们先用 BeautifulSoup 筛选一些, 然后完全可以用正则给匹配出来. 比如你想下载这个页面的图片, 我们就可以将图片形式的 url 个匹配出来. 之后再下载就简单多了.

    正则匹配

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

    from bs4 import BeautifulSoup
    from urllib.request import urlopen
    import re
    
    # if has Chinese, apply decode()
    html = urlopen("https://morvanzhou.github.io/static/scraping/table.html").read().decode('utf-8')

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

    <td>
        <img src="https://morvanzhou.github.io/static/img/course_cover/tf.jpg">
    </td>

    所以, 我们可以用 soup 将这些 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://morvanzhou.github.io/static/img/course_cover/tf.jpg https://morvanzhou.github.io/static/img/course_cover/rl.jpg https://morvanzhou.github.io/static/img/course_cover/scraping.jpg """

    所以, 我们可以用 soup 将这些 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://morvanzhou.github.io/static/img/course_cover/tf.jpg https://morvanzhou.github.io/static/img/course_cover/rl.jpg https://morvanzhou.github.io/static/img/course_cover/scraping.jpg """

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

    course_links = soup.find_all('a', {
        'href': re.compile('https://morvan.*')})
    for link in course_links:
        print(link['href'])
    
    """ https://morvanzhou.github.io/ https://morvanzhou.github.io/tutorials/scraping https://morvanzhou.github.io/tutorials/machine-learning/tensorflow/ https://morvanzhou.github.io/tutorials/machine-learning/reinforcement-learning/ https://morvanzhou.github.io/tutorials/data-manipulation/scraping/ """

    小练习: 爬百度百科

    有了前面几节内容了练习, 我们现在完全有能力爬出你想要的信息了. 看吧, 我说很简单的, 只要你用 Python 打开网页, 用 BeautifulSoup 找准地方, 然后这样循环往复, 就叫做爬虫了. 哈哈. 被我抽象得不行了. 不过说到底, 爬虫就这么回事. 今天我们就来爬一爬百度百科, 让我们的爬虫从 “网络爬虫” 这一页开始爬, 然后在页面中寻找其他页面的信息, 然后爬去其他页面, 然后循环这么做, 看看最后我们的爬虫到底爬去了哪.

    百度百科

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

    这个练习看起来挺没意义的, 但是对于了解爬虫, 还是挺有意义的. 用最简单的规律解释了爬虫的真谛.

    观看规律

    这个爬虫说实在的, 并不难, 只有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])
    
    # 网络爬虫     url:  /item/%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB/5162711

    接下来我们开始在这个网页上找所有符合要求的 /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) # ['/item/%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB/5162711', '/item/%E4%B8%8B%E8%BD%BD%E8%80%85']

    有了这套体系, 我们就能把它放在一个 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()

    这样我们就能观看我们的爬虫现在爬去了哪? 是不是爬到了和 “网页爬虫” 起始页完全不相关的地方去了.

    0 网络爬虫     url:  /item/%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB/5162711
    1 路由器     url:  /item/%E8%B7%AF%E7%94%B1%E5%99%A8
    2 服务等级     url:  /item/%E6%9C%8D%E5%8A%A1%E7%AD%89%E7%BA%A7
    ...
    17 呼损率     url:  /item/%E5%91%BC%E6%8D%9F%E7%8E%87
    18 服务等级     url:  /item/%E6%9C%8D%E5%8A%A1%E7%AD%89%E7%BA%A7
    19 呼损率     url:  /item/%E5%91%BC%E6%8D%9F%E7%8E%87

    多功能的 Requests

    之前 我们通常使用 Python 的自带模块 urllib, 来提交网页请求. 这个模块能满足我们大部分的需求, 但是为了满足你日益膨胀的其他需求, 比如向网页发送信息, 上传图片等等, 我们还有一个伟大的 Python 外部模块 requests, 来有效的处理这些问题.

    获取网页的方式

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

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

    post
    账号登录
    搜索内容
    上传图片
    上传文件
    往服务器传数据 等
    get
    正常打开网页
    不往服务器传数据
    这样看来, 很多网页使用 get 就可以了, 比如 莫烦Python 里的所有页面, 都是只是 get 发送请求. 而 post, 我们则是给服务器发送个性化请求, 比如将你的账号密码传给服务器, 让它给你返回一个含有你个人信息的 HTML.

    从主动和被动的角度来说, post 中文是发送, 比较主动, 你控制了服务器返回的内容. 而 get 中文是取得, 是被动的, 你没有发送给服务器个性化的信息, 它不会根据你个性化的信息返回不一样的 HTML.
    拿登录账号举个例子. 比如我想登录知乎账号(https://www.zhihu.com/people/morvan/activities, 在登录前, 我看到的页面是这样.

    而如果我登录了(登录一般用 post), 我再次登录这个网页 https://www.zhihu.com/people/morvan/activities, 现在显示的 HTML 界面就和我上面那个不一样了, 这就是使用 post 给网页输入你的个性化信息后 (账号密码), 得到的服务器返回的个性化网页. 每个人的账号密码的到的页面都不一样.

    而今天要说的 requests 模块就是干这些的. 它有着各种不同的请求方法, 而且用起来很方便.

    安装 requests

    Requests 是一个 Python 的外部模块, 我们需要手动安装它. 简单的方法, 在你的 terminal 或者是 cmd, 使用 pip 安装就好了.

    # python 2+
    pip install requests
    
    # python 3+
    pip3 install requests

    requests get 请求

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

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

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

    import requests
    import webbrowser
    param = {
        "wd": "莫烦Python"}  # 搜索的信息
    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

    这时, python 会弹出一个浏览器界面, 然后你看到的, 就是 “莫烦Python” 的搜索结果了.

    requests post 请求

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

    比如我在这里填上自己的姓名, 当我点 “submit” 的时候, 这个姓名(Morvan, Zhou) 就会被提交给服务器, 然后它会根据提交的姓名返回这个网页.

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

    首先我们调出浏览器的 inspect (右键点击 inspect, 中文是检查还是什么来着). 然后发现我们填入姓名的地方原来是在一个 里面.

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

    这些数据包括了:

    Request URL (post 要用的 URL)
    Request Method (post)
    Form Data (post 去的信息)
    有了这些记录, 我们就能开始写 Python 来模拟这一次提交 post 了. 根据 ‘firstname’ 和 ‘lastname’, 也就是上图里面的 Form data, 组织成一个 python 字典. 让后把这个字典传入 requests.post(), 注意, 这里的 post 里面的 url, 不是我们填表时的 url (http://pythonscraping.com/pages/files/form.html), 而是要把 Form 信息提交去的那个网页, 也就是上图中查看到的 Request URL (http://pythonscraping.com/files/processing.php).

    data = {
        'firstname': '莫烦', 'lastname': '周'}
    r = requests.post('http://pythonscraping.com/files/processing.php', data=data)
    print(r.text)
    
    # Hello there, 莫烦 周!

    通过这个练习, 我们对 HTML 中的 Form 有了理解, 学会了怎么样使用 python 来提交 Form, 登录上提交后的页面.

    上传图片

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

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

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

    file = {
        'uploadFile': open('./image.png', 'rb')}
    r = requests.post('http://pythonscraping.com/files/processing2.php', files=file)
    print(r.text)
    
    # The file image.png has been uploaded.

    登录

    用 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('http://pythonscraping.com/pages/cookies/welcome.php', data=payload)
    print(r.cookies.get_dict())
    
    # {
        'username': 'Morvan', 'loggedin': '1'}
    
    
    r = requests.get('http://pythonscraping.com/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 来 post 和 get. 而且这次 get 的时候, 我们并没有传入 cookies. 但是实际上 session 内部就已经有了之前的 cookies 了.

    session = requests.Session()
    payload = {
        'username': 'Morvan', 'password': 'password'}
    r = session.post('http://pythonscraping.com/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!

    下载文件

    从网上下载文件的时候你肯定很开心, 比如说什么电影, pdf, 音乐等. 我们使用爬虫, python 一样也可以做到. 而且做得途径还有很多. 今天我们就来探讨有哪些可行的途径, 之后我们还能用这些途径来做一个小实战, 下载国家地理杂志上的美图.

    下载之前

    在下载之前, 我们的弄清楚怎么样下载. 打个比方, 以前有朋友留言说我的教程太多, 不知道从何学起, 我专门制作了一张学习流程图, 这张莫烦Python的个性化学习路线应该也拯救了无数迷途少年吧. 哈哈哈, 今天我们来爬这张图, 还有下载这张图.

    想下这张图, 我们首先要到这张图所在的网页. 在这个网页中找到这张图的位置, 并右键 inspect, 找到它在 HTML 中的信息.

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

    src="/static/img/description/learning_step_flowchart.png"

    补全后的网址是:

    https://morvanzhou.github.io/static/img/description/learning_step_flowchart.png

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

    import os
    os.makedirs('./img/', exist_ok=True)
    
    IMAGE_URL = "https://morvanzhou.github.io/static/img/description/learning_step_flowchart.png"

    使用 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)

    小练习: 下载美图

    学会爬虫, 关键在于练习, 见多识广, 见多了网页的构造, 才知道怎么爬. 今天我们就来一个小实战, 结合之前学习的 requests 访问和 下载功能, 还有 BeautifulSoup, 来下载一些国家地理杂志的美图.

    找到图片位置

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

    找到这张图片的所在位置, 对比这类型的图片, 找到一种手段来筛选这些图片. 发现他们都存在于 img_list 的这种

    • 中.

      而图片地址都是在 中.
    <img src="http://image.nationalgeographic.com.cn/2017/1228/20171228030617696.jpg">

    现在我们有了思路, 先找带有 img_list 的这种

    • , 然后在
      • 里面找 .

    下载图片

    有了思路, 现在我们就用 python 来下图吧. import BeautifulSoup 和 requests. 定义爬取的 url.

    from bs4 import BeautifulSoup
    import requests
    
    URL = "http://www.nationalgeographic.com.cn/animals/"

    用 BeautifulSoup 找到带有 img_list 的这种

    • ,
    html = requests.get(URL).text
    soup = BeautifulSoup(html, 'lxml')
    img_ul = soup.find_all('ul', {
        "class": "img_list"})

    从 ul 中找到所有的 , 然后提取 的 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)
    
    """ Saved 20171227102206573.jpg ... Saved 20171214020322682.jpg """

    我从下载好的照片中抽了一张出来, 哈哈, 是张河马.

    加速爬虫: 多进程分布式

    当你看到这里的时候, 说明你已经不满足于自己的爬虫速度, 你想要最求更快, 更便捷的爬虫方法. 你常常会听到用爬虫的人说分布式爬虫. 这就是为了体现便捷和效率而出现的方法. 这一节内容, 我们简单地介绍一下我使用的分布式爬虫方法, 并且用 python 的 multiprocessing 模块编写一个分布式爬虫.

    什么是分布式爬虫

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

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

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

    我们的分布式爬虫

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

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

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

    首先 import 全部要用的模块, 并规定一个主页. 注意, 我用这份代码测试我内网的网站(速度不受外网影响) 所以使用的 base_url 是 “http://127.0.0.1:4000/”, 如果你要爬 莫烦Python, 你的 base_url 要是 “https://morvanzhou.github.io/” (***会受外网影响).

    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://morvanzhou.github.io/'

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

    测试普通爬法

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

    特别注意: 任何网站都是有一个服务器压力的, 如果你爬的过于频繁, 特别是使用多进程爬取或异步爬取, 一次性提交请求给服务器太多次, 这将可能会使得服务器瘫痪, 你可能再也看不到莫烦 Python 了. 所以为了安全起见, 我限制了爬取数量(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

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

    测试分布式爬法

    还是上一个 while 循环, 首先我们创建一个进程池(Pool). 不太懂进程池的朋友看过来. 然后我们修改得到 htmls 和 results 的两句代码. 其他都不变, 只将这两个功能给并行了. 我在这里写的都是简化代码, 你可以在这里 看到完整代码.

    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倍. 而且我还不是在外网测试的. 如果在外网, 爬取一张网页的时间更长, 使用多进程会更加有效率, 节省的时间更多.

    看到这里, 你一定觉得多线程是爬虫的救星. 其实不然, 要不然我们的教程为什么还能继续. 哈哈. 下一节, 我们会讲到比多进程更加厉害的一种方法. 叫做异步爬取 (asyncio 模块).

    加速爬虫: 异步加载 Asyncio

    之前我一直在想如何用 multiprocessing 或者 threading 加速我的爬虫, 也做过了一些小实验, 确实, 我们看到了不小的效率提升. 但是当我更加深入的时候, 我发现, Python 还提供了一个有力的工具, 叫做 asyncio. 这是一个仅仅使用单线程, 就能达到多线程/进程的效果的工具. 它的原理, 简单说就是: 在单线程里使用异步计算, 下载网页的时候和处理网页的时候是不连续的, 更有效利用了等待下载的这段时间.

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

    如果换一张 Python 自家解释 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 版的. 这些代码我都会放在我的 github 中, 如果想一次性看全部, 请来这里.

    # 不是异步的
    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 1 和 job 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://morvanzhou.github.io/'
    
    
    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)
    
    """ https://morvanzhou.github.io/ https://morvanzhou.github.io/ Normal total time: 0.3869960308074951 """

    用 requests 用久了以后, 这样的代码真是信手拈来. 很好, 我们打开 莫烦 Python 的首页两次只花了 0.38秒. 然后我们在用 aiohttp 来实现一样的功能. 结果 asyncio 的方式只用了 0.11秒! 大获全胜.

    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)
    
    """ ['https://morvanzhou.github.io/', 'https://morvanzhou.github.io/'] Async total time: 0.11447715759277344 """
    

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

    和多进程分布式爬虫对比

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

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

    具体的代码可以在这里详细观看, 需要注意的是, 我使用的内网进行测试(外网的***变动太大), 在下载网页的地方, 我使用 sleep(0.1) 的功能模拟了网页下载的延迟. 一共下载了我 莫烦 Python 的快400个网页. 因为代码表达的内容我已经用上图展示给大家了, 每一个代码都有50-60行, 我就不粘贴在这里了. 具体的结果, 我们可以总结一下.

    Number of Process   Multiprocessing     Asyncio
        2                   25.5s           7.5s
        4                   15.4s           7.0s
        8                   11.5s           7.2s

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

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

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

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

    那么你什么时候会要用到 Selenium 呢? 当你:

    • 发现用普通方法爬不到想要的内容
    • 网站跟你玩”捉迷藏”, 太多 JavaScript 内容
    • 需要像人一样浏览的爬虫

    安装 Selenium

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

    # python 2+
    pip install selenium
    
    # python 3+
    pip3 install selenium

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

    • Chrome driver
    • Edge driver
    • Firefox driver
    • Safari driver
      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, 点它. 然后用火狐登录上 莫烦Python, 开始你的各种点击工作, 比如我的一连串操作是 (强化学习教程->About页面->赞助页面->教程->数据处理->网页爬虫)

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

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

    Python 控制浏览器

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

    from selenium import webdriver
    
    driver = webdriver.Chrome()     # 打开 Chrome 浏览器
    
    # 将刚刚复制的帖在这
    driver.get("https://morvanzhou.github.io/")
    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)
    ...

    最后同样再截一张图, 证明 driver 真的爬到了这个 “网页爬虫” 教程页面. 不过因为没有出现实体的浏览器, 这个页面大小和上面的图片还是有点差别的.

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

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

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

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

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

    Scrapy 的优势

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

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

    Scrapy 爬虫

    好了, 我们开始今天的简单 Scrapy 教程吧. 首先你得安装 Scrapy. 在 terminal 或者 cmd 使用 pip 安装就好.

    # python 2+
    pip install scrapy
    
    # python 3+
    pip3 install scrapy

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

    我们之前有做过爬取 莫烦Python 全网的信息. 用多进程 和异步爬取都做过. 这次, 我们就用 Scrapy 来实现这样的一个爬虫. 剧透一下, 做前两个的时候, 代码行数差不多都是 50+ 行, 但是 scrapy 只需要用 20+ 行代码就解决的上面的事. 哈哈, 功能强大吧.

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

    import scrapy
    
    class MofanSpider(scrapy.Spider):
        name = "mofan"
        start_urls = [
            'https://morvanzhou.github.io/',
        ]
        # 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.

    tutorial/
        scrapy.cfg            # deploy configuration file
    
        tutorial/             # project's Python module, you'll import your code from here
            __init__.py
    
            items.py          # project items definition file
    
            middlewares.py    # project middlewares file
    
            pipelines.py      # project pipelines file
    
            settings.py       # project settings file
    
            spiders/          # a directory where you'll later put your spiders
                __init__.py