Django bulk_update可能卡死问题

在一个多年前的历史项目中,我们发现,django cron job出现了卡死现象。 应用逻辑没有被完整执行直到结束,程序已经僵死了。

调试分析卡死的Python进程,请参考: https://wiki.python.org/moin/DebuggingWithGdb

通过调试进程,我们发现应用卡死在了

Domain.objects.bulk_update(update_domain_list, [‘ips_from_fuxi’])

如下图所示:

回到源代码中,在上下文找到了原因,原来,需要bulk update的列表,没有在循环体内部正确被初始为空。 需要update的列表出现了持续增长,最终导致bulk update 超大的列表,卡死应用程序。

正确初始化列表,即可解决bulk update hung死的问题。

测试前面写的Django数据防爬程序

写一小段测试代码来测试前面写的数据防爬程序,不停访问某个页面:

实际上REMOTE_HOST是服务器根据自己DNS解析得到的,但我这里测试它能否直接在header中伪装:

import httplib  
import sys
import datetime
import random

headers = {'User-Agent': 'Googlebot/2.1 (+http://www.google.com/bot.html)',
           'Connection': 'keep-alive',
           'REMOTE_HOST': 'www.googlebot.com'}

while True:
    conn = httplib.HTTPConnection("www.fachun.net")
    conn.request(method='GET', url='/album/972-Nine%20Objects%20Of%20Desire/?' + str(random.random()), headers=headers)
    response = conn.getresponse()
    print response.status, datetime.datetime.now()
    if response.status == 403:
        print 'forbidden error, exit'
        sys.exit(-1)
    conn.close()

test-anti-spider-app
数据库中的内容: 继续阅读测试前面写的Django数据防爬程序

Django中实现一个简单的数据防爬取系统

www.fachun.net是我做的一个音乐资源站,因为资源丰富,容易成为他人抓取的目标。

被抓取资源事小,另一方面,爬虫的频繁访问还会严重占用服务器资源。

前期写代码的时候,我已经考虑到防爬问题,做了一些简单的事,比如避免使用数字ID,而是ID + 名字。

例如一个歌手的名字是ABC,ID是36,最终的URL就是:

http://www.fachun.net/musician/36-ABC/

这是爬虫难以直接猜解的。

今天我再写一些额外的代码来限制单个IP请求的频率,实现防爬。有几个要点:

1. 使用MySQL Memory数据库引擎来存储用户IP地址

因为多了个过滤层,响应时间必然增加。应该尽量减小这个影响,把IP放在内存中可以节省查找时间。

原来的应用是InnoDB引擎,所以,需要新建数据库,并在settings.DATABASES中添加它。

2. 目前我的过滤条件是10分钟内请求超过100次(已改为60次),封IP十分钟

3. 搜索引擎百度、Google等的爬虫应该不受过滤,因此,需要特别处理搜索引擎的爬虫。

设IP白名单会再次影响效率,我不希望设用白名单来筛选。

只是检查REMOTE_HOST是否包含googlebot.com、crawl.baidu.com等字符串

1. 修改配置文件,增加数据库

首先新建一个app,我这里命名为robotkiller,执行:

manage.py startapp robotkiller

然后在工程settings.py文件中添加一个数据库,我仍然用同名的robotkiller,并在INSTALLED_APPS中添加robotkiller。

DATABASES = {
    'default': {...},
    'robotkiller': {
        'ENGINE': 'django.db.backends.mysql', 
        'NAME': 'robotkiller',                      
        'USER': 'your_user',
        'PASSWORD': 'your_pass',
        'HOST': '127.0.0.1',                    
        'PORT': '3306',
        'OPTIONS': {
            "init_command": "SET storage_engine=MEMORY",
        }
    }
}

models.py中的内容是这样的:

from django.db import models

class RobotKiller(models.Model):
    id = models.IntegerField(primary_key=True)
    ip = models.CharField(max_length=16)    #IP地址
    visits = models.IntegerField()          #请求次数
    time = models.DateTimeField()           #第一次发起请求的时间
    class Meta:
        db_table = 'robotkiller'

记得为ip字段建一个索引(让MySQL为内存数据库再维护个索引,是对是错,暂不深究):

我在settings.py中设置了时区为’Asia/chongqing’,所以,取得时间差,不应该直接用datetime,

而是用django.utils.timezone。下面是我的视图函数:

from models import RobotKiller
from django.utils import timezone

max_visits = 100
min_seconds = 600

def filterIP(request):
    domain = request.META.get('REMOTE_HOST')
    white_list = ['googlebot.com', 'crawl.baidu.com', 'sogou.com', 'bing.com', 'yahoo.com']
    for bot_domain in white_list:
        if domain.find(bot_domain) > 0:
            return bot_domain

    user_ip = request.META['REMOTE_ADDR']

    try:
        record = RobotKiller.objects.using('robotkiller').get(ip=user_ip)
    except RobotKiller.DoesNotExist:
        RobotKiller.objects.using('robotkiller').create(ip=user_ip, visits=1, time=timezone.now())
        return

    passed_seconds = (timezone.now() - record.time).seconds

    if record.visits > max_visits and passed_seconds < min_seconds:
        raise Exception('user ip banned.')
    else:
        if passed_seconds < min_seconds:
            record.visits = record.visits + 1
            record.save()
        else:
            record.visits = 1
            record.time = timezone.now()
            record.save()

上述代码就是基本的过滤逻辑。在urls.py中添加:

from robotkiller import filterIP

然后在需要防爬取的对应app的views.py中添加该函数:

def checkIP(request):
    try:
        filterIP(request)
    except Exception, e:
        if unicode(e) == 'user ip banned.':
            raise PermissionDenied()

接着,在所有视图函数的第一行添加checkIP(request) 就可以过滤所有页面的请求了。

当用户在10分钟内请求了超过100个页面,会得到一个403错误。

可以在模板文件夹下新建一个403.html,自定义出错提示了。

django makemessages时出现DjangoUnicodeDecodeError

今天对www.fachun.net做了些改动,需要增加一点翻译。

当我执行django-admin.py makemessages -l en时,遇到了DjangoUnicodeDecodeError,

DjangoUnicodeDecodeError: ‘utf8’ codec can’t decode byte 0xcd in position 12: in
valid continuation byte. You passed in ‘2013/10/11 \xcd\xea\xb3\xc9\xc1\xcb\xd5
\xbe\xb5\xe3\xb5\xc4\xb9\xfa\xbc\xca\xbb\xaf\xa3\xac\xb2\xb9\xb3\xe4\xc1\xcb\xd3
\xa2\xce\xc4\xb7\xad\xd2\xeb\n\n2013/10/12 \xca\xb5\xcf\xd6\xc1\xcb\xa1\xb0\xbb
\xbb\xd2\xbb\xc5\xfa\xb9\xa6\xc4\xdc\xa1\xb1\xa1\xa2\xd4\xda\xb2\xbb\xcd\xac\xd3
\xef\xd1\xd4\xd1\xa1\xd4\xf1\xcf\xc2\xb8\xdf\xc1\xc1\xb2\xbb\xcd\xac\xb5\xc4\xce
\xc4\xd7\xd6,\xca\xb5\xcf\xd6\xc1\xcb\xb6\xe0\xd3\xef\xd1\xd4URL\xb2\xee\xd2\xec
\xbb\xaf\xb5\xc4\xd3\xc5\xbb\xaf\n\n2013/’ (<type ‘str’>)

看名字知道是出现了uniocde解码错误。

但自己一向非常注意编码问题,数据库、文件都是以utf8编码保存的。

因为错误信息没有提到是在哪个文件出现问题,

我在python里将上述字符串打印出来,发现竟然是更新记录:

DjangoUnicodeDecodeError-print

这样就定位到出现问题的文件,它是app目录下的一个logs.txt文件。

按个人理解,这个文件是不应该被makemessages处理的,它既非模板,又不是.py文件或.po文件。

但makemessages不想遗漏需要处理的文件,也可以理解。

为什么这个文件会出现问题呢?因为它是我用记事本在windows下直接创建和保存的。

在windows下,记事本默认地将这个文件以”ANSI code page”编码保存。

我使用win7中文版(codepage = 936),文件就默认以GBK编码保存,跟我在cmd下默认是gbk编码一样。

 

知道了出错的原因,再次用记事本打开文件,另存utf8就行了。

不得不说,windows的记事本真是很奇葩。

有Ctrl + S保存习惯的同学可以考虑Alt + F + A另存为utf8,或者用editPlus

Django i18n多语言的URL差异化解决方案

昨天我已经对站点“春天音乐”进行了国际化,添加了英文翻译。

但当时后端是根据session和cookie记录用户的语言设定,

这造成的问题是,同一URL,存在一个中文版、一个英文版。

搜索引擎的爬虫进入后,大多只能抓取并保存对应的中文版(默认语言),而遗失了对应的英文版。

因此,必须对中文版和英文版进行URL差异化。有两种选择:

1. 增加一个参数,类似 http://www.fachun.net/?lang=en

2. 增加一个前缀,类似 http://www.fachun.net/en/

因为第二种更好看,我选择了第二种。整个实现过程较繁琐,下面提供我采用的解决方案。

1. 安装django-localeurl

我安装的版本是2.0.1: https://pypi.python.org/pypi/django-localeurl

该程序包的作用,是允许你在URL中设定语言,而不需要修改原有的URL配置(urls.py中的设置)。

安装完成后,编辑settings.py文件配置:

LANGUAGE_CODE = ‘zh-CN’
LANGUAGES = {
(‘zh-cn’, ‘中文’),
(‘en’, ‘English’),
}
PREFIX_DEFAULT_LOCALE = False
LOCALEURL_USE_ACCEPT_LANGUAGE = True

MIDDLEWARE_CLASSES = (
#’django.middleware.cache.CacheMiddleware’,
‘django.contrib.sessions.middleware.SessionMiddleware’,
‘localeurl.middleware.LocaleURLMiddleware’,
‘django.middleware.common.CommonMiddleware’,

#’django.middleware.locale.LocaleMiddleware’,

)

INSTALLED_APPS = (

..

‘localeurl’,

)

几点说明: 继续阅读Django i18n多语言的URL差异化解决方案

Django i18n国际化在windows测试环境中需要注意的问题

这两天准备把前面写的网站 春天音乐 做一下国际化

(补充:翻译量小,半天就完成了,不过教训是一开始应该预留翻译函数接口。在模板中找字符串浪费了很多时间),

以曾经的经验,这样的资源站,超过一半的访客都是通过google进入的,国外用户数高于内地访客。

在windows开发环境中测试i18n可能会遇到一些问题,此处简单罗列:

1. 在windows下安装gnu gettext

i18n需要使用到gettext,windows下单独编译或安装。

当前我win7上所用的django版本为1.5.1,它要求提供至少提供0.15以上的gettext

附0.17的win32安装包: gettext-0.17-win32-setup.exe

安装完毕之后,请把子文件夹bin添加到用户的环境变量PATH中。

 

2. 让locale文件夹位于app目录下

django-admin.py makemessages -l en

上述命令会自动搜寻 视图文件views.py、模板文件中需要翻译的中文字符串(此处我是中译英),

执行这个命令之前,可以首先切换到app目录下,创建一个locale文件夹。

执行后会自动生成en\LC_MESSAGES\django.po这样的文件,以及其上级父文件夹。

 

3. 在修改翻译之后,编译并重启web server

在我们修改了django.po中的翻译后,每次都需要执行:

django-admin.py compilemessages

编译它,随后会生成一个django.mo文件。

为了让修改生效,需要重启web服务器。

测试环境中使用WSGIServer同样需要重启。

下载翻译编辑工具poedit:

http://www.poedit.net/download.php#win32