网络爬虫和相关工具

网络爬虫

网络爬虫(web crawler),以前经常称之为网络蜘蛛(spider),是按照一定的规则自动浏览万维网并获取信息的机器人程序(或脚本),曾经被广泛的应用于互联网搜索引擎。使用过互联网和浏览器的人都知道,网页中除了供用户阅读的文字信息之外,还包含一些超链接。网络爬虫系统正是通过网页中的超链接信息不断获得网络上的其它页面。正因如此,网络数据采集的过程就像一个爬虫或者蜘蛛在网络上漫游,所以才被形象的称为网络爬虫或者网络蜘蛛。

爬虫的应用领域

在理想的状态下,所有ICP(Internet Content Provider)都应该为自己的网站提供API接口来共享它们允许其他程序获取的数据,在这种情况下爬虫就不是必需品,国内比较有名的电商平台(如淘宝、京东等)、社交平台(如腾讯微博等)等网站都提供了自己的Open API,但是这类Open API通常会对可以抓取的数据以及抓取数据的频率进行限制。对于大多数的公司而言,及时的获取行业相关数据是企业生存的重要环节之一,然而大部分企业在行业数据方面的匮乏是其与生俱来的短板,合理的利用爬虫来获取数据并从中提取出有商业价值的信息是至关重要的。当然爬虫还有很多重要的应用领域,下面列举了其中的一部分:

  1. 搜索引擎
  2. 新闻聚合
  3. 社交应用
  4. 舆情监控
  5. 行业数据
合法性和背景研究
爬虫合法性探讨
  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: *
Disallow: /

注意上面robots.txt第一段的最后一行,通过设置“Disallow: /”禁止百度爬虫访问除了“Allow”规定页面外的其他所有页面。因此当你在百度搜索“淘宝”的时候,搜索结果下方会出现:“由于该网站的robots.txt文件存在限制指令(限制搜索引擎抓取),系统无法提供该页面的内容描述”。百度作为一个搜索引擎,至少在表面上遵守了淘宝网的robots.txt协议,所以用户不能从百度上搜索到淘宝内部的产品信息。


百度搜索淘宝界面
相关工具介绍
HTTP协议

HTTP中文名叫超文本传输协议,因为我们在网页上看到的内容通常是浏览器执行HTML语言得到的结果,而HTTP就是传输HTML数据的协议。HTTP和其他很多应用级协议一样是构建在TCP(传输控制协议)之上的,它利用了TCP提供的可靠的传输服务实现了Web应用中的数据交换。按照维基百科上的介绍,设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法,也就是说这个协议是浏览器和Web服务器之间传输的数据的载体。

HTTPS

HTTPS是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层。HTTPS的安全基础是SSL,因此通过它传输的内容都是经过SSL加密的,它的主要作用可以分为两种。

  1. 建立一个信息安全通道来保证数据传输的安全。
  2. 确认网站的真实性,凡是使用HTTPS的网站,都可以通过点击浏览器地址栏的锁头标示来查看网站认证之后的真实信息,也可以通过CA机构颁发的安全签章来查询。
HTTP请求

HTTP请求(请求行+请求头+[请求体]):

HTTP请求

HTTP请求报文
  1. 请求行

请求行 = 请求方法+请求URL+HTTP协议及其版本

请求方式
常见的请求方法有两种POST和GET。
GET和POST请求方法有如下区别:

  • GET请求中的参数包含在URL里面,数据可以在URL中看到,而POST请求的URL不会包含这些数据,数据都是通过表单形式传输的,包含在请求体中,所以POST请求比GET请求个安全。
  • GET请求提交的数据最多只有1024字节,而POST请求没有限制。一般情况下,GET请求用于查询,POST请求用于增、删、改。

请求URL
请求的网址,即统一资源定位符URL,可以唯一确认我们想请求的资源

  1. 请求头
    请求头,用来说明服务器要使用的附加信息,比较重要的信息有Cookie、Referer、User-Agent等。
    下面说一下一些常用的头信息。
  • Accept:请求报头域,用于指定客户端可以接收哪些类型的信息。
  • Accept-Language:指定客户端可以接受的语言类型。
  • Accept-Encoding:指定客户端可以接受的内容编码。
  • Host:用于指定请求资源的主机IP和端口号,器内容为请求URL的原始服务器或者网关的位置。从HTTP1.1banbenkaishi,请求必须包含此内容。
  • Cookie:也常用复数形式Cookies,这是网站为了辨别用户进行会话跟踪而存储在本地的用户本地数据。它的主要功能是维持当前访问会话。Cookies里面有信息标示了我们所对应的服务器的会话,每次浏览器在在请求该站点的页面时,都会在请求头上加上Cookies并将其发送给服务器,服务器通过Cookies识别出是我们自己,并且查出当前状态是登录状态,所以返回结果就是登陆之后才能看到的网页内容。
  • Referer:用来标识这个请求是从那个网页发过来的,服务器可以拿到这一信息并做相应的处理,如做来源统计、防盗链处理等。
  • User-Agent:简称UA,它是一种特殊的字符串头,可以使服务器识别客户使用的操作系统以及版本、浏览器及版本等信息。在做爬虫时加上此信息,可以伪装为浏览器;如果不加,很有可能被识别出是爬虫。
    -User-Type: 也叫互联网媒体类型或者MIME类型,在HTTP协议的消息头中,它用来表示具体请求中的媒体类型信息。例如,text/html表示HTML格式,image/gif代表GIF图片,application/json代表JSON类型,更多对应关系可以查看此对照表http://tool.oschina.net/commons

因此,请求头是请求的总要组成部分,在写爬虫时,大部分情况下都需要设定请求头。

  1. 请求体
    请求体一帮承载的内容都是POST请求中华的表单数据,而对于GET请求,请求体则为空。
响应

响应,由服务端返回给客服端,可以分为三个部分:响应状态吗、响应头,响应体

  1. 响应状态码
    响应状态码表示响应的响应状态,如200表示服务器正常响应,404代表页面没有找到,500代表服务器内部发生错误,在爬虫中,我们可以根据状态码来判断服务器响应状态,如果状态码为200,则证明成功返回数据,再进行进一步的处理,否则直接忽略。响应码对照表http://tool.oschina.net/commons?type=5
  2. 响应头
    响应头包含了服务器对请求的应答信息,如Content-Type、Server 、Server-Cookei等。下面简要说明一下一些常用的头信息。
  • Date:标识响应产生的时间。
  • Last-Modified:指定资源的最后修改时间。
  • Content-Encoding:指定响应内容的编码。
  • Server:包含服务器的信息,比如:名称、版本号等。
  • Content-Type:文档类型,指定返回的数据类型是什么,text/html表示HTML格式,image/gif代表GIF图片,application/json代表JSON类型,更多对应关系可以查看此对照表http://tool.oschina.net/commons
  • Set-Cookie:设置Cookies。响应头中的Set-Cookie告诉浏览器需要j将次内容放在cookies中,下次请求携带cookies请求。
  • Expires:指定响应的过期时间,可以使用代理服务器或者浏览器将加载的内容更新到缓存中。如果再次访问时,可以直接从缓存中加载,降低服务器负载,缩短响应时间。
  1. 响应体
    最重要的当属响应体的内容了。响应的正文数据都在响应体中,比如请求网页时,它的响应体就是网页的HTML代码;请求一张图片时,它的响应就是图片的二进制数据。我们做爬虫请求网页后,要解析的内容就是响应体。
    响应体内容

相关工具

  1. Chrome Developer Tools:谷歌浏览器内置的开发者工具。
  1. POSTMAN:功能强大的网页调试与RESTful请求工具。


  1. HTTPie:命令行HTTP客户端。

    $ http --header http://www.scu.edu.cn
    HTTP/1.1 200 OK
    Accept-Ranges: bytes
    Cache-Control: private, max-age=600
    Connection: Keep-Alive
    Content-Encoding: gzip
    Content-Language: zh-CN
    Content-Length: 14403
    Content-Type: text/html
    Date: Sun, 27 May 2018 15:38:25 GMT
    ETag: "e6ec-56d3032d70a32-gzip"
    Expires: Sun, 27 May 2018 15:48:25 GMT
    Keep-Alive: timeout=5, max=100
    Last-Modified: Sun, 27 May 2018 13:44:22 GMT
    Server: VWebServer
    Vary: User-Agent,Accept-Encoding
    X-Frame-Options: SAMEORIGIN
    
  2. BuiltWith:识别网站所用技术的工具。

    >>>
    >>> import builtwith
    >>> builtwith.parse('http://www.bootcss.com/')
    {'web-servers': ['Nginx'], 'font-scripts': ['Font Awesome'], 'javascript-frameworks': ['Lo-dash', 'Underscore.js', 'Vue.js', 'Zepto', 'jQuery'], 'web-frameworks': ['Twitter Bootstrap']}
    >>>
    >>> import ssl
    >>> ssl._create_default_https_context = ssl._create_unverified_context
    >>> builtwith.parse('https://www.jianshu.com/')
    {'web-servers': ['Tengine'], 'web-frameworks': ['Twitter Bootstrap', 'Ruby on Rails'], 'programming-languages': ['Ruby']}
    

一个简单的爬虫

一个基本的爬虫通常分为数据采集(网页下载)、数据处理(网页解析)和数据存储(将有用的信息持久化)三个部分的内容,当然更为高级的爬虫在数据采集和处理时会使用并发编程或分布式技术,这就需要有调度器(安排线程或进程执行对应的任务)、后台管理程序(监控爬虫的工作状态以及检查数据抓取的结果)等的参与。

爬虫的大概工作流程

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

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

下面的例子给出了一个从“搜狐体育”上获取NBA新闻标题和链接的爬虫。

from urllib.error import URLError
from urllib.request import urlopen

import re
import pymysql
import ssl

from pymysql import Error

# 通过指定的字符集对页面进行解码(不是每个网站都将字符集设置为utf-8)
def decode_page(page_bytes, charsets=('utf-8',)):
    page_html = None
    for charset in charsets:
        try:
            page_html = page_bytes.decode(charset)
            break
        except UnicodeDecodeError:
            pass
            # logging.error('Decode:', error)
    return page_html

# 获取页面的HTML代码(通过递归实现指定次数的重试操作)
def get_page_html(seed_url, *, retry_times=3, charsets=('utf-8',)):
    page_html = None
    try:
        page_html = decode_page(urlopen(seed_url).read(), charsets)
    except URLError:
        # logging.error('URL:', error)
        if retry_times > 0:
            return get_page_html(seed_url, retry_times=retry_times - 1,
                                 charsets=charsets)
    return page_html

# 从页面中提取需要的部分(通常是链接也可以通过正则表达式进行指定)
def get_matched_parts(page_html, pattern_str, pattern_ignore_case=re.I):
    pattern_regex = re.compile(pattern_str, pattern_ignore_case)
    return pattern_regex.findall(page_html) if page_html else []

# 开始执行爬虫程序并对指定的数据进行持久化操作
def start_crawl(seed_url, match_pattern, *, max_depth=-1):
    conn = pymysql.connect(host='localhost', port=3306,
                           database='crawler', user='root',
                           password='123456', charset='utf8')
    try:
        with conn.cursor() as cursor:
            url_list = [seed_url]
            # 通过下面的字典避免重复抓取并控制抓取深度
            visited_url_list = {seed_url: 0}
            while url_list:
                current_url = url_list.pop(0)
                depth = visited_url_list[current_url]
                if depth != max_depth:
                    # 尝试用utf-8/gbk/gb2312三种字符集进行页面解码
                    page_html = get_page_html(current_url, charsets=('utf-8', 'gbk', 'gb2312'))
                    links_list = get_matched_parts(page_html, match_pattern)
                    param_list = []
                    for link in links_list:
                        if link not in visited_url_list:
                            visited_url_list[link] = depth + 1
                            page_html = get_page_html(link, charsets=('utf-8', 'gbk', 'gb2312'))
                            headings = get_matched_parts(page_html, r'<h1>(.*)<span')
                            if headings:
                                param_list.append((headings[0], link))
                    cursor.executemany('insert into tb_result values (default, %s, %s)',
                                       param_list)
                    conn.commit()
    except Error:
        pass
        # logging.error('SQL:', error)
    finally:
        conn.close()

def main():
    ssl._create_default_https_context = ssl._create_unverified_context
    start_crawl('http://sports.sohu.com/nba_a.shtml',
                r'<a[^>]+test=a\s[^>]*href=["\'](.*?)["\']',
                max_depth=2)

if __name__ == '__main__':
    main()

由于使用了MySQL实现持久化操作,所以要先启动MySQL服务器再运行该程序。

爬虫注意事项

通过上面的例子,我们对爬虫已经有了一个感性的认识,在编写爬虫时有以下一些注意事项:

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

  2. 设置代理服务。有些网站会限制访问的区域(例如美国的Netflix屏蔽了很多国家的访问),有些爬虫需要隐藏自己的身份,在这种情况下可以设置使用代理服务器,代理服务器有免费(如西刺代理快代理)和付费两种(如讯代理阿布云代理),付费的一般稳定性和可用性都更好,可以通过urllib.request中的ProxyHandler来为请求设置代理。

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

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

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