用Python写爬虫

Python Crawler learning

参考书:用Python写网络爬虫

书上的例子采用的是Python 2.7版本

如何下载网页

背景调研

在深入讨论爬取一个网站之前,我们首先需要对目标站点的规模和结构进行一定程度的了解。

  1. 检查robots.txt,大多数网站都会定义这个robots协议文件,这样可以让爬虫了解爬取该网站时存在哪些限制(可以用站长工具来查看这个文件)
  2. 通过robots.txt中的Sitemap地址找到网站地图,检查网站地图提供的链接,便于爬取所有网页的链接(但是这个文件可能存在过期、缺失等情况)
  3. 估算网站大小,这会影响到如何爬取。可以用site关键词在Google上搜索,查看有多少条结果,大概估算大小
  4. 识别网站所用的技术,使用builtwith模块。pip install builtwith安装,引入后使用builtwith.parse(url)得到结果
  5. 寻找网站所有者(比如属于哪个公司),pip install python-whois安装,引入whois后使用whois.whois(url)得到结果

爬虫示例

三种爬取网站的常见方法:

  1. 爬取网站地图
  2. 遍历每个网页的数据库ID
  3. 跟踪网页链接

选用哪种方法取决于目标网站的结构

但是在此之前,要知道怎么下载网页,想要爬取网站的信息,首先要把网页内容下载下来

最简单的方法:

    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"
        )

上面的爬虫已经可用!下面是一些高级功能:

  1. 可以使用Python自带的robotparser模块,解析robots.txt文件,以避免下载禁止爬取的URL
  2. 可以使用requests模块或urllib2本身来实现代理(proxy),来绕过针对某个国家或地区的访问限制
  3. 避免爬取过快而导致被封禁或服务器过载,因此可以在两次下载之间添加延时,从而对爬虫限速
  4. 避免爬虫陷阱——一些网站会动态生成页面内容,这样就有可能出现无限多的网页,比如日历功能,可能会无止境地链接下去。简单的方法是记录到达当前网页经过了多少个链接,也就是深度,当到达最大深度时,爬虫就不再向队列添加该网页中的链接了

书上的最终版本

数据爬取

分析网页

想要了解一个网页的结构,可以使用浏览器自带的“查看源代码”或使用Firebug Lite插件,即可知道网页HTML文档的层次结构

三种抓取网页文档的数据的方法:

  1. 正则表达式
  2. BeautifulSoup模块
  3. lxml模块

正则表达式不再介绍,官方英文文档

BeautifulSoup模块

此模块可以解析网页,并提供定位内容的便捷接口

安装方法很简单:pip install beautifulsoup4sudo apt-get install Python-bs4

如何使用:

第一步是将已下载的HTML内容解析为soup文档,这个模块除了可以规范格式,还可以补全一些引号缺失、标签未闭合等语法小错误(如:

  • 1234会规范为
  • 123
  • )

    然后便可以调用此模块提供的简易方法,显然比正则要好写太多

    令人感动的是,当打开英文文档的时候,意外出现一行字,“这篇文档当然还有中文版”,这就很强

    此模块使用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选择器)

    下载缓存

    要想支持缓存,我们需要修改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封装库

    1. Mongodb
    2. pip install pymongo
    3. 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,这些页面内容在爬取时不是加载后马上下载,而是要用不同的方法:

    1. JavaScript 逆向工程
    2. 渲染 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需要基于一个浏览器(驱动),如:

    1. FireFox: geckodriver
    2. 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)用于测试用户是否为真实人类,一个典型的验证码由扭曲的文本组成,此时计算机程序难以解析,但人类仍然可以阅读

    加载验证码图像

    安装Pillowsudo 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
    

    你可能感兴趣的:(用Python写爬虫)