在一个网页当中,除了可阅读的文字信息之外,还包括很多超链接。网络爬虫便是通过超链接不断再网络上获取其他页面,进而获取更多信息。

合法性

1. 网络爬虫领域目前还属于拓荒阶段,虽然互联网世界已经通过自己的游戏规则建立起一定的道德规范(Robots协议,全称是“网络爬虫排除标准”),但法律部分还在建立和完善中,也就是说,现在这个领域暂时还是灰色地带。

2. “法不禁止即为许可”,如果爬虫就像浏览器一样获取的是前端显示的数据(网页上的公开信息)而不是网站后台的私密敏感信息,就不太担心法律法规的约束,因为目前大数据产业链的发展速度远远超过了法律的完善程度。

3. 在爬取网站的时候,需要限制自己的爬虫遵守Robots协议,同时控制网络爬虫程序的抓取数据的速度;在使用数据的时候,必须要尊重网站的知识产权(从Web 2.0时代开始,虽然Web上的数据很多都是由用户提供的,但是网站平台是投入了运营成本的,当用户在注册和发布内容时,平台通常就已经获得了对数据的所有权、使用权和分发权)。如果违反了这些规定,在打官司的时候败诉几率相当高。

Robots.txt文件

大多数网站都会定义一个Robots.txt文件对爬虫做一些限制,例如淘宝的Robots.txt文件:

User-agent:  Baiduspider
Allow:  /article
Allow:  /oshtml
Disallow:  /product/
Disallow:  /

User-Agent:  Googlebot
Allow:  /article
Allow:  /oshtml
Allow:  /product
Allow:  /spu
Allow:  /dianpu
Allow:  /oversea
Allow:  /list
Disallow:  /

User-agent:  Bingbot
Allow:  /article
Allow:  /oshtml
Allow:  /product
Allow:  /spu
Allow:  /dianpu
Allow:  /oversea
Allow:  /list
Disallow:  /

User-Agent:  360Spider
Allow:  /article
Allow:  /oshtml
Disallow:  /

User-Agent:  Yisouspider
Allow:  /article
Allow:  /oshtml
Disallow:  /

User-Agent:  Sogouspider
Allow:  /article
Allow:  /oshtml
Allow:  /product
Disallow:  /

User-Agent:  Yahoo!  Slurp
Allow:  /product
Allow:  /spu
Allow:  /dianpu
Allow:  /oversea
Allow:  /list
Disallow:  /

User-Agent:  *
Disallow:  /

在第一项中可以看到,淘宝对百度设置了"Disallow: / "限制百度除了规定的页面之外不允许爬取,所以当我们用百度搜索淘宝时,会出现如下的界面:

image-20211210093538837

所以至少在明面上,百度遵守了淘宝的Robots.txt协议

相关工具

HTTP协议

HTTP协议真是个老生常谈的东西了,简单来说,网页上的内容都是通过浏览器执行HTML语言所得到的,而传输HTML数据的协议便是HTTP协议。HTTP和其他很多应用级协议一样是构建在TCP(传输控制协议)之上的,它利用了TCP提供的可靠的传输服务实现了Web应用中的数据交换。

Chrome Developer Tools

谷歌浏览器内置的开发者工具,我们只需要按下F12就会出现这个工具

image-20211210094234838

Postman

一个功能强大的网页调试与RESTful请求工具

image-20211210100631716

HTTPie

命令行HTTP客户端

pip3 isntall httpie
http --header https://blog.ikedong.cn
HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=UTF-8
Date: Fri, 10 Dec 2021 02:15:47 GMT
Server: nginx
Set-Cookie: wp-editormd-lang=zh-CN; path=/
Strict-Transport-Security: max-age=31536000
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Frame-Options: SAMEORIGIN

builtwith库

识别网站所用技术的工具

pip3 install builtwith
python
>>> import builtwith
>>> import ssl
>>> ssl._create_default_https_context = ssl._create_unverified_context
>>> builtwith.parse('https://blog.ikedong.cn')
{'web-servers': ['Nginx'], 'cms': ['WordPress'], 'programming-languages': ['PHP'], 'blogs': ['PHP', 'WordPress'], 'javascript-frameworks': ['jQuery']}

python-whois库

查询网站所有者的工具

pip3 install python-whois
python
import whois
>>> whois.whois('blog.ikedong.cn')
{'domain_name': 'ikedong.cn', 'registrar': '广州云讯信息科技有限公司', 'creation_date': datetime.datetime(2021, 10, 5, 3, 15, 42), 'expiration_date': datetime.datetime(2022, 10, 5, 3, 15, 42), 'name_servers': ['happy.dnspod.net', 'tie.dnspod.net'], 'status': 'ok', 'emails': None, 'dnssec': 'unsigned', 'name': '该域名已采取WHOIS隐私保护服务'}

robotparser模块

解析robots.txt的工具

python
>>> from urllib import robotparser
>>> parser = robotparser.RobotFileParser()
>>> parser.set_url('https://www.taobao.com/robots.txt')
>>> parser.read()
>>> parser.can_fetch('Baiduspider', 'http://www.taobao.com/article')
False
>>> parser.can_fetch('Baiduspider', 'http://www.taobao.com/product')
False
# 看来现在淘宝似乎什么也不允许爬取了

DEMO

一个简单的爬虫包含数据采集(网页下载)、数据处理(网页解析)和数据存储(将有用的信息持久化)三个部分的内容

一般来说,爬虫的工作流程包括以下几个步骤:

1. 设定抓取目标(种子页面/起始页面)并获取网页。
2. 当服务器无法访问时,按照指定的重试次数尝试重新下载页面。
3. 在需要的时候设置用户代理或隐藏真实IP,否则可能无法访问页面。
4. 对获取的页面进行必要的解码操作然后抓取出需要的信息。
5. 在获取的页面中通过某种方式(如正则表达式)抽取出页面中的链接信息。
6. 对链接进行进一步的处理(获取页面并重复上面的动作)。
7. 将有用的信息进行持久化以备后续的处理。
import re
from collections import deque
from urllib.parse import urljoin

import requests

# 编写正则表达式
LI_A_PATTERN = re.compile(r'<li class="item">.*?</li>') #匹配li item标签
A_TEXT_PATTERN = re.compile(r'<a\s+[^>]*?>(.*?)</a>') # 匹配任意<a/>标签
A_HREF_PATTERN = re.compile(r'<a\s+[^>]*?href="(.*?)"\s*[^>]*?>') # 匹配有链接的<a/>标签

def decode_page(page_bytes, charsets):
    # 通过指定的字符集对页面解码
    for charset in charsets:
        try:
            return page_bytes.decode(charset)
        except UnicodeDecodeError:
            pass

def get_matched_parts(content_string, pattern):
    # 提取所有与正则表达式匹配的内容
    return pattern.findall(content_string, re.I)\
        if content_string else []

def get_matched_part(content_string, pattern, group_no=1):
    # 提取与正则表达式匹配的内容
    match = pattern.search(content_string)
    if match:
        return match.group(group_no)

def get_page_html(seed_url, *, charsets=('utf-8', )):
    # 获取页面的HTML代码
    resp = requests.get(seed_url)
    if resp.status_code == 200:
        return decode_page(resp.content, charsets)

def repair_incorrect_href(current_url, href):
    # 修正获取的href属性
    if href.startswith('//'):
        href = urljoin('http://', href)
    elif href.startswith('/'):
        href = urljoin(current_url, href)
    return href if href.startswith('http') else ''

def start_crawl(seed_url, pattern, *, max_depth=-1):
    # 开始爬取数据
    new_urls, visited_urls = deque(),set()
    new_urls.append((seed_url, 0))
    while new_urls:
        current_url, depth = new_urls.popleft()
        if depth != max_depth:
            page_html = get_page_html(current_url, charsets=('utf-8', 'gbk'))
            contents = get_matched_parts(page_html, pattern)
            for content in contents:
                text = get_matched_part(content, A_TEXT_PATTERN)
                href = get_matched_part(content, A_HREF_PATTERN)
                if href:
                    href = repair_incorrect_href(seed_url, href)
                print(text, href)
                if href and href not in visited_urls:
                    new_urls.append((href, depth + 1)) 

def main():
    start_crawl(
        seed_url='http://sports.sohu.com/nba_a.shtml',
        pattern=LI_A_PATTERN,
        max_depth=2
    )

if __name__ == '__main__':
    main()

注意事项

  1. 上面的代码使用了requests三方库来获取网络资源,这是一个非常优质的三方库,关于它的用法可以参考它的官方文档

  2. 上面的代码中使用了双端队列(deque)来保存待爬取的URL。双端队列相当于是使用链式存储结构的list,在双端队列的头尾添加和删除元素性能都比较好,刚好可以用来构造一个FIFO(先进先出)的队列结构。

  3. 处理相对路径。有的时候我们从页面中获取的链接不是一个完整的绝对链接而是一个相对链接,这种情况下需要将其与URL前缀进行拼接(urllib.parse中的urljoin()函数可以完成此项操作)。

  4. 设置代理服务。有些网站会限制访问的区域(例如美国的Netflix屏蔽了很多国家的访问),有些爬虫需要隐藏自己的身份,在这种情况下可以设置使用代理服务器,代理服务器有免费的服务器和付费的商业服务器,但后者稳定性和可用性都更好,强烈建议在商业项目中使用付费的商业代理服务器。如果使用requests三方库,可以在请求方法中添加proxies参数来指定代理服务器;如果使用标准库,可以通过修改urllib.request中的ProxyHandler来为请求设置代理服务器。

  5. 限制下载速度。如果我们的爬虫获取网页的速度过快,可能就会面临被封禁或者产生“损害动产”的风险(这个可能会导致吃官司且败诉),可以在两次获取页面数据之间添加延时从而对爬虫进行限速。

  6. 避免爬虫陷阱。有些网站会动态生成页面内容,这会导致产生无限多的页面(例如在线万年历通常会有无穷无尽的链接)。可以通过记录到达当前页面经过了多少个链接(链接深度)来解决该问题,当达到事先设定的最大深度时,爬虫就不再像队列中添加该网页中的链接了。

  7. 避开蜜罐链接。网站上的有些链接是浏览器中不可见的,这种链接通常是故意诱使爬虫去访问的蜜罐,一旦访问了这些链接,服务器就会判定请求是来自于爬虫的,这样可能会导致被服务器封禁IP地址。如何避开这些蜜罐链接我们在后面为大家进行讲解。

  8. SSL相关问题。如果使用标准库的urlopen打开一个HTTPS链接时会验证一次SSL证书,如果不做出处理会产生错误提示“SSL: CERTIFICATE_VERIFY_FAILED”,可以通过以下两种方式加以解决:

    • 使用未经验证的上下文

      import ssl
      request = urllib.request.Request(url='...', headers={...})
      context = ssl._create_unverified_context()
      web_page = urllib.request.urlopen(request, context=context)

    • 设置全局性取消证书验证

      import ssl
      ssl._create_default_https_context = ssl._create_unverified_context