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死的问题。

Python子进程logging config在Win和Linux下表现不一致的问题

Python父进程通过logging.basicConfig,设定日志文件的路径,子进程再次通过basicConfig设定另一个新的路径。这个操作,在Windows系统和Linux下表现不一致。

区别在于,Windows下将打开不同的文件句柄,写入2个不同的文件 。而在Linux下,则config看起来无效,子进程和父进程使用同一个文件描述符,第二次config无效。(解决的方法文末补充)

import logging
import os
import multiprocessing
import threading

cwd = os.getcwd()


def set_log(log_file_path):
    logging.basicConfig(filename=log_file_path,
                        filemode='a+',
                        format='%(message)s',
                        level=logging.INFO)
    logging.info('%s:  %s' % (logging.root, log_file_path) )


def test_func():
    set_log(os.path.join(cwd, 'test2.log'))


class TestThread(threading.Thread):
    def __init__(self):
        p = multiprocessing.Process(target=test_func)
        p.start()


if __name__ == '__main__':
    set_log(os.path.join(cwd, 'test.log'))
    TestThread()

这个程序在Windows的执行结果是

logging.RootLogger object at 0x0000000002610C18>:  C:\test\test.log
logging.RootLogger object at 0x000000000276C7B8>:  C:\test\test2.log

可以看到,日志写入了两个不同的文件。并且Logger ID不一样

在Linux下的执行结果是

[[email protected] test]# ls *.log
test.log
[[email protected] test]# cat test.log 
:  /root/test/test.log
:  /root/test/test2.log

两个进程的日志都写到了test.log中。 并且 Logger ID 和 fd是一样的

解决的办法

子进程在set_log之前,先 reload(logging), test_func变成

def test_func():
    reload(logging)
    set_log(os.path.join(cwd, 'test2.log'))

修改之后,在Linux执行,日志同样会写入不同的文件

[[email protected] test]# ls *.log
test2.log  test.log
[[email protected] test]# cat *.log
logging.RootLogger object at 0x1df28d0>:  /root/test/test2.log
logging.RootLogger object at 0x1d80bd0>:  /root/test/test.log

Python浅复制导致for loop删除剩下偶数位元素的问题

昨天群里,大家聊到了一个面试题,关于 浅复制 (shallow copy )的。 相信很多人都写过类似的错误代码。

items = list(range(1, 11))

while items:
    tmp_items = items     # tmp_items referred to items
    for i in tmp_items:
        items.remove(i)
    print items

通过一个for loop遍历删除列表中的元素(这里示例1到10)。最终执行的结果是

[2, 4, 6, 8, 10]
[4, 8]
[8]
[]

可以看到,每次循环,只有奇数位的数字被从列表移除了,偶数位的数字被保留下来。

这跟列表是一个链表结构有关系。

第二,这里的 tmp_items = items 是浅复制, tmp_items只是items的引用

迭代器在遍历列表的时候,从head开始

head -> pointer1 -> 1
        pointer2 -> 2
        ...
        pointer10 -> 10

循环开始,首先删除第一个元素(1),此时,对应的head指针移动到指针2

head -> pointer2 -> 2
        ...
        pointer10 -> 10

继续遍历时, head->next 指向的是已经是元素3。    于是,最终只有奇数位置的元素被移除了。

requests.get 异常hang住

这两天优化端口扫描脚本,在获取HTTP页面内容时,发现requests.get hang住了(之前httplib不会)。

requests.get 提供了超时参数timeout,但在实际使用过程中,仍发现get请求可能被hang住。

原因在于非HTTP  Server,response中一直没有出现换行符\n,导致_read_status()在获取状态码的时候就hang住了。

一个简单的解决方法,可以创建新的线程,示例代码参考如下,这部分代码是无法直接工作的:

def do_get_thread(url, ret):
    try:
        headers = {
            'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 '
                          '(KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25',
            'Range': 'bytes=0-10240',
            'Connection': 'Close'}
        ret.append(requests.get(url, headers=headers, timeout=(4, 30)))
    except:
        pass


def get_http_title(ip, port, service, log_queue):
    if service.lower().find('https') >= 0 or service.lower().find('ssl') >= 0:
        service = 'https'
    else:
        service = 'http'
    url = '%s://%s:%s' % (service, ip, port)
    msg = 'Get http title %s' % url
    log_queue.put(msg)

    try:
        start_time = time.time()
        ret = []
        t = threading.Thread(target=do_get_thread, args=(url, ret))
        t.daemon = True
        t.start()
        while t.isAlive():
            if time.time() - start_time > 31:
                log_queue.put('[get_http_title timed out] %s' % url)
                return {}, ''
            else:
                time.sleep(1.0)
        if not ret:
            return {}, ''
        resp = ret[0]
        headers = resp.headers
        m = re.search('<title>(.*?)</title>', resp.text, re.IGNORECASE)
        http_title = m.group(1).strip() if m else ''
        if http_title:
            http_title = decode_response_text(http_title)
        else:
            http_title = resp.text.replace('\n', ' ').strip()[0:200]

        return headers, http_title
    except requests.exceptions.RequestException, e:
        pass
    except Exception, e:
        log_queue.put('[get_http_title.exception] %s' % str(e))
    return {}, ''

.DS_Store文件泄漏利用工具: ds_store_exp

.DS_Store是Mac下Finder用来保存如何展示 文件/文件夹 的数据文件,每个文件夹下对应一个。

如果开发/设计人员将.DS_Store上传部署到线上环境,可能造成文件目录结构泄漏,特别是备份文件、源代码文件。

ds_store_exp 是一个.DS_Store 文件泄漏利用脚本,它解析.DS_Store文件并递归地下载文件到本地: https://github.com/lijiejie/ds_store_exp

DS_Store parser is based on ds_store 1.1.0 。

一个示例

python ds_store_exp.py http://hd.zj.qq.com/themes/galaxyw/.DS_Store

hd.zj.qq.com/
└── themes
    └── galaxyw
        ├── app
        │   └── css
        │       └── style.min.css
        ├── cityData.min.js
        ├── images
        │   └── img
        │       ├── bg-hd.png
        │       ├── bg-item-activity.png
        │       ├── bg-masker-pop.png
        │       ├── btn-bm.png
        │       ├── btn-login-qq.png
        │       ├── btn-login-wx.png
        │       ├── ico-add-pic.png
        │       ├── ico-address.png
        │       ├── ico-bm.png
        │       ├── ico-duration-time.png
        │       ├── ico-pop-close.png
        │       ├── ico-right-top-delete.png
        │       ├── page-login-hd.png
        │       ├── pic-masker.png
        │       └── ticket-selected.png
        └── member
            ├── assets
            │   ├── css
            │   │   ├── ace-reset.css
            │   │   └── antd.css
            │   └── lib
            │       ├── cityData.min.js
            │       └── ueditor
            │           ├── index.html
            │           ├── lang
            │           │   └── zh-cn
            │           │       ├── images
            │           │       │   ├── copy.png
            │           │       │   ├── localimage.png
            │           │       │   ├── music.png
            │           │       │   └── upload.png
            │           │       └── zh-cn.js
            │           ├── php
            │           │   ├── action_crawler.php
            │           │   ├── action_list.php
            │           │   ├── action_upload.php
            │           │   ├── config.json
            │           │   ├── controller.php
            │           │   └── Uploader.class.php
            │           ├── ueditor.all.js
            │           ├── ueditor.all.min.js
            │           ├── ueditor.config.js
            │           ├── ueditor.parse.js
            │           └── ueditor.parse.min.js
            └── static
                ├── css
                │   └── page.css
                ├── img
                │   ├── bg-table-title.png
                │   ├── bg-tab-say.png
                │   ├── ico-black-disabled.png
                │   ├── ico-black-enabled.png
                │   ├── ico-coorption-person.png
                │   ├── ico-miss-person.png
                │   ├── ico-mr-person.png
                │   ├── ico-white-disabled.png
                │   └── ico-white-enabled.png
                └── scripts
                    ├── js
                    └── lib
                        └── jquery.min.js

21 directories, 48 files

QNetworkReply获取Content-Length的方法(PyQT)

使用pyqt webkit写headless 动态爬虫,需要捕获到所有的HTTP request和response(包括JS异步请求)。

QNetworkReply 没有返回content-length相关的方法,rawHeaderList实际上并不包括content-length。

一个可行的方法,是在readyRead信号触发时,size()方法获取到body的大小。示例代码如下,请注意self.reply_lst这个列表是必不可少的:

import sys
from PyQt5.Qt import QApplication
from PyQt5.QtCore import QUrl
from PyQt5.Qt import QWebView, QWebPage
from PyQt5.QtNetwork import QNetworkAccessManager


class Manager(QNetworkAccessManager):
    def __init__(self, parent=None):
        QNetworkAccessManager.__init__(self, parent)
        self.finished.connect(self._finished)
        self.reply_lst = []

    def _finished(self, reply):
        print '[%s bytes] %s' % (reply.content_length, reply.url().toString())
        self.reply_lst.remove(reply)

    def createRequest(self, operation, request, body=None):
        _url = request.url()
        reply = super(Manager, self).createRequest(operation, request, body)
        reply.readyRead.connect(self.read_read)
        self.reply_lst.append(reply)
        return reply

    def read_read(self):
        self.sender().content_length = self.sender().size()

def app_quit():
    app.quit()

if __name__ == "__main__":
    app = QApplication(['-platform', 'minimal'])
    browser = QWebView()
    page = QWebPage()
    manager = Manager()
    page.setNetworkAccessManager(manager)
    browser.setPage(page)
    browser.load(QUrl('http://www.lijiejie.com/'))
    browser.loadFinished.connect(app_quit)
    sys.exit(app.exec_())

最后,拿到所有URL的content-length如下,可以把它写到爬虫的结果中:

[499 bytes] http://www.lijiejie.com/
[2082 bytes] https://s6.cnzz.com/stat.php?id=3804994&web_id=3804994&show=pic
[11414 bytes] http://www.lijiejie.com/wp-includes/js/wp-emoji-release.min.js?ver=4.7.1
[20172 bytes] http://www.lijiejie.com/wp-content/plugins/crayon-syntax-highlighter/css/min/crayon.min.css?ver=_2.7.2_beta
[2850 bytes] http://www.lijiejie.com/wp-content/plugins/crayon-syntax-highlighter/themes/github/github.css?ver=_2.7.2_beta
[86 bytes] http://www.lijiejie.com/wp-content/plugins/crayon-syntax-highlighter/fonts/courier-new.css?ver=_2.7.2_beta
[374 bytes] http://www.lijiejie.com/wp-content/plugins/wp-pagenavi/pagenavi-css.css?ver=2.70
[27597 bytes] http://www.lijiejie.com/wp-content/themes/retro-fitted/style.min.css?ver=0.4
[10056 bytes] http://www.lijiejie.com/wp-includes/js/jquery/jquery-migrate.min.js?ver=1.4.1
[3678 bytes] http://www.lijiejie.com/wp-content/plugins/dynamic-to-top/js/libs/jquery.easing.js?ver=1.3
[4214 bytes] http://www.lijiejie.com/wp-content/themes/retro-fitted/library/js/drop-downs.min.js?ver=20110920
[22337 bytes] http://www.lijiejie.com/wp-content/plugins/crayon-syntax-highlighter/js/min/crayon.min.js?ver=_2.7.2_beta
[1192 bytes] http://www.lijiejie.com/wp-content/plugins/dynamic-to-top/js/dynamic.to.top.min.js?ver=3.5
[1398 bytes] http://www.lijiejie.com/wp-includes/js/wp-embed.min.js?ver=4.7.1
[6108 bytes] http://www.lijiejie.com/wp-includes/js/jquery/jquery.js?ver=1.12.4
[765 bytes] https://c.cnzz.com/core.php?web_id=3804994&show=pic&t=z
[3197 bytes] http://www.lijiejie.com/wp-content/themes/retro-fitted/images/bg.jpg
[2236 bytes] http://www.lijiejie.com/wp-content/plugins/crayon-syntax-highlighter/css/images/toolbar/buttons.png
[576 bytes] http://www.lijiejie.com/wp-content/themes/retro-fitted/images/quotes.png
[9766 bytes] http://www.lijiejie.com/wp-content/themes/retro-fitted/images/header.png
[7210 bytes] http://www.lijiejie.com/wp-content/uploads/2014/10/stom.tencent.com_.png
[719 bytes] https://icon.cnzz.com/img/pic.gif
[43 bytes] https://hzs9.cnzz.com/stat.htm?id=3804994&r=&lg=zh-cn&ntime=none&cnzz_eid=1834513944-1484813777-&showp=1920x1080&t=李劼杰的博客&h=1&rnd=1792993998
[43 bytes] https://cnzz.mmstat.com/9.gif?abc=1&rnd=2124379566
[142 bytes] http://www.lijiejie.com/wp-content/themes/retro-fitted/images/bullet.png
[2516 bytes] http://www.lijiejie.com/wp-content/uploads/2017/01/eping.png
[3282 bytes] http://www.lijiejie.com/wp-content/uploads/2014/10/stom.tencent.com_upfile.png