迁移 Python 3

使用 Python 3 的呼声一直很高,Python 3 解决了很多 2 中的坑,比如 unicode,在向他们解释为什么 print str 乱码,fp.write(str) 时报错,在什么时候需要 encode,更容易了。

但是由于一开始接触的就是 Python 2,熟悉的包都是 Python 2(我也不确定他们是否支持 Python 3)。公司机器上的 Python 2.7 就算是“最新”版本。于是一直没有升级。不过有一种说法,切换到 Python 3 的最好时机就是现在。-为了庆祝 star 过 3000-,由于见到两次要求支持 Python 3,用一个周末为 pyspider 加入了 Python 3 支持(怎么样,不难吧)。

主要参考:

开始之前

其实 Porting Python 2 Code to Python 3 这篇文章是一个非常好的索引,能让你对将要进行的工作有一个整体的把握,同时能提供细节的链接,能让你立即开始工作。而且这一节内容就来自此文的 The Short Explanation 一节。因为总结得很好,所以就不重复造轮子了。

首先,低版本的 Python 2 与 Python 3 之间的鸿沟太大了,特别是 Python 2.5(含) 以前的版本。要同时兼容他们的代价太大。而 Python 2.6 和 Python 2.7 已经带有部分 Python 3 的特性,这让迁移的代价大大降低了。同时,不建议支持 Python 3.3 以下的 3 字头版本,由于 Python 3 实际上已经 release 6 年了,这些 Python 3.x 版本也比较老了,很多特性还没有,或者包不支持。所以建议跳过他们。

其次,一定要有测试,保证测试足够的代码覆盖。Python 2 到 Python 3 从包改名到语法都有变化,几乎所有的代码都需要有修改。足够的代码覆盖,才能在这样大规模修改中,保证所有功能可用。而 pyspider 正是因为有 86% 的代码覆盖,我能这么快地完成代码迁移。

读一读 Python 2 和 Python 3 有什么不同。这个可以看看 What’s New in Python,特别是 What’s New In Python 3.0。当然也可以找一些中文的文章,这个方面应该还蛮多的。反正最主要的就是大量的包改名,以及 bytes, str, unicode 三者的变化。或者你可以先读一读 Cheat Sheet,虽然等下我们还需要它。

好,现在可以来看看你的包依赖是否支持 Python 3 了。并不是 pip 能安装的包就是支持 Python 3 的,可能装上了依旧不能工作。你可以用 Can I Use Python 3 检测包是否支持。不过我更推荐 PYTHON 3 WALL OF SUPERPOWERS (需要翻墙)。不过也不用担心,大部分包都是支持 Python 3 的,如果不支持,一般都会有替代,例如 pika 就可以被 ampq 替换,而 MySQL-python 能被 mysql-connector-python 替代。

第一步——查找替换

首先我们从大的方向入手,把一些改名了的包和函数处理一下。请打开 Cheat Sheet: Writing Python 2-3 compatible code 参照它们一条条进行。在能搜索的地方,使用搜索统一修改,不然挨个文件太慢,而且会忘记的。因为我用的是 six 作为多环境间的桥梁。所以需要同时参考 six的文档。你可能需要打开两个窗口,同时运行 Python 2 和 Python 3,确认语句在两个环境下都能执行。

在这一步,我做了以下处理:

  • 相对导入 - Imports relative to a package
  • urlparse / urllib 库改名 - six
  • thread 包改名,而且 get_ident 函数不再存在了。将 thread.get_ident() 改为 threading.current_thread().ident six
  • basestring 类型不再存在,用 six.string_types 代替 sheet
  • __metaclass__ 不再存在,用 six.add_metaclass 代替 sheet
  • UserDict.DictMixin 不再存在,用 collections.Mapping 或者 collections.MutableMapping 代替
  • / 现在是真的除法了,也就是说 int / int 会得到一个 float,使用 // 获得地板除效果(由于在 python 中,地板除用得少,实际上不改关系不大) sheet
  • StringIO 现在分为 io.BytesIOio.StringIO 视情况使用
  • print 现在是一个 function 了 sheet
  • unicode 关键字不再存在 使用 six.text_type 代替
  • __builtins__ 不存在了,six.moves.builtins sheet
  • reload 改为 six.reload_module
  • dict 的 keysitemsvalues 现在都是迭代器了,不返回列表,原来的 iteritems, itervalues 不再存在,使用 six.iterkeys 等函数代替。
  • raise exc_type, exc_value, tb 的形式不再支持,使用 six.reraise(exc_type, exc_value, tb) 代替。

其他的例如 try…catch,如果你在 Python 2 中就比较标准地使用 as,那么这时就不用修改了。

另外,如果你和我一样有 str(object) 来获得 object 的文字结果的习惯话,每次写 six.text_type(object) 太长了。可以写一些兼容性函数,然后在整个项目中使用。

注意到这里,我们并没有处理 bytes, string, unicode,请放下他们,我们在下一节处理这些问题。

第二步——处理 unicode

由于在 Python 3 中,所有的 'text' 都变成 unicode 了,所以你会觉得它会是一个大问题,是否需要给所有的 'text' 加上 u ,或者干脆所有文件都加上 from __future__ import unicode_literals

实际上,大部分时候不需要。

在 Python 2 中,我们很少有意识地区分 strunicode,对于大部分函数调用来说,给它 str 或者 unicode 都是一样的,因为他们共享大部分行为。但是在 Python 3 中,bytesstr(unicode) 却大不一样。例如当你 for c in bytes 时,得到的是一个 int 而不是一个 str

虽然不做任何修改,'text' 在 Python 2 中,是 str(bytes),而在 Python 3 中是 str(unicode)。但是提交给函数时,既然 Python 2 的函数同时支持 strunicode,所以没有任何问题。而且,在 Python 2 中,'text'+u'中文' 会自动升级为 unicode,所以,只需要注意在出现中文的地方使用 u'中文' 就好了(即使在 Python 2 中,这也是一个好的习惯)。而 b'bytes' 的场合非常少,更多的是使用 text.encode 进行转换。所以,对于习惯良好的 Python 2 代码来说,是几乎不需要修改的。

除了源代码之中的 unicode 问题,其他主要问题出现在输入输出上。但是,只要遵循:程序中流通的数据,只能是 unicode。数据进来之后必须转换成 unicode 即可。

最后

运行测试,哪报错改哪就好了。

pyspider介绍

虽然已经发过一篇架构设计,但是觉得还是有必要发一篇介绍。而且拖了那么久的第二里程碑的commit数已经超过第一个版本了。。

那么由我再次介绍一下 pyspider。

缘起

pyspider 来源于以前做的一个垂直搜索引擎使用的爬虫后端。我们需要从200个站点(由于站点失效,不是都同时啦,同时有100+在跑吧)采集数据,并要求在5分钟内将对方网站的更新更新到库中。

所以,灵活的抓取控制是必须的。同时,由于100个站点,每天都可能会有站点失效或者改版,所以需要能够监控模板失效,以及查看抓取状态。

为了达到5分钟更新,我们使用抓取最近更新页上面的最后更新时间,以此来判断页面是否需要再次抓取。

可见,这个项目对于爬虫的监控和调度要求是非常高的。

pyspider 的主要特性

  • python 脚本控制,可以用任何你喜欢的html解析包(内置 pyquery)
  • WEB 界面编写调试脚本,起停脚本,监控执行状态,查看活动历史,获取结果产出
  • 支持 MySQL, MongoDB, SQLite
  • 支持抓取 JavaScript 的页面
  • 组件可替换,支持单机/分布式部署,支持 Docker 部署
  • 强大的调度控制

由于功能太多,更多请参考脚本编写指南

感谢 +PhoenixNemo 提供的VPS,提供了一个 demo: demo.pyspider.org。无需安装即可体验。

demo

脚本样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from libs.base_handler import *

class Handler(BaseHandler):
'''
this is a sample handler
'''
@every(minutes=24*60, seconds=0)
def on_start(self):
self.crawl('http://scrapy.org/', callback=self.index_page)

@config(age=10*24*60*60)
def index_page(self, response):
for each in response.doc('a[href^="http://"]').items():
self.crawl(each.attr.href, callback=self.detail_page)

def detail_page(self, response):
return {
"url": response.url,
"title": response.doc('title').text(),
}

例如这就是创建任务后默认生成的一个脚本示例。

  • 通过 on_start 回调函数,作为爬取的入口点,当点击主面板上的 run 的时候,就是调用这个函数,启动抓取。
  • self.crawl 告诉调度器,我们需要抓取 'http://scrapy.org/' 这个页面,然后使用 callback=self.index_page 这个回调函数进行解析。
  • 所有 return 的内容默认会被捕获到 resultdb 中,可以直接在 WEBUI 上看到。

更多特性和文档

签到 —— qiandao.today 介绍

qiandao.today 已经上线了一个半月,这篇blog一个半月以前就应该写了。直到我刷了14遍水晶塔没有ROLL到任何装备(不对,我最后通过贪婪ROLL到了!),打了两晚麻将,把把最小胡牌距离大于5(任意更换手牌达到胡牌的最小张数),房子里刷JJ怪之后。我觉得我必须做点什么。。。

好了,不扯蛋了。自动签到是我对于 “如何请求到数据” ,进行请求自动分析的一个尝试(实际是我 U2 因为45天没登录被封了)。通过浏览器捕获页面请求瀑布流,进行内容/请求分析,找出关键请求。所以,签到这个项目,我就是先从 HAR编辑器 开始做的。做的时候还玩了一下 angularjs然后其他部分都是随便写的

但是,对于签到来说,哪些请求是必要的,这个请求是怎么组装的(例如 token 参数怎么来),特征不明显。自动分析出来就能直接用的概率太低了,即使是人还得单步测试呢。于是 HAR编辑器 成为编辑和单步调试的辅助。自动分析变成了 “推荐相关请求”。

  • 用户部分系统尝试了一下 PBKDF2 进行密码加密。PBKDF2 的优势在于通过随机盐 加 可配置的多轮加密,加大了单个key的运算代价。
  • 模板执行部分通过提取页面信息,和 jinja2 引擎渲染,可以动态地改变请求的 url、header、data 各个部分。
  • 执行断言加上邮件系统,可以检测签到是否成功,在失败的时候给用户发送邮件提醒。

本来还想要做互助打码的验证码系统的,但是通过 雪月秋水cookie插件,其实大部分只有登录需要验证码,签到并不需要。关键是做这个东西不好玩,于是就算了。

运行了一个半月,目前有11个公开签到模板,400+个签到任务,每天进行300次签到。不过由于担心单IP登录帐号过多被封,只在v2ex做了一次广告,不敢大范围推广。。。


以下是面向普通用户的简介:

  • 云代签
  • 支持多个网站
  • 失败邮件提醒
  • 自制模板并分享(文档
  • https 传输安全保证
  • 一号一密用户数据加密
  • 开放源码,支持本地执行(提供本地lite版)

github: binux/qiandao
网站: https://qiandao.today

如何从 WEB 页面中提取信息

已经五个月没有更新 blog 了,这五个月全身心投入到了两个关于如何从页面上抽取结构化数据的项目上。这也是我加入某厂最主要的原因。其中一个全自动模板生成抽取器,虽然还不完全能够实用,但比1年前效果好太多,同时也让我想明白了一些问题。这都是下文主要讨论的问题。

关于 pyspider 项目,这几天我也在慢慢填上这延期了3个月的坑,至少完成第二里程碑吧。但缺少实际应用的环境,很多东西是否工作得很好,我也不是很有把握。如果有的话,还是希望支持1-2个实际的抓取项目吧。

而 “如何获得页面/数据” 这个问题依旧是我持续关注中,想要去解决的问题。但是,既然某厂的后续解决方案是将所有抓取页面过 webkit 渲染(虽然很多时候渲染不出 或 需要点击动作,代价往往大于直接抓 API),不会有很多精力投入,待我慢慢想想。。

##四种解析模式

xpath / css选择器 / 正则表达式

示例: https://www.kimonolabs.com/

通过手动、自动、半自动方式,设定需要抽取元素的 xpathcss选择器 或 正则表达式 进行定位提取的方法(这里需要指出的是,html 并不是正则的,正则表达式可能在部分简单提取时有效,但 不要用正则表达式进行页面提取)。其根本思想是提供一种定位元素的规则进行页面抽取。

这个方法被用得最多,好处是有效,嗯。缺陷在于用户需要会 xpath / css选择器 / 正则语法,虽然有一些工具(例如上面的kimono、chrome的调试工具、pyspider里面的脚本)辅助生成规则,但可能通用性不足 或 区分度不够,选取到不需要的内容。这在大批量抽取时需要大量的高级人力去配置,即使是熟练工也需要5-10分钟配置一个页面(6-8个属性),需要耗费大量精力。

这种抽取方式的一种变形是:将 key 和 value 同时在页面中标出,通过 key 和 value 总是穿插出现的这一假设,省去单独为每个属性设置规则的人力,极大增快标注效率。
例如:http://movie.douban.com/subject/7054604/ 这个页面中的

导演: 迈克尔·贝
编剧: 伊伦·克鲁格
主演: 马克·沃尔伯格…
类型: 动作 / 科幻 / 冒险
制片国家/地区: 美国 / 中国大陆
语言: 英语 / 汉语普通话 / 粤语
上映日期: 2014-06-27(美国/中国大陆)
片长: 166分钟
又名: 变形金刚:歼灭世纪(港) / 变形金刚4:灭绝时代 / 变形金刚4 / 变4 / Transformers 4
IMDb链接: tt2109248

导演/编剧/类型等 属性名 往往拥有相同的 xpath,而值的 xpath 也是独立的几种。他们一定是 key: value 的形式组织的,通过用 key 分割 value 的方式能轻松将所有信息提取出来。

data highlighter

示例: http://googlewebmastercentral.blogspot.com/2012/12/introducing-data-highlighter-for-event.html

Data Highlighter 的标注方式是:给一系列相似的页面,让用户标出(高亮)每个属性在页面中的位置。通过多个页面的标注信息,寻找每个属性的特征。当然了,这个特征可以是 xpath,也可以是上下文,也有可能是机器学习的特征向量。

Data Hightlighter 通过高亮 多个页面中相同属性 进行规则学习,省去了人为设置规则时的学习成本。实践表明,在单一页面模板下,标记2个页面就足以生成规则了。效率远大于手工设置规则。Google Data Highlighter 甚至对文字进行了切分,能在 英语 / 汉语普通话 / 粤语 xpath 相同的情况下,分别选出三种语言。是我目前见过的成熟度最高、通用性最好、最简便的数据抽取方式。

micro-data

示例: http://microformats.org/ 以及各大网站

页面属性标记,通过在页面数据元素上增加属性标识,通过开放的标准格式,为数据提取提供便利,例如这是豆瓣的评论数据:

1
2
3
4
5
< p class="rating_self clearfix" typeof="v:Rating">
<span class="ll bigstar35"></span>
<strong class="ll rating_num" property="v:average">6.7</strong>
<span property="v:best" content="10.0"></span>
</p>

typeof="v:Rating" 表明这里是 rating 数据,v:best 表明 rating 的最大值。通过开放的 data format 标准,只按照标准抽取,就能得到包含的结构化数据。但是,需要站长的支持,在页面中加入标记才可以。

从广义上讲,主图识别,页面发布时间这样的属性,其实也可以是通过对页面内容进行分析获得的。这与 micro-data 一样,通过元素足够强的特征,对元素的含义进行理解分析。

模板生成与提取

image

页面模板(wrapper)抽取是基于这样一个假设:结构化页面都是 通过模板 将数据库中的数据 映射成页面的。通过页面分析,得到页面模板,通过模板提取出实际的结构化数据。

例如,我使用过的方法,将多个相似页面放在一起比对,寻找等位节点(具有相同结构或表示相同数据类型的元素),将 DOM树 合并。通过比较不同页面上的同类节点,能够获知页面中哪部分是变化的,哪部分是不变的。变化的部分为数据,不变部分为模板。最后形成如上图所示的模板,页面变化部分被涂黑。这个方法类似于将多张纸叠在一起,透过光去看,就会发现变化的文字部分会比其他部分更黑。

当然了,这个方法也有缺陷,例如:页面一页面二 的标题部分,一个是蓝色,一个是绿色,虽然在人类视觉上它们相差不大,但从页面结构上绿色多了一层 <font>,作为算法很难理解,这样的样式表示他们是否有相同的含义,是否有区别。同理左侧推荐的蓝绿相间,即使作为人也很难理解它们有什么区别。

##两个核心问题

总结起来,以上四种解析模式都在尝试解决以下两个问题:

一个元素在说什么

当你打开一个页面,你怎么知道一个页面在传递什么信息?你怎么知道一个元素是文章的标题?怎么知道一个元素是作者?作为人类,我们可能会看到一个元素的位置是否在页面中间,元素的字体大小、颜色,元素前面是不是有一个 “作者:”,元素内容是否长得像一个人名/时间,上下文中这个元素都在讲什么,这篇文章是什么领域,等等。人类可能会有非常多的 经验知识 ,当看到一个页面的时候能够解读出页面上的信息。

在 “xpath / css选择器 / 正则表达式” 的解析模式中,这个工作正是人肉去完成的,人去解读这个页面,找到信息所在元素。而在 “data highlighter” 的解析模式中,也需要人在多个页面中进行标注,告诉机器每个属性所在。

但是作为计算机,是否能做到这一点?micro-data 通过开放的格式约定,通过 property 这一个特殊的属性标记告诉计算机一个元素说的是什么。而模板挖掘通过:xpath,元素class,id属性,上下文等特征去挖掘元素的含义。

但是,页面样式结构,在人类在没有足够的知识情况下,也有可能会无法解读,例如我们的爷爷奶奶可能就看不懂网页上说的是什么。同时,正如语言是有二义性的一样,页面结构也会如此,这给计算机去理解,页面说的是什么,带来了巨大的困难。

这个元素和其他的元素有什么区别

因为,大批量数据抽取是计算机的活,这需要 准确 地告诉计算机,你想要抽取的元素是哪一个。在 “xpath / css选择器 / 正则表达式” 的解析模式中,xpath、css选择器、正则表达式正是对这一信息的描述。选取一个正确的表达式,即涵盖不同页面,又和其他属性有所区分,是一件需要经验和技巧的工作。而 “data highlighter” 将这个工作交给了计算机。“模板生成和套用” 过程中也由计算机分析出了规则。

而对于 “micro-data” 来说,这个问题有些特殊。通过开放的标准格式,程序已经能够了解每个元素在说什么了,那么定位就不再有意义。但是反过来,这又何尝不是一种定位。

结构化解析

结构化解析实质是计算机对一个页面的理解,无论这种理解是人去创建规则、做出某种约定 还是 机器学习。上面列举的四种解析方式,“xpath / css选择器 / 正则表达式” 和 “data highlighter” 回答了这个元素和其他的有什么区别。 “micro-data” 利用了一个元素在说什么。而 “模板生成与提取” 同时涉及元素说什么,它在哪。

那么作为结构化解析的究级形态是怎样?我们可以假想一个人,他打开一个页面就能知道上面说的是什么,有什么样的信息,这是人类对于:通过网页获取知识的一种能力,一种方式。计算机也是一样,结构化抽取 就是 计算机从网页中获取知识的过程。“这个元素和其他的元素有什么区别” 终究只是在无法达到:计算机理解 一个页面在说什么 的辅助手段。理解 “一个元素在说什么” 乃至 “一个页面在说什么” 我认为是才是其究级形态,而结构化数据也不过是计算机,对于浩瀚互联网信息理解的一种表达罢了。

pyspider架构设计

前言

pyspider是我一年多之前做的一个爬虫架构的开源化实现。主要的功能需求是:

  • 抓取、更新调度多站点的特定的页面
  • 需要对页面进行结构化信息提取
  • 灵活可扩展,稳定可监控

而这也是绝大多数python爬虫的需求 —— 定向抓取,结构化化解析。但是面对结构迥异的各种网站,单一的抓取模式并不一定能满足,灵活的抓取控制是必须的。为了达到这个目的,单纯的配置文件往往不够灵活,于是,通过脚本去控制抓取是我最后的选择。
而去重调度,队列,抓取,异常处理,监控等功能作为框架,提供给抓取脚本,并保证灵活性。最后加上web的编辑调试环境,以及web任务监控,即成为了这套框架。

pyspider的设计基础是:以python脚本驱动的抓取环模型爬虫

  • 通过python脚本进行结构化信息的提取,follow链接调度抓取控制,实现最大的灵活性
  • 通过web化的脚本编写、调试环境。web展现调度状态
  • 抓取环模型成熟稳定,模块间相互独立,通过消息队列连接,从单进程到多机分布式灵活拓展

这与后来在某厂看到的spider系统整体架构上区别不大

pyspider

功能

webui

  • web的可视化任务监控
  • web脚本编写,单步调试
  • 异常捕获、log捕获,print捕获等

scheduler

  • 任务优先级
  • 周期定时任务
  • 流量控制
  • 基于时间周期 或 前链标签(例如更新时间)的重抓取调度

fetcher

  • dataurl支持,用于假抓取模拟传递
  • method, header, cookie, proxy, etag, last_modified, timeout 等等抓取调度控制
  • 可以通过适配类似 phantomjs 的webkit引擎支持渲染

processor

  • 内置的pyquery,以jQuery解析页面
  • 在脚本中完全控制调度抓取的各项参数
  • 可以向后链传递信息
  • 异常捕获

架构

pyspider的架构主要分为 scheduler(调度器), fetcher(抓取器), processor(脚本执行):

pyspider-arch

  • 各个组件间使用消息队列连接,除了scheduler是单点的,fetcher 和 processor 都是可以多实例分布式部署的。 scheduler 负责整体的调度控制

  • 任务由 scheduler 发起调度,fetcher 抓取网页内容, processor 执行预先编写的python脚本,输出结果或产生新的提链任务(发往 scheduler),形成闭环。

  • 每个脚本可以灵活使用各种python库对页面进行解析,使用框架API控制下一步抓取动作,通过设置回调控制解析动作。

注:output部分设计尚未决定,因为希望输出也可以很灵活地进行。现在是在脚本中有一个on_result的回调,在里面可以自行实现结果输出。

基于封禁IP名单的自动路由

年末本来很闲的,一个月把标日初级上看完了;结果前天开始被拉去做一个要求年后第一周上线的的项目。。还是本来是一个部门做的那种。。于是本月的blog只好凑字数了。。

自动墙路由基本除去apnic的国内ipv4白名单走国内方案,就剩下autoddvpn的封禁ip列表了(透明代理不考虑)。国内ip白名单的问题是,如果要玩外服DOTA,还得手动加上各种游戏的服务器IP,而autoddvpn万年不更新,很多时候根本命中不了。

于是,有了下面这个根据DNS查询记录添加封禁IP记录的方法:

  • Linux环境
  • 有VPN
  • 通过dnsmasq查询DNS,并打开日志
  • 通过匹配gfwlist的域名判断对应ip是否被封禁,然后添加到路由表中

通过脚本

logread -f 可以替换为 tail -f 日志文件
dev pptp-vpn 可以替换为建立VPN的链接的名字

不过,缺陷是。。第一次访问时需要过1分钟左右才能生效。。

另外,这个是福利: lifandb.html 来自 github/youxiachai/lifandb (请用chrome打开,如果安装了adblock-plus请先禁用)

足兆叉虫的2013

我是从来不记日子的,这导致我也不知道有些事情是2013年发生的,还是2012年发生的,亦或只是我的臆想。即便如此,2013年也是变化的一年。

跳槽,工资没涨,工作忙了2倍,但经手了更大的系统(虽然设计很渣),更多协调,带小弟,基本达到了初衷,也说不上是好是坏。搬离大学生活圈、一个人住,第一次有家的感觉,虽然依旧一个人。

想学日语,想出国,但完全没有干劲。

依旧是没有理想,没有希望的一年,就这样一觉不起就好了。

用FTP的方式访问迅雷离线

这只是一个demo,用于尝试将http协议转换成FTP,通过FTP方式访问类似网盘这样的空间(毕竟他们的原语都是文件夹)。使用 tornado ioloop 实现完全异步,在 tornado 的 iostream 之上手写了一个ftp服务器。

如果你想要快速使用:

ftp方式访问迅雷:
python -c "u='http://f.binux.me/pyproxy.zip';import urllib2,sys,tempfile;f=tempfile.NamedTemporaryFile(suffix='.zip');urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler()));f.write(urllib2.urlopen(u).read());sys.path.insert(0,f.name);f.flush();from xunlei_ftpserver import run;run();"

http串流离线内容
python -c "u='http://f.binux.me/pyproxy.zip';import urllib2,sys,tempfile;f=tempfile.NamedTemporaryFile(suffix='.zip');urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler()));f.write(urllib2.urlopen(u).read());sys.path.insert(0,f.name);f.flush();from xunlei_webserver import run;run();"

另外还有一个使用代理api方式直接共享离线空间的例子:
http://jsbin.com/uQinidA/2/quiet

github地址:https://github.com/binux/xunlei-lixian-proxy

中文简介

  • 通过ftp的方式访问你的迅雷离线空间
  • 在线串流离线空间中的视频到任何播放器
  • 完全异步化(使用tornado ioloop)
  • 这只是一个多协议转换的原理验证演示,不保证可以用于生产环境

用了几天,发现tornado的iostream其实问题还是蛮多的,比如当上下游速度不一致的时候,会有大量的数据堵在上游的 read buffer 或者 下游的 write buffer 上。因为tornado是定位于web服务器的,单个请求大都不大,但是在代理文件的时候 buffer 就会占用大量的内存。代码里面有尝试修复,但是效果不理想,在小内存的 Linux 盒子上经常因为爆内存被 kill。

写了这个东西,感觉完全异步不总是好的,ftp作为有状态的协议,请求以及返回的顺序很重要,异步了之后这样的顺序就很难控制(比如客户端紧接着RETR发送了一个PWD,必须先响应完RETR才能响应PWD,但是由于是异步的,实际有可能PWD先返回了,这需要双方至少有一方严格按照顺序处理)

动画新番订阅-从ktxp到迅雷离线

需要解决方案请直接跳到 解决方案 一节

曾经折腾过flexget+迅雷离线的新番订阅方案,但是因为越来越忙+越来越懒,这种挨个写配置文件的方式太麻烦了,之后就再也没用过。
于是我在想,用RSS追番的痛点在哪?

  • 需要挨个构造过滤条件?
  • flexget配置文件太难写,不方便下载到本地?
  • 下载的文件看不懂罗马拼音?
  • 不方便记录进度?

过滤调教

首先是需要构造过滤条件,这个好说,每一集动画,每一个字幕组发布的名字只会有集数不同,那么只需要配上关键字(字幕组/作品名/格式/语言)就可以了。
即便如此,痛点依旧存在。当季连续播放的内容还好说,OVA怎么办?OAD怎么办?TV未放送怎么办?这些很可能命名方式不一样,甚至是原来追番的字幕组压根没做!
像bilibili合集或者专有tag这样人工收集固然完美,但是作为程序员,使用人工是可耻的!

于是,设想中的方案:

  • 抓取所有资源,对抓取到的信息,通过分词等手段对资源打tag,这些第一批产生的tag称为raw tag
  • 对所有raw tag,通过推导规则生成衍生tag。(例如作品名归一化,作品名生成x月番,作品名+第x季关键词进行消歧义)
  • 对不确定信息,产生不确定tag,多个不确定tag依旧可以产生衍生tag
  • 选取主要的tag(包括raw tag,衍生tag,不确定tag)保存,以供筛选
  • 对tag进行分类

方案的核心在于tag推导,而这样的推导公式可以人工配置,也可以通过机器学习/规则聚类获得。
最后给用户展现的是一堆tag(但是同一部作品只会有一个tag,即使不同字幕组译法不一样),用户首先选择作品,之后是字幕组/格式/语言等等tag组合。而OVA等资源只要能衍生出和作品一致的tag,资源就不会漏。

下载到本地

下载管理,本地资源库管理,这些其实都是大问题。不过我们不如换个思路。以现在的用户带宽,200M的一部新番在线播放根本不成问题,但是问题在于资源从哪来:bt? ftp? http? 目前可能的资源来源无外乎这三种,直接播放不就好了嘛!对于任何主流视频播放器,http,ftp自不用说,基本没有不支持的。即使http需要cookie验证,本地做一个代理就好了。bt直接播放也不是什么困难的问题,缓冲之后流式输出,甚至可以把bt/http/ftp统统封装成http/FTP协议,内容想怎么播就怎么播。

海报查看/播放进度管理

既然上面说到,在线播放实际上可以来源于任何媒介——只要有一个本地代理,将这些内容转换成播放器支持的格式即可。那么,web化的内容展示+本地播放也就顺理成章地得以实现。
将本地代理API开放出来,任何网站只要通过这个API即可让用户通过本地的协议转换/播放来源于任何地方的任何内容,而网站不需要花费流量。

临时解决方案

好吧。。既然我连配个flexget都没时间,怎么有时间搞爬虫, tag system, 机器学习, 分词, 流式bt, http代理, 协议转换, 动画内容站呢?
既然这样,先来个简单的解决方案吧,从ktxp订阅到迅雷离线:

第一步:ktxp筛选器 保存为书签

第二步: 访问 bt.ktxp.com ,点击书签,你会发现标题都被分割成tag了,根据节目单+字幕组+格式+语言等筛选tag。选好之后,列表右上角有个黄色的小小的rss,复制它的url,比如http://bt.ktxp.com/rss-search-%E9%AD%94%E7%AC%9B.xml 这样的。保存下载。

第三步: 访问 aria2/迅雷离线订阅器,根据提示保存书签,访问迅雷离线,点击书签。你会看到一个aria2 path的框(如果你不用aria2,那就留空吧),和一个RSS订阅的框。将上面提取的RSS地址填到RSS订阅的框中(每行一个),点保存即可。现在,最近更新的新番会自动添加到你的迅雷离线里面了!

如何抓取WEB页面

好忙好忙,忙到打完dota,看完新番,写完一个外挂就懒得更新blog的地步。。。一不小心从事spider已经快3年了,也没给爬虫写过点什么。本来打算趁着十一写个什么《三天学会爬虫》什么的,但是列了下清单,其实爬虫这东西简单到爆啊。看我一天就把它搞定了(・ω<)☆

##HTTP协议
WEB内容是通过HTTP协议传输的,实际上,任何的抓取行为都是在对浏览器的HTTP请求的模拟。那么,首先通过 http://zh.wikipedia.org/wiki/超文本传输协议 来对HTTP协议来进行初步的了解:

  • HTTP通常通过创建到服务器80端口的TCP连接进行通信
  • HTTP协议的内容包括请求方式(method), url,header,body,通常以纯文本方式发送
  • HTTP返回内容包括状态码,header,body,通常以纯文本方式返回
  • header以及body间以CRLF(\r\n)分割

由于富web应用越来越盛行,单纯的HTTP协议已经不能满足 -人类的欲望- 人们的需求了,websocket, spdy等越来越多的非HTTP协议信息传输手段被使用,但是目前看来,web的主要信息依旧承载于http协议。

HTTP请求

现在打开 chrome>菜单>工具>开发者工具 切换到network面板,访问 http://www.baidu.com/,点击红色高亮处的view source

http in chrome

我们可以看到一个真实的HTTP请求的全部内容(这里的换行均为CRLF):

GET / HTTP/1.1

Host: www.baidu.com

Connection: keep-alive

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.66 Safari/537.36

DNT: 1

Accept-Encoding: gzip,deflate,sdch

Accept-Language: zh-CN,zh;q=0.8

请求中的第一行称为Request-Line,包含了请求方式 URL HTTP版本,在上面的例子中,这个请求方式(method)为 GET,URL为 /, HTTP版本为 HTTP/1.1

注意到这里的URL并不是我们访问时的 http://www.baidu.com/ 而只是一个 /,而www.baidu.com的域名在Header Host: www.baidu.com 中体现。这样表示请求的资源 / 是位于主机(host) www.baidu.com 上的,而 GET http://www.baidu.com/ HTTP/1.1 则表示请求的资源位于别的地方,通常用于http代理请求中。

请求的后续行都是Header,其中比较重要的header有 Host, User-Agent, Cookie, Referer, X-Requested-With (这个请求中未展现)。如果是POST请求,还会有body。

虽然并不需要理解HTTP请求,只要参照chrome中展示的内容模拟请求就可以抓取到内容,但是学习一下各个header的作用有助于理解哪些元素是必须的,哪些可以被忽略或修改。

更多内容可以通过以下链接进行进一步学习:
http://zh.wikipedia.org/wiki/URL
http://en.wikipedia.org/wiki/Query_string
http://en.wikipedia.org/wiki/HTTP_POST
http://en.wikipedia.org/wiki/List_of_HTTP_header_fields
抱歉很多内容无法找到好的中文版本,欢迎在留言中提供

好了,这就是一个请求的全部,只要正确模拟了method,uri,header,body 这四要素,任何内容都能抓下来,而所有的四个要素,只要打开 chrome>菜单>工具>开发者工具 切换到network面板 就能看到,怎么样,很简单吧!

现在我们就可以通过curl命令来模拟一个请求:
curl -v -H "User-Agent: Chrome" http://www.baidu.com/
其中 -v 用于显示了请求的内容,-H 指定header,具体curl的使用方式可以 man curl 或者你可以在chrome或者其他平台上找到很多类似的工具。
如果想看到请求是否正确,可以 curl http://httpbin.org/get 这个地址,它会返回经过解析的请求内容,来看看你的请求是否符合预期(http://httpbin.org/中有包括POST在内的完整API)

HTTP返回

下面展示了一个http返回的header部分,body内容被省略:

HTTP/1.1 200 OK

Date: Mon, 30 Sep 2013 06:51:11 GMT

Server: BWS/1.0

Content-Length: 4379

Content-Type: text/html;charset=utf-8

Cache-Control: private

BDPAGETYPE: 1

BDUSERID: 0

BDQID: 0x8e3bf8800bcc3d7e

Set-Cookie: BDSVRTM=2; path=/

Set-Cookie: H_PS_PSSID=3409_3381_1462_2980_3089_3502_3439; path=/; domain=.baidu.com

Set-Cookie: BAIDUID=5DDF70314DF9C307385D1821EC3B9F78:FG=1; expires=Mon, 30-Sep-43 06:51:11 GMT; path=/; domain=.baidu.com

Expires: Mon, 30 Sep 2013 06:51:11 GMT

Content-Encoding: gzip

P3P: CP=" OTI DSP COR IVA OUR IND COM "

Connection: Keep-Alive

其中第一行为 HTTP版本 状态码 状态文字说明 之后的内容都是header,其中比较重要的有:Content-Type, Set-Cookie, Location, Content-Encoding(参见 HTTP_header#Requests)。返回之后的内容就是我们看到的网页内容了。

返回中最重要的是状态码和body中的内容,状态码决定抓取是否成功(200),是否会有跳转 (HTTP状态码),内容就是我们关心的内容了。

其他

http库

在实际抓取中,选择一个方便的HTTP库会帮你解决很多http的细节问题,比如http库会帮你:

  • 建立http连接
  • 设定常用header,生成正确的http请求
  • get/post参数编码
  • 跳转重定向
  • 自动保存处理cookie
  • 返回gzip解压,内容编码

python中推荐 requests,在命令行中我一般用curl进行调试。

AJAX

现在越来越多的页面使用了AJAX技术,表现为内容并不在打开的页面的源码中,而是通过称为 AJAX 的技术,在页面打开后加载的。但实际上,AJAX也是通过HTTP传送信息的,只不过内容来自于页面发起的另一个http请求,通过查看chome中的network列出的页面所有请求,一定可以找到内容,之后只需要模拟对应的这个请求即可。

HTML内容解析

web页面大都以HTML编写,对于简单的内容提取,使用正则即可。但是对付复杂的内容提取需求正则并不是一个好的选择(甚至称不上一个正确的选择),一款HTML/XML解析器+xpath/css selector是一个更有效的选择。

富web应用

对于富web应用,可能分析AJAX请求,和内容提取的代价太高。这时可能需要上最后手段——浏览器渲染。通过 phantomjs 或类似浏览器引擎,构建一个真实的浏览器执行js、渲染页面。