Python Crawler learning
参考书:用Python写网络爬虫
书上的例子采用的是
Python 2.7
版本
如何下载网页
背景调研
在深入讨论爬取一个网站之前,我们首先需要对目标站点的规模和结构进行一定程度的了解。
- 检查
robots.txt
,大多数网站都会定义这个robots协议文件,这样可以让爬虫了解爬取该网站时存在哪些限制(可以用站长工具来查看这个文件) - 通过
robots.txt
中的Sitemap
地址找到网站地图,检查网站地图提供的链接,便于爬取所有网页的链接(但是这个文件可能存在过期、缺失等情况) - 估算网站大小,这会影响到如何爬取。可以用
site
关键词在Google
上搜索,查看有多少条结果,大概估算大小 - 识别网站所用的技术,使用
builtwith
模块。pip install builtwith
安装,引入后使用builtwith.parse(url)
得到结果 - 寻找网站所有者(比如属于哪个公司),
pip install python-whois
安装,引入whois
后使用whois.whois(url)
得到结果
爬虫示例
三种爬取网站的常见方法:
- 爬取网站地图
- 遍历每个网页的数据库ID
- 跟踪网页链接
选用哪种方法取决于目标网站的结构
但是在此之前,要知道怎么下载网页,想要爬取网站的信息,首先要把网页内容下载下来
最简单的方法:
import urllib2
def download1(url):
return urllib2.urlopen(url).read()
但是这里遇到不存在的页面的时候,会抛出urllib2.URLError
,需要改进:
import urllib2
def download2(url):
print "Downloading:", url
try:
html = urllib2.urlopen(url).read()
except urllib2.URLError as e:
print "Downloading error:", e.reason
html = None
except:
print "Unknown error!"
html = None
return html
特殊的一些情况:
遇到临时性的错误,如服务器过载返回503 Service Unavailable
,可以重新下载。这种情况针对的是遇到5xx
的错误
import urllib2
# -*- coding: utf-8 -*-
def download3(url, num_retries = 2):
print "Downloading:", url
try:
html = urllib2.urlopen(url).read()
except urllib2.URLError as e:
print "Downloading error:", e.reason
html = None
if num_retries > 0:
if hasattr(e, 'code') and 500 <= e.code < 600:
# 默认遇到5xx的错误的时候重试两次(共3次的访问)
return download3(url, num_retries-1)
except:
print "Unknown error!"
html = None
return html
print download3("http://httpstat.us/500")
在发送http
包的时候,urllib2
默认使用Python-urllib/2.7
作为用户代理下载网页内容(2.7
为版本号),如果网站封禁了这个默认的用户代理,就会出现拒绝访问的提示,所以可以通过设置用户代理的方式使下载更加可靠,下面的例子将代理改为Web Scraping with Python
的缩写:
def download4(url, user_agent = 'wswp', num_retries = 2):
print "Downloading:", url
headers = {'User-agent': user_agent}
request = urllib2.Request(url, headers=headers)
try:
html = urllib2.urlopen(request).read()
except urllib2.URLError as e:
print "Downloading error:", e.reason
html = None
if num_retries > 0:
if hasattr(e, 'code') and 500 <= e.code < 600:
return download4(url, user_agent, num_retries-1)
except:
print "Unknown error!"
html = None
return html
上面的版本是我们目前的最终版本,后面import download
引入的就是这个文件
网站地图爬虫
通常以一个很简单的正则表达式就可以从网站地图中提取链接,如:从
标签中提取URL
正则表达式
# -*- coding: utf-8 -*-
import download
import re
def crawl_sitemap(url):
# 爬取广外官网的网站地图
sitemap = download.download4(url)
links = re.findall(r'(.*?)', sitemap)
return links
if __name__ == '__main__':
links = crawl_sitemap('http://www.gdufs.edu.cn/wzdt.htm')
for link in links:
print link[0], link[2]
ID遍历爬虫
许多新闻网站的URL结构都是news/数字
的形式,数字一般是顺序的,所以只需要遍历一遍,即可抓取
# -*- coding: utf-8 -*-
import download
def crawl_by_id(prefix, ext, start_id, end_id):
res = []
for page in range(start_id, end_id + 1):
url = prefix + str(page) + ext
html = download.download4(url)
res.append((url, html))
return res
if __name__ == '__main__':
# tuples = crawl_by_id("http://news.gdufs.edu.cn/Item/", ".aspx", 88990, 88998)
for url, html in tuples:
print url
print html
print "------------------------------------------------------------------------------"
链接爬虫
以上的两种爬虫方式都比较简单,所以,只要这两种技术可用,就应当使用其进行爬取
有一种需求是,我们需要让爬虫表现得更像普通用户,跟踪链接,访问感兴趣的内容。这样的后果是会下载大量我们并不需要的网页,这里就需要通过使用正则表达式来确定需要下载哪些页面
import download
import re
from collections import deque
def crawl_by_link(seed_url, link_regex):
"""
给定一个url,然后开始爬取里面的链接,然后放入队列逐个爬取,不断循环
:param seed_url
:param link_regex: 匹配的url再筛选
"""
# 使用deque进行栈或队列的出入操作比较快
crawl_queue = deque([seed_url])
while crawl_queue:
url = crawl_queue.popleft()
html = download.download4(url)
for link in get_links(html):
# 这里设置一个正则,用于限制某些特殊条件
if re.search(link_regex, link):
print "爬到了:", link
crawl_queue.append(link)
def get_links(html):
"""
从html文档中匹配出url并返回list
:param html
:return: list
"""
# 注意以下正则,用于匹配网页中的url
webpage_regex = re.compile(']+href=["\'](.*?)["\']', re.IGNORECASE)
return webpage_regex.findall(html)
if __name__ == '__main__':
# 爬取新闻
crawl_by_link(
"http://news.gdufs.edu.cn/",
"/Item/[0-9]+\.aspx"
)
以上的方法,引入了一个队列,用于放置从seed_url
爬取的链接,然后逐个出队列进行爬取,然后将爬取的链接不断放入队列,循环得到结果
但是有一个问题,执行代码之后,马上就跳出了,原因是HTML
中的链接很多都是用相对路径表示的,如/Item/80998.aspx
,但是urllib2
库并不知道上下文,所以无法定位网页
解决方案:使用urlparse
模块(内建模块),将相对路径转换成绝对路径
于是,
import download
import re
from collections import deque
import urlparse
def crawl_by_link(seed_url, link_regex):
"""
给定一个url,然后开始爬取里面的链接,然后放入队列逐个爬取,不断循环
:param seed_url
:param link_regex: 匹配的url再筛选
"""
# 使用deque进行栈或队列的出入操作比较快
crawl_queue = deque([seed_url])
while crawl_queue:
url = crawl_queue.popleft()
html = download.download4(url)
for link in get_links(html):
# 这里设置一个正则,用于限制某些特殊条件
if re.search(link_regex, link):
link = urlparse.urljoin(seed_url, link)
print "爬到了:", link
crawl_queue.append(link)
def get_links(html):
"""
从html文档中匹配出url并返回list
:param html
:return: list
"""
# 注意以下正则,用于匹配网页中的url
webpage_regex = re.compile(']+href=["\'](.*?)["\']', re.IGNORECASE)
return webpage_regex.findall(html)
if __name__ == '__main__':
# 爬取新闻
crawl_by_link(
"http://news.gdufs.edu.cn/",
"/Item/[0-9]+\.aspx"
)
这里发现一个问题,这个爬虫停不下来,并且由于有“上一条”和“下一条”的链接,所以会不断循环已经访问过的网页。要想避免重复爬取相同的链接,我们需要记录哪些链接已经被爬取过(利用集合set
的特性就很容易做到)
import download
import re
from collections import deque
import urlparse
def crawl_by_link(seed_url, link_regex):
"""
给定一个url,然后开始爬取里面的链接,然后放入队列逐个爬取,不断循环
:param seed_url
:param link_regex: 匹配的url再筛选
"""
# 使用deque进行栈或队列的出入操作比较快
crawl_queue = deque([seed_url])
# 已经查看过的链接
seen = set([seed_url])
while crawl_queue:
url = crawl_queue.popleft()
html = download.download4(url)
for link in get_links(html):
# 这里设置一个正则,用于限制某些特殊条件
if re.search(link_regex, link):
link = urlparse.urljoin(seed_url, link)
if link not in seen:
seen.add(link)
print "爬到了:", link
crawl_queue.append(link)
def get_links(html):
"""
从html文档中匹配出url并返回list
:param html
:return: list
"""
# 注意以下正则,用于匹配网页中的url
webpage_regex = re.compile(']+href=["\'](.*?)["\']', re.IGNORECASE)
return webpage_regex.findall(html)
if __name__ == '__main__':
# 爬取新闻
crawl_by_link(
"http://news.gdufs.edu.cn/",
"/Item/[0-9]+\.aspx"
)
上面的爬虫已经可用!下面是一些高级功能:
- 可以使用
Python
自带的robotparser
模块,解析robots.txt
文件,以避免下载禁止爬取的URL
- 可以使用
requests
模块或urllib2
本身来实现代理(proxy
),来绕过针对某个国家或地区的访问限制 - 避免爬取过快而导致被封禁或服务器过载,因此可以在两次下载之间添加延时,从而对爬虫限速
- 避免爬虫陷阱——一些网站会动态生成页面内容,这样就有可能出现无限多的网页,比如日历功能,可能会无止境地链接下去。简单的方法是记录到达当前网页经过了多少个链接,也就是深度,当到达最大深度时,爬虫就不再向队列添加该网页中的链接了
书上的最终版本
数据爬取
分析网页
想要了解一个网页的结构,可以使用浏览器自带的“查看源代码”或使用Firebug Lite
插件,即可知道网页HTML
文档的层次结构
三种抓取网页文档的数据的方法:
- 正则表达式
-
BeautifulSoup
模块 -
lxml
模块
正则表达式不再介绍,官方英文文档
BeautifulSoup模块
此模块可以解析网页,并提供定位内容的便捷接口
安装方法很简单:pip install beautifulsoup4
或 sudo apt-get install Python-bs4
如何使用:
第一步是将已下载的HTML
内容解析为soup
文档,这个模块除了可以规范格式,还可以补全一些引号缺失、标签未闭合等语法小错误(如:
会规范为
)
然后便可以调用此模块提供的简易方法,显然比正则要好写太多
令人感动的是,当打开英文文档的时候,意外出现一行字,“这篇文档当然还有中文版”,这就很强
此模块使用Python
语言编写,所以速度上比较慢,但是安装和使用都很方便,适合小规模、时间上不严格的爬取。
Lxml模块(推荐使用)
与BeautifulSoup
模块不同的是,该模块使用C
语言编写,解析速度比前者快。
安装方法
简单的使用例子:
>>> import lxml.html
>>> broken_html = '- Area
- Population
'
>>> tree = lxml.html.fromstring(broken_html)
>>> fixed_html = lxml.html.tostring(tree, pretty_print=True)
>>> print fixed_html
- Area
- Population
>>> li1 = tree.cssselect('ul > li')[1]
>>> print li1.text_content()
Population
这里可能会出现的错误是ImportError: cssselect does not seem to be installed
,只需要pip install cssselect
即可
可以看到,Lxml
可以使用CSS选择器来获取节点的内容(内部实现中,实际上是将CSS选择器转换为等价的XPath选择器)
- 选择所有标签:
*
- 选择
标签:
a
- 选择所有
class="link"
的元素:.link
- 选择
class="link"
的标签:
a.link
- 选择
id="home"
的标签:
a#home
- 选择父元素为
标签的所有
标签:
a > span
- 选择
标签内部的所有
标签:
a span
- 选择
title
属性为"home"
的所有标签:
a[title=home]
下载缓存
要想支持缓存,我们需要修改download
函数,使其在URL
下载之前进行缓存检查,另外,还需要把限速功能移到函数内部,只有真正发生下载时才会触发限速,而在加载缓存时不会触发
参考代码如下:
import urlparse
import urllib2
import random
import time
from datetime import datetime, timedelta
import socket
DEFAULT_AGENT = 'wswp'
DEFAULT_DELAY = 5
DEFAULT_RETRIES = 1
DEFAULT_TIMEOUT = 60
class Downloader:
def __init__(self, delay=DEFAULT_DELAY, user_agent=DEFAULT_AGENT, proxies=None, num_retries=DEFAULT_RETRIES, timeout=DEFAULT_TIMEOUT, opener=None, cache=None):
socket.setdefaulttimeout(timeout)
self.throttle = Throttle(delay)
self.user_agent = user_agent
self.proxies = proxies
self.num_retries = num_retries
self.opener = opener
self.cache = cache
def __call__(self, url):
result = None
if self.cache:
try:
result = self.cache[url]
except KeyError:
# url is not available in cache
pass
else:
if self.num_retries > 0 and 500 <= result['code'] < 600:
# server error so ignore result from cache and re-download
result = None
if result is None:
# result was not loaded from cache so still need to download
self.throttle.wait(url)
proxy = random.choice(self.proxies) if self.proxies else None
headers = {'User-agent': self.user_agent}
result = self.download(url, headers, proxy=proxy, num_retries=self.num_retries)
if self.cache:
# save result to cache
self.cache[url] = result
return result['html']
def download(self, url, headers, proxy, num_retries, data=None):
print 'Downloading:', url
request = urllib2.Request(url, data, headers or {})
opener = self.opener or urllib2.build_opener()
if proxy:
proxy_params = {urlparse.urlparse(url).scheme: proxy}
opener.add_handler(urllib2.ProxyHandler(proxy_params))
try:
response = opener.open(request)
html = response.read()
code = response.code
except Exception as e:
print 'Download error:', str(e)
html = ''
if hasattr(e, 'code'):
code = e.code
if num_retries > 0 and 500 <= code < 600:
# retry 5XX HTTP errors
return self._get(url, headers, proxy, num_retries-1, data)
else:
code = None
return {'html': html, 'code': code}
class Throttle:
"""Throttle downloading by sleeping between requests to same domain
"""
def __init__(self, delay):
# amount of delay between downloads for each domain
self.delay = delay
# timestamp of when a domain was last accessed
self.domains = {}
def wait(self, url):
"""Delay if have accessed this domain recently
"""
domain = urlparse.urlsplit(url).netloc
last_accessed = self.domains.get(domain)
if self.delay > 0 and last_accessed is not None:
sleep_secs = self.delay - (datetime.now() - last_accessed).seconds
if sleep_secs > 0:
time.sleep(sleep_secs)
self.domains[domain] = datetime.now()
主要要关注的地方是__call__()
函数中,首先会检查缓存是否已经定义,然后在缓存列表中检查url
是否已经被缓存,最后检查之前的下载中是否遇到了服务端错误,都没有问题的话,则表明该缓存结果可用。否则,需要下载该url
建立了爬虫的基本架构之后,开始构建实际的缓存
磁盘缓存
文件系统缓存
需要将URL安全地映射为跨平台的文件名(各种文件系统有不同的限制字符和最大长度限制),参考代码
为了最小化缓存所需的磁盘空间。可以对下载得到的HTML文件进行压缩处理,只需要使用zlib
压缩序列化字符串即可
fp.write(zlib.compress(pickle.dumps(result)))
return pickle.loads(zlib.decompress(fp.read()))
压缩后,缓存占用的空间变小,但是整体的爬取速度会变慢(因为多了一个步骤)
由于网页内容随时都有可能发生变化,所以缓存要设置过期时间,以便爬虫知道何时需要重新下载网页。实现方法很简单:可以将过期时间与爬取的正文内容一起存入缓存中,取出缓存的同时检查缓存是否过期即可
文件系统的优势是无须安装其他模块,比较容易实现。缺点是,受制于本地文件系统的限制,上面为了将URL映射为安全文件名,应用了多种限制。但是这可能造成映射为相同文件名的情况(例子略),解决方案可以是使用URL的哈希值作为文件名。但是还有一个问题,每个目录的最大文件数是65535,文件系统可存储的文件总数也有限制,于是需要把多个缓存网页合并到一个文件中,并使用类似B+树的算法进行索引,并不用自己实现,使用实现这类算法的数据库即可
数据库缓存
需要缓存的数据,不需要任何复杂的连接操作,所以选用NoSQL
数据库更容易扩展,这里使用Mongodb
Python
使用Mongodb
需要安装Mongodb
并安装Python
封装库
- Mongodb
pip install pymongo
-
mongod -dbpath .
(在项目目录中执行)
>>> from pymongo import MongoClient
>>> client = MongoClient('localhost', 27017)
>>> client
MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True)
Mongodb-Python 英文官方文档
使用Mongodb
缓存,无法按照给定时间精确清理过期记录,会存在至多1分钟的延时,不过缓存通常设定的时间是几周或几个月,所以没有很大的影响
数据库缓存同样可以引入zlib
压缩
Mongodb
进行缓存不会磁盘缓存快(可能更慢),不过,它可以让我们免受文件系统的各种限制
并发下载
之前的爬虫都是串行下载网页的,在爬取大型网站的时候,耗时就会非常恐怖,于是可以使用多线程或多进程的方法来下载,节省很多时间
多线程爬虫 和 多进程爬虫
多线程爬虫请求内容速度过快,可能会造成服务器过载,或者IP地址被封禁。为了避免这一问题,可以设置一个delay
标识,用于设定请求同一域名的最小时间间隔
Python 的多线程
此外,在多处理器的情况下,多进程也可以提高很多效率,为了解决进程间数据访问的问题,可以采用Mongodb
等数据库或消息传输工具来解决
要想获得更好的性能,就需要在多台服务器上分布式部署爬虫,并且所有服务器都要指向同一个Mongodb
队列实例
爬取动态内容
测试链接
大多数主流网站的功能都非常依赖于JavaScript
,这些页面内容在爬取时不是加载后马上下载,而是要用不同的方法:
- JavaScript 逆向工程
- 渲染 JavaScript
逆向工程
要想抓取使用JavaScript (AJAX)
动态加载的数据,我们需要了解网页是如何加载该数据的,该过程被称为逆向工程
实际上,很多AJAX
响应返回的数据都是JSON
格式的,而且是通过访问某个特定的链接得到,所以跟之前一样,只要下载那个链接的内容,就可以直接得到Json
数据,不需要有其他操作,反而更简单
“特定链接”可能长这样:
example.webscraping.com/ajax/search.json?page=0&page_size=10&search_term=a
结果:
{"records": [{"pretty_link": "", "country": "Afghanistan", "id": 2113273}, {"pretty_link": "", "country": "Aland Islands", "id": 2113274}, {"pretty_link": "", "country": "Albania", "id": 2113275}, {"pretty_link": "", "country": "Algeria", "id": 2113276}, {"pretty_link": "", "country": "American Samoa", "id": 2113277}, {"pretty_link": "", "country": "Andorra", "id": 2113278}, {"pretty_link": "", "country": "Angola", "id": 2113279}, {"pretty_link": "", "country": "Anguilla", "id": 2113280}, {"pretty_link": "", "country": "Antarctica", "id": 2113281}, {"pretty_link": "", "country": "Antigua and Barbuda", "id": 2113282}], "num_pages": 22, "error": ""}
这样每次只能获取10条,可以通过人工分析参数,尝试不同的边界条件,快速匹配到更多的数据,如:使用*
, (空格)
, .
等
渲染JavaScript
有些网站由于产生的JavaScript
代码是机器生成的压缩版,即使使用工具还原,有一些变量名也已经丢失,所以可以用渲染引擎对JavaScript
进行渲染,从而获取加载了JavaScript
的内容
可以使用Webkit
+ Qt
+ PyQt 或 PySide
来执行渲染(复杂,略)
Selenium
使用Webkit
库可以自定义浏览器渲染引擎,这样就能完全控制想要执行的行为。但是如果不需要这么高的灵活性,就可以使用Selenium
,它提供使浏览器自动化的API
接口
安装:sudo pip install selenium
文档
由于运行Selenium
需要基于一个浏览器(驱动),如:
- FireFox: geckodriver
- Chrome: chromedriver
下载之后解压,将可执行文件放入PATH
中,如:/usr/local/bin/
然后,
>>> from selenium import webdriver
>>> driver = webdriver.Chrome()
这样会在桌面系统中打开一个浏览器窗口,后面可以通过调用driver
对象的方法来操作浏览器
>>> driver.get("http://example.webscraping.com/search")
>>> driver.find_element_by_id('search_term').send_keys("abc")
>>> driver.find_element_by_id('search_term').send_keys(".")
>>> driver.find_element_by_id('search_term').clear()
>>> driver.find_element_by_id('search_term').send_keys(".")
>>> js = "document.getElementById('page_size').options[1].text = '1000'"
>>> driver.execute_script(js)
>>> driver.find_element_by_id('search').click()
>>> driver.implicitly_wait(30)
>>> links = driver.find_elements_by_css_selector('#results a')
>>> countries = [link.text for link in links]
>>> countries
[u'Afghanistan', u'Aland Islands', ..., u'Zambia', u'Zimbabwe']
>>> links[0].get_attribute("href")
u'http://example.webscraping.com/view/Afghanistan-1'
>>> driver.close()
以上例子控制了输入、清空、执行脚本、设置等待AJAX
请求返回的超时时间、多种方法查找元素、获取属性、关闭浏览器等操作
逆向工程和与渲染网页的比较
浏览器渲染可以节省了解后端工作原理的时间,但是渲染增加了开销,比单纯下载HTML
慢,并且由于需要轮询网页,在网络较慢的时候经常会失败,所以适用于短期解决方案,而长期的解决方案还是使用逆向工程
表单交互
之前的爬虫总是返回相同的内容,这是因为缺少了与服务器的交互,这里介绍如何根据用户输入返回对应的内容
发送POST请求提交表单
GET请求可以直接在URL
后拼接query string
即可,而POST方法可以传递更多、较安全、不同编码的数据
POST方法的默认编码类型是application/x-www-form-urlencoded
,此时所有非字母数字类型的字符都需要转换为十六进制的ASCII
值。如果数据中包含大量非字母数字类型的时候,效率就会非常低,比如上传二进制文件的时候,此时应该定义multipart/form-data
作为编码类型,这种编码类型不会对输入进行编码,而是使用MIME
协议将其作为多个部分进行发送
除了编码类型、action
的地址、method
属性(POST/GET
),还有具体的数据传输,通常这些数据不是全部可视,会有一些隐藏域,并且需要知道这些输入的数据的name
属性是什么,可以通过查看源代码或定义一个函数,提取表单中所有input标签的详情
>>> import lxml.html
>>> def parse_form(html):
... tree = lxml.html.fromstring(html)
... data = {}
... for e in tree.cssselect('form input'):
... if e.get('name'):
... data[e.get('name')] = e.get('value')
... return data
...
>>> import pprint
>>> import urllib2
>>> html = urllib2.urlopen("http://auth.gdufs.edu.cn/wps/portal/newhome/!ut/p/c5/04_SB8K8xLLM9MSSzPy8xBz9CP0os3j_QA8DTycLI0t3Zw9TA09fD6MgDwtXQwN3U30_j_zcVP2CbEdFALkG2FQ!/dl3/d3/L2dBISEvZ0FBIS9nQSEh/").read()
>>> form = parse_form(html)
>>> pprint.pprint(form)
{'password': None, 'randomFlag': '0', 'username': None}
HTTP是无状态的,所以登录的结果是,浏览器和服务器会有一个cookie
数据来让网站识别和跟踪用户。于是,除此之外,还要保存一个cookie
,并带着这个数据去访问网站内容
参考代码
def login_cookies():
"""working login
"""
cj = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
html = opener.open(LOGIN_URL).read()
data = parse_form(html)
data['email'] = LOGIN_EMAIL
data['password'] = LOGIN_PASSWORD
encoded_data = urllib.urlencode(data)
request = urllib2.Request(LOGIN_URL, encoded_data)
response = opener.open(request)
print response.geturl()
return opener
还有一种方法是从浏览器中直接加载cookie
,不过不同的系统、浏览器,存储的路径都不相同,所以代码非常复杂(略)
Mechanize模块实现自动化表单处理
安装:sudo pip install mechanize
Mechanize文档(扶墙而入)
>>> # 没有权限的时候
>>> br.open('http://jxgl.gdufs.edu.cn/jsxsd/framework/xsMain.jsp')
Traceback (most recent call last):
File "", line 1, in
File "/usr/local/lib/python2.7/dist-packages/mechanize/_mechanize.py", line 203, in open
return self._mech_open(url, data, timeout=timeout)
File "/usr/local/lib/python2.7/dist-packages/mechanize/_mechanize.py", line 255, in _mech_open
raise response
mechanize._response.httperror_seek_wrapper: HTTP Error 500: Internal Server Error
>>> # 登录的形式访问
>>> br.open('http://jxgl.gdufs.edu.cn/jsxsd/')
>>
>>> # nr表示第几个form,从0开始记起
>>> br.select_form(nr=0)
>>> print br.form
=) (readonly)>>
>>> br['USERNAME'] = '2013100****'
>>> br['PASSWORD'] = '******'
>>> response = br.submit()
>>> br.open('http://jxgl.gdufs.edu.cn/jsxsd/framework/xsMain.jsp')
>>
>>> response.read()
'\r\n\r\n\r\n\r\n\r\n...'
验证码处理
验证码(CAPTCHA)用于测试用户是否为真实人类,一个典型的验证码由扭曲的文本组成,此时计算机程序难以解析,但人类仍然可以阅读
加载验证码图像
安装Pillow
: sudo pip install Pillow
Pillow
提供一个便捷的Image
类,其中包含了很多用于处理验证码图像的高级方法
光学字符识别
光学字符识别(Optical Character Recognition, OCR)用于从图像中抽取文本,这里使用开源的Tesseract OCR
引擎
安装:
sudo apt-get install tesseract-ocr
sudo pip install pytesseract
如何处理?
以上的两个核心步骤,加载验证码图像和光学字符识别理论上就可以得到结果,但是验证码中可能有背景噪音,所以可以先用Pillow
类中的一些阈值化处理的函数,使图片变得更清晰,然后再传递给OCR引擎识别,提高准确度
import urllib2
from io import BytesIO
import lxml.html
from PIL import Image
import pytesseract
REGISTER_URL = "http://example.webscraping.com/user/register"
def get_captcha(html, cssselect):
tree = lxml.html.fromstring(html)
img_data = tree.cssselect(cssselect)[0].get('src')
img_data = img_data.partition(',')[-1]
binary_img_data = img_data.decode('base64')
file_like = BytesIO(binary_img_data)
img = Image.open(file_like)
return img
def captcha_to_string(img):
img.save('captcha_image/origin.png')
gray = img.convert('L')
gray.save('captcha_image/gray.png')
bw = gray.point(lambda x: 0 if x < 1 else 255, '1')
bw.save('captcha_image/thresholded.png')
res = pytesseract.image_to_string(bw)
return res
html = urllib2.urlopen(REGISTER_URL).read()
img = get_captcha(html, "div#recaptcha img")
res = captcha_to_string(img)
print "res: ", res