网络知识总结

环境搭建

Python Unix 安装pip

  • python2.7
  • pip, 并配置Pip

配置 pip conf, 自动设置源

# mkdir ~/.pip/
# vim ~/.pip/pip.conf
[global]
Index-url=https://pypi.edu.cn/simple


也可以每次安装的时候定Source

#pip install –I https://pypi.edu.cn/simple lxml

Python Windows 安装pip

  • 直接下载 Anaconda,很多比较难以安装的源都已经包含了
  • 仍然配置pip源,各个系统的默认pip.ini位置不同,需要根据实际情况设置

官网: https//anaconda.org/

下载主页: https://www.continuum.io/downloads

 

TCP/IP 四层 与OSI七层

 

 

 

HTTP 协议

  • 物理层:电器连接
  • 数据链路层:交换机,STP,帧中继
  • 网络层:路由器,IP协议
  • 传输层:TCPUDP协议
  • 会话层:建立通信连接,网络拨号
  • 表示层:每次连接只处理一个请求
  • 应用层:HTTPFTP

HTTP 协议属于应用层协议

  • 无连接:每次连接只处理一个请求
  • 无状态:每次连接、传输都是独立的

REQUEST部分的HTTP HEADER

 


Keep-alive

Http是一个请求-响应模式的典型范例,即客户端向服务器发送一个请求信息,服务器来响应这个信息。在老的HTTP版本中,每个请求都将被创建一个新的客户端-服务器的连接,在这个连接上发送请求,然后接收请求。这样的模式有一个很大的优点就是简单,很容易理解和编译实现;他也有一个很大的缺点效率很低,因此Keep-Alive被提出用来解决效率低的问题。

Keep-Alive 功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。

HTTP/1.1

默认情况下所在的HTTP1.1中所有连接都被保持,除非在请求头或响应头中指明要关闭:Connection:Close

 

ResopnseHTTP Header

 

HTTP 请求方法

 

HTTP 响应状态码

l 2XX 成功

l 3XX 跳转

l 4XX 客户端错误

l 500 服务器错误

HTTP 响应状态码300

300 Multiple Choices 存在多个可用的资源, 可处理或丢弃

301 Moved Permanetly 重定向

302 Found 重定向

304 Not Modified 请求的资源未更新,丢弃

 

一些Python库,例如urllib2已结对重定向做了处理,会自动跳转;动态网页处理的时候,也是自动跳转,所以不需要单独处理

 

HTTP 响应状态码 400 500

400 Bad Request 客户端请求有语法错误,不能被服务器所理解

401 Unauthorized 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用

403 Forbidden 服务器收到请求,但是拒绝提供服务

404 Not Found 请求资源不存在,eg:输入了错误的URL

500 Internal Server Error 服务器发生不可预期的错误

503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常

错误处理

400 Bad Request 检查请求的参数或者路径

401 Unauthorized 如果需要授权的网页,尝试重新登录

403 Forbidden

如果是需要登录的网站,尝试重新登录

IP被封,暂停爬取,并增加爬虫的等待时间,如果拨号网络,尝试重新联网更改IP

404 Not Found 直接丢弃

5XX 服务器错误,直接丢弃,并计数,如果连接不成功,WARNING并停止爬取

 

网页抓取原理

 

 

深度优化策略

 

采用递归算法

public class Crawler{

	String[] getAllUrls(String htmlContent);

	void appendDownloadedUrl(String url);

	void appendErrorUrl(String url);

	boolean isDownloaded(String url);

	String getPageContent(String url) throws CrawlException, IOException;

	String savePageContent(String pageContent);

	String rootNode;

	final static int WIDTH = 50;
	final static int HEIGHT = 5;


	void crawl(String url, level){
		try{
			String pageContent = getPageContent(url);
		} catch( Exception e ){
			appendErrorUrl(url);
			return;
		}

		savePageContent(pageContent);

		if ( level == HEIGHT )
			return;

		String[] urls = getAllUrls(pageContent);

		for( int i = 0; i < urls.length; i ++ ){

			if ( i == WIDTH )
				return;
			if ( isDownloaded(urls[i]))
				continue;
			crawl( urls[i], level + 1 );
		}
	}

	start(){
		crawl("http://www.mq.com.cn", 0 );
	}

}


宽度优化策略


 

采用队列方式

package cn.marble;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class Main {

    Queue<String> currentQueue = new ConcurrentLinkedQueue<String>();

    Queue<String> childQueue = new ConcurrentLinkedQueue<String>();

    int currentLevel = 0;

    void appendDownloadedUrl(String url){

    }

    void appendErrorUrl(String url){

    }

    boolean isDownloaded(String url){

    }

    String getPageContent(String url) throws CrawlException, IOException {
        return "";
    }

    String savePageContent(String pageContent){
        return "";
    }

    void enqueueUrls(ConcurrentLinkedQueue<String> urls){
        childQueue.addAll(urls);
    }

    void enqueueUrlsFromPageSrc(String pageContent){

    }

    void enqueueUrl(String url){
        childQueue.add(url);
    }

    String dequeueUrl(){
        String url = currentQueue.poll();
        if ( url == null ){
            currentLevel ++;
            if ( currentLevel == HEIGHT )
                return null;
            currentQueue = childQueue;
            childQueue = new ConcurrentLinkedQueue<String>();
            url = currentQueue.poll();
        }
        return url;
    }

    String rootNode;

    final static int WIDTH = 50;
    final static int HEIGHT = 5;

    void crawl(String url){
        String pageContent;
        try{
            pageContent = getPageContent(url);
            savePageContent(pageContent);
        } catch( Exception e ){
            appendErrorUrl(url);
            return;
        }

        enqueueUrlsFromPageSrc(pageContent);
    }

    void start(){

        int curLevel = 0;
        enqueueUrl("http://www.mq.cn");

        while( true ){
            String url = dequeueUrl();
            if ( url == null ){
                if ( url == null )
                    break;
            }
            crawl(url);

        }
    }

    public static void main(String[] args) {
	// write your code here
    }
}


选择哪种策略

  • 重要的网页距离种子站点比较近
  • 万维网的深度并没有很深,一个网页有很多路径可以到达
  • 宽度优先有利于多爬虫并行合作抓取
  • 深度限制于宽度有效相结合

不重复抓取

如何记录抓取历史

  • 将访问过的url保存到数据库效率太低
  • HashSet将访问过的URL保存起来。那只需接近O(1)的代价就可以查到一个URL是否被访问过了。消耗内存
  • URL经过MD5SHA-1等单向哈希后再保存到HashSet或数据库。
  • Bit-Map方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。

 哈希算法

哈希算法将任意长度的二进制映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值哈希值是一段数据唯一且极其紧凑的数值表示形式。如果散列一段明文而且哪怕只更改该段落的一个字母,随后的哈希都将产生不同的值。要找到散列为同一个值的两个不同的输入,在计算上是不可能的,所以数据的哈希值可以检验数据的完整性。

MD5 签名是一个哈希函数,可以将任意长度的数据量转换为一个固定长度的数据(通常是4个整形,128位)。计算机不可能有2128那么大内存,因此实际的哈希表都会是URL.MD5%n(即取模)。现实世界的URL组合必然超越哈希表的槽位数,因此碰撞是一定存在的,一般的Hash函数,例如JavaHashTable是一个Hash表再跟上一个链表,链表里面存放的是碰撞结果



提高效率:

  • 评估网站的网页数量
  • 选择合适的HASH算法和空间阀值,降低碰撞记录
  • 选择合适的存储结构和算法



BITMAP 方式记录

  • 将URLMD5值再次哈希,用一个或多个BIT位来记录一个URL
  • 确定空间大小
  • 按倍增加槽位
  • Hash算法映射


Pip install murmurhash3 bitarray

From bitarray import bitarray
Import mmh3

offset = 2147483647 //2^31 -1
bit_array = bitarray(4*1024*1024*1024)
bit_array.setAll(0)

# mmh3 hash value 32 bit signed int
# add offset to make it unsigned int 0 ~ 2^32 -1
b1 = mmh3.hash(url, 42) + offset
bit_array[b1] = 1

BitMap方式记录

  • 优势: 对存储进行了进一步压缩,在MD5的基础上,可以从128位最多压缩到1位,一般情况,如果用4bit或者8bit表示一个url,也能压缩32或者16
  • 缺陷:碰撞概率增加

Bloom Filter

Bloom Filter 使用了多个哈希函数,而不是一个。创建一个mBitSet,先将所有位初始化为0,然后选择k个不同的哈希函数。第i个哈希函数对字符串str哈希的结果记为h(i,str),h(i, str)的范围是0m-1.

只能插入,不能删除.


pybloomfilter

安装

pip install pybloomfilter(可能运行时会crash)
git clone https//github.com/axiak/pybloomfiltermmap.git
python setup.py install


构造函数

class pybloomfilter.BloomFilter(capacity: int, error_rate: float[,filename=None : string ][,perm=0755)

并不实际检查容量,如果需要比较低的error_rate,则需要设置更大的容量

Sample

>>> fruit = pybloomfilter.BloomFilter(10000, 0.1, ‘/tmp/words.bloom’)
>>> fruit.update((‘apple’, ‘pear’, ‘orange’, ‘apple’))
>>> ‘mike’ in fruit
False
>>>’apple’ in fruit
True


管方文档

https://media.readthedocs.org/pdf/pybloomfiltermmap3/latest/pybloomfiltermmap3.pdf

 

如何有效记录抓取历史

  • 多数情况下不需要压缩,尤其网页数量少的情况
  • 网页数量大的情况下,使用Bloom Filter 压缩
  • 重点是计算碰撞概率,并根据碰撞概率来确定存储空间的阈值
  • 分布式系统,将散列映射到多台主机的内存

Robots.txt

  • 网站对爬虫的限制
  • 利用sitemap 来分析网站结构和估算目标网页的规模

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

	[#if index == 0]
		<url>
			<loc>${setting.siteUrl}/</loc>
			<changefreq>hourly</changefreq>
			<priority>1</priority>
		</url>
	[/#if]

	[#list articles as article]
		<url>
			<loc>${setting.siteUrl}${article.path}</loc>
			<lastmod>${article.modifyDate?string("yyyy-MM-dd")}</lastmod>
			<changefreq>weekly</changefreq>
			<priority>0.6</priority>
		</url>
	[/#list]
	
	[#list products as product]
		<url>
			<loc>${setting.siteUrl}${product.path}</loc>
			<lastmod>${product.modifyDate?string("yyyy-MM-dd")}</lastmod>
			<changefreq>weekly</changefreq>
			<priority>0.6</priority>
		</url>
	[/#list]
	
	[#list brands as brand]
		<url>
			<loc>${setting.siteUrl}${brand.path}</loc>
			<lastmod>${brand.modifyDate?string("yyyy-MM-dd")}</lastmod>
			<changefreq>weekly</changefreq>
			<priority>0.5</priority>
		</url>
	[/#list]
	
	[#list promotions as promotion]
		<url>
			<loc>${setting.siteUrl}${promotion.path}</loc>
			<lastmod>${promotion.modifyDate?string("yyyy-MM-dd")}</lastmod>
			<changefreq>weekly</changefreq>
			<priority>0.5</priority>
		</url>
	[/#list]

</urlset>

有效率抓取特定内容

利用sitemap里的信息,直接对目标网页.html进行抓取

对网站的目录结构进行分析

对大多数的网站都存在明确的top-down的分类的目录结构,我们可以进入特定目录进行抓取

如:产品目录: /product/content/201707/1.html

    文章目录:/article/content/201707/111.html


import urllib2
import httplib
import re
from pybloomfilter import BloomFilter
import os

request_headers = {
    'host': "www.mq.cn",
    'connection': "keep-alive",
    'cache-control': "no-cache",
    'upgrade-insecure-requests': "1",
    'user-agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36",
    'accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    'accept-language': "zh-CN,en-US;q=0.8,en;q=0.6"
}

city_home_pages = []
city_ids = []
dirname = 'mq_notes/'

# 创建 Bloom Filter
download_bf = BloomFilter(1024 * 1024 * 16, 0.01)


def download_city_notes(id):
    for i in range(1, 999):
        url = 'http://www.mq.cn/yj/%s/1-0-%d.html' % (id, i)
        if url in download_bf:
            continue
        print 'open url %s' % (url)
        download_bf.add(url)
        req = urllib2.Request(url, headers=request_headers)
        response = urllib2.urlopen(req)
        htmlcontent = response.read()
        city_notes = re.findall('href="/i/\d{7}.html', htmlcontent)

        # 如果导航页错误,该页的游记数为0,则意味着 1-0-xxx.html 已经遍历完,结束这个城市
        if len(city_notes) == 0:
            return
        for city_note in city_notes:
            try:
                city_url = 'http://www.mq.cn%s' % (city_note[6:])
                if city_url in download_bf:
                    continue
                print 'download %s' % (city_url)
                req = urllib2.Request(city_url, headers=request_headers)
                response = urllib2.urlopen(req)
                html = response.read()
                filename = city_url[7:].replace('/', '_')
                fo = open("%s%s" % (dirname, filename), 'wb+')
                fo.write(html)
                fo.close()
                download_bf.add(city_url)
            except Exception, Arguments:
                print Arguments
                continue

# 检查用于存储网页文件夹是否存在,不存在则创建
if not os.path.exits(dirname):
    os.makedirs(dirname)

try:
    # 下载目的地的首页
    req = urllib2.Request('http://www.mq.cn/mdd/', headers=request_headers)
    response = urllib2.urlopen(req)
    htmlcontent = response.read()

    # 利用正则表达式,找出所有的城市主页
    city_home_pages = re.findall('/travel-scenic-spot/mq/\d{5}.html', htmlcontent)

    # 通过循环,依次下载每个城市下的所有游记
    for city in city_home_pages:
        city_ids.append(city[29:34])
        download_city_notes(city[29:34])
except urllib2.HTTPError, Arguments:
    print Arguments
except httplib.BadStatusLine:
    print 'BadStatusLine'
except Exception, Arguments:
    print Arguments

爬虫相关技术

python


Python 的正则表达式

Module name: import re

Doc: https://docs.python.org/2/library/re.html

Useful Methods: findall(pattern, string, flags = 0)

Useful Patterns:

.

Any Char

*

0 or more

\

escape

^

Start

+

1 or more

{m,n}

M to n

$

End

?

0 or 1

[]

A set of characters

\A

Start with

\d

Decimal digit

|

Or e.g. A|B

 

HTML

HTML 指的是超文本标记语言(Hyper Text Markup Language

HTML 不是一种编程语言,而是一种标记语言

标记语言是一套标记标签

HTML 使用标记标签来描述网页




网页的关键内容

Title 网页的标题

Content Title 正文的标题

Content 正文部分

Anchor 内部的描点

Link 外部链接


爬虫相关的网页技术

<a> 链接

<tr> <p> <li> 等标签

Dom:快速选择内容,在爬取和网页解析的时候都会用到

Css: class id

Cookie: 对于需要登录的网站

Ajax: javascript 的动态内容加载

Chrome: Inspector

 

Python lxml

Summary: 网页DOM选择器,快速定位操作HTML对象,也可以用于XML

Install: pip install lxml

Official doc: http://lxml.de/

Methods:

Html = etree.HTML(html_content.lower().decode(‘utf-8’))

Hrefs = html.xpath(u”//a”)

Hrefs= html.xpath(u’a[@class=”last-page”]’)

Hrefs= html.xpath(u’*[@calss=”last-page”]’)

 

Html = lxml.html.fromstring(html_content)

Elements = html.cssselect(‘div #page-let > a.last-page’)

 

多线程的复杂性:

资源、数据的安全性:锁保护

原子性:数据操作是天然互斥的

同步等待:wait() notify() notifyAll()

死锁:多个线程对资源互锁,造成死锁

容灾:任何线程出现错误,整个进程都会停止

 

多线程的优势

内存空间共享,信息数据交换效率高

提高CPU的使用率

开发便捷

轻、创建、销毁的开销小

 

Python线程

支持多线程

Python线程直接映射到native线程

GIL(Global Interpretor Lock): 对于多核的利用能力有限

 

实现一个多线程爬虫

创建一个线程池 threads = []

确认url 队列线程安全 Queue Deque

从队列取出url,分配一个线程开始爬取pop()/get() threading.Thread

如果线程池满了,循环等待,直到有线程结束 t.is_alive()

从线程池移除已经完成下载的线程 threads.remove(t)

如果当前级别的url已经遍历完成,t.join() 函数等待所有现场结束,然后开始下一级别的爬取


多线程爬虫评价

优势:

有效利用CPU时间

极大减小下载出错、阻塞对抓取速度的影响,整体上提高下载的速度

对于没有反爬虫限制的网站,下载速度可以多倍增加

 

局限性:

对于有反爬虫的网站,速度提升有限

提高了复杂度,对编码要求更高

线程越多,每个线程获得的时间就越少,同时线程切换更频繁也带来额外的开销

线程之间资源竞争激烈

 

多进程爬虫评估

目的:

控制线程数量

对线程进行隔离,减少资源竞争

某些环境下,在单机上利用多个IP来伪装

 

局限性:

不能突破网络瓶颈

单机单IP的情况下,变得没有意义

数据交换的代价更大

 

进程间通信

管道(PIPE

信号(Signal: 复杂

消息队列:Posix system V

共享内存:速度最快,需要结合信号量达到进程同步及互斥

信号量:用于数据同步

Socket: 可以标准化,可以用于多机

 

创建多进程爬虫

Solution A -c/s模式

一个服务进程,入队及出队URL,入队需检查是否已经下载

监控目前的爬取状态、进度

多个爬取进程,从服务进程获取URL, 并将新的URL返回给服务进程

使用Socket 来做IPC

Solution B -数据库模式

使用数据库来读写爬取列表

多个爬取进程,URL的获取与增加都通过数据库操作

 

C/S vs 数据库

Cs优势:

运行速度快,添加、修改、查询都是内存的BIT位操作

扩展方便,例如动态URL队列重排

数据库:

开发便捷,数据库天生具备读写保护及支持IPC

只需要写一个爬虫程序

 

创建MySql数据库表

 

Key

Type

Description

index

Int(11)

PRIMARY KEY AUTOINCREMENT

url

Varchar(512)

UNIQUE

status

Varchar(11)

Download status:new, downloading,done

Md5

Varchar(16)

UNIQUE md5 value of url

depth

Int(11)

Page depth

Queue_time

timestamp

CURRENT_TIMESTAMP time url enqueue

Done_time

timestamp

Time page downloaded

 

SET FOREIGN_KEY_CHECKS=0;
 
-- ----------------------------
-- Table structure for urls
-- ----------------------------
DROP TABLE IF EXISTS `urls`;
CREATE TABLE `urls` (
  `index` int(11) NOT NULL AUTO_INCREMENT,
  `url` varchar(512) NOT NULL,
  `md5` varchar(16) NOT NULL,
  `status` varchar(11) NOT NULL DEFAULT 'new',
  `depth` int(11) NOT NULL,
  `queue_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
  `done_time` time NOT NULL,
  PRIMARY KEY (`index`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Python Mysql Connector

使用MySQLConnectionPool 来管理多线程下的mysql数据库连接

mysql.connector.pooling.MySQLConnectionPool

Self.cnxpool.get_connection()

Init类实例的时候自动检查和创建数据库及表

Cursor 类: cursor = con.cursor(dictionnary=True)

Select ... For update 加读锁 避免多个进程取同一个url

Cursor.commit() 提交事务,默认关闭了 autocomit,需要手动调用



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值