配置 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
HTTP 协议属于应用层协议
无连接:每次连接只处理一个请求无状态:每次连接、传输都是独立的REQUEST部分的HTTP HEADER
Http是一个请求-响应模式的典型范例,即客户端向服务器发送一个请求信息,服务器来响应这个信息。在老的HTTP版本中,每个请求都将被创建一个新的客户端-服务器的连接,在这个连接上发送请求,然后接收请求。这样的模式有一个很大的优点就是简单,很容易理解和编译实现;他也有一个很大的缺点效率很低,因此Keep-Alive被提出用来解决效率低的问题。
Keep-Alive 功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。
HTTP/1.1
默认情况下所在的HTTP1.1中所有连接都被保持,除非在请求头或响应头中指明要关闭:Connection:Close
Resopnse的HTTP Header
HTTP 请求方法
l 2XX 成功
l 3XX 跳转
l 4XX 客户端错误
l 500 服务器错误
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经过MD5或SHA-1等单向哈希后再保存到HashSet或数据库。Bit-Map方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。哈希算法
哈希算法将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。如果散列一段明文而且哪怕只更改该段落的一个字母,随后的哈希都将产生不同的值。要找到散列为同一个值的两个不同的输入,在计算上是不可能的,所以数据的哈希值可以检验数据的完整性。
MD5 签名是一个哈希函数,可以将任意长度的数据量转换为一个固定长度的数据(通常是4个整形,128位)。计算机不可能有2的128那么大内存,因此实际的哈希表都会是URL.MD5再%n(即取模)。现实世界的URL组合必然超越哈希表的槽位数,因此碰撞是一定存在的,一般的Hash函数,例如Java的HashTable是一个Hash表再跟上一个链表,链表里面存放的是碰撞结果
提高效率:
评估网站的网页数量选择合适的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] = 1Bloom Filter
Bloom Filter 使用了多个哈希函数,而不是一个。创建一个m位BitSet,先将所有位初始化为0,然后选择k个不同的哈希函数。第i个哈希函数对字符串str哈希的结果记为h(i,str),且h(i, str)的范围是0到m-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
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>l 利用sitemap里的信息,直接对目标网页.html进行抓取
l 对网站的目录结构进行分析
对大多数的网站都存在明确的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,需要手动调用