Category Archives: python

Spyder介绍

Spyder这个项目本来也没打算要写,但是最近因为一些业务上的需求,需要重新使用,就顺手写一点介绍。Spyder是一个用python2写出来的页面抓取工具,代码托管在Github上。原先Spyder是需要mysql才行的,这次改版我把管理界面和采集器本身进行了一次分离。因此这一次只来说说采集器的本身,Web管理界面等到下一次再说吧。

在src目录里spyder可以采集器,web为web管理界面,libs放了一些通用的函数在里面。用Spyder前,你需要安装lxml。这个是一个非常有用的库,可以对采集回来的html数据进行dom操作。

Spyder中最基础的单元为Seed,也就是所谓的种子。一个种子他包含了你需要采集的需求。看下这例子:

import os, sys
parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)));
if parentdir not in sys.path:
    sys.path.insert(0, parentdir)

from spyder.seed import Seed

config = {
    'listtype': u'html',
    'tries': 5,
    'frequency': 7200,
    'lang': u'zhCN',
    'seed_name': u'抓取在线人数',
    'enabled': 1,
    'rule': {
        'urlformat': 'http://www.douyu.tv/directory/all?offset=$page&limit=30',
        'pageparent': '',
        'maxpage': 25,
        'step': 30,
        'startpage': 0,
        'contenturl': '',
        'listparent': 'div[id="item_data"] ul li',
        'urltype': 'createLink',#链接模式
        'contentparent': 'a[class="list"]',
        'zero': 1,
        'entryparent': '',
        'filters': [
            #filterid, value, fetch_all, type(content/list)
        ],
        'extrarules':[
            ('title', 'h1[class="title"].text()', 0, 'list'),
            ('view', 'span[class="view"].text()', 0, "list"),
            ('name', 'span[class="nnt"].text()', 0, "list"),
            ('game', 'span[class="zbName"].text()', 0, "list"),
        ]
    },
    'timeout': 5,
    'sid': 1000L
}

seed = Seed(config);

上面就是一个抓取某网站直播在线人数的配置文本。这里包含了listtype(列表页面类型:html, json, feed)、tries(尝试次数,默认为5次)、seed_name(种子名称)、sid(种子ID,必须要分配一个)、enable(是否启用)、rule(采集配置)。
rule中包含了基础的采集信息和过滤器以及额外需要抓取的数据。 先来说说基础的

  1. 生成列表链接
  2. urlformat 列表页面模板,这个需要和下面的urltype结合在一起使用。一般来说需要抓取的列表页面都是有规则的。所以我在urltype中设定了三种模式inputLink(自定义模式)、createLink(根据设定的step, maxpage, startpage,来生成列表链接)、dateLink(根据设定的日期格式)。 在这些列表中可选参数是$page。这个用于指定需要填充的位置。比如上面例子中的,就是createLink类型。根据所指定的规则,他会生成为:
    http://www.douyu.tv/directory/all?offset=0&limit=30
    http://www.douyu.tv/directory/all?offset=30&limit=30

  3. 提取列表及获取文章链接
  4. 在你生成列表链接之后,就可以开始抓取了。抓取下来的都是html页面。这个时候你需要配置需要采集的列表区域以及文章链接。这里需要涉及listparent,contentparent。这里的配置很简单,如果你熟悉jquery的话,那么配置起来相当简单。

上面已经简单的描述了如何去获取列表内容。在这里,可能我会囉嗦几句关于配置正则方面的东西。先来说下extrarules。这里面包含了你需要采集的其他信息。rule本身只是去抓取页面和获取文章链接,其余信息是都没有的。所以你需要配置extrarules让Spyder知道你需要从页面上获取哪里信息。

('title', 'h1[class="title"].text()', 0, 'list'),

上面这个就是一个简单的extrarules中的一条。 分别对应name,parent, fetchall, type。 name就是在你后期获取数据时候的一个key。第二个为抓取正则,这个正则如同上面一样,都是和jquery差不多。获取文字用text(),如果你要获取html就用html()。这个就可以在浏览器先试成功之后,再复制粘帖进来。第三个为是否全部抓取,第四个为类型(list列表, content 文章)。

除了extrarules还有一个叫做filters,过滤器功能可以用做替换,可以改文本内容。这个下次时候我在述说。

通过上面的配置,一个采集的种子已经配置完成了。

第二步就是数据的采集过程了。

import os, sys
parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)));
if parentdir not in sys.path:
    sys.path.insert(0, parentdir)

from spyder.seed import Seed
from spyder.document import Grab

seed = Seed(config);
data = Grab(seed);

你只要引用document中的Grab就可以了。他会采集完成后的数据全部放在items中。你可以只需要进行数据遍历即可。

以上简单介绍了一个Spyder,它与其他采集器不一样的地方在于灵活,配置方便。可以用于各种情况。项目在一年前写的,但是集成了相当多的功能,比如图片存储转换链接,存储到数据库,可扩展插件等等。目前我正在所以对代码进行一次整理,这篇文章也算是对原有代码逻辑的一次梳理。

自定义stdout

在python中, 有时候你需要捕捉print的输出保存到一个指定的文件下面. 一般的情况下, print实际上是调用的sys.stdout. 如果你需要需要对print进行捕捉. 你只要需要hook sys.stdout即可.

例如我们将所有的print都保存到StringIO下, 只需要写成

from StringIO import StringIO
import sys
old_stdout = sys.stdout
sys.stdout = mystdout = StringIO()

#获取内部值
sys.stdout = old_stdout
print mystdout.getvalue()
mystdout.close()

但是当你的print中有unicode字符的时, 使用StringIO会报TyperError或者UnicodeError之类的错误.这个时候我们需要进行一下改写.

from StringIO import StringIO
import sys, itertools

class Logger(object):
  def __init__(self):
    self.log = StringIO();
    self.terminal = sys.stdout

  def write(self, message):
    self.log.write(self.safestr(message))

  def safestr(self, obj, encoding='utf-8'):
    #包含能正确转换
    if isinstance(obj, unicode):
      return obj.encode(encoding)
    elif isinstance(obj, str):
      return obj
    elif hasattr(obj, 'next'):
      return itertools.imap(self.safestr, obj)
    else:
      return str(obj)

  def close(self):
    self.log.close()

  def output(self):
    self.terminal.write(self.log.getvalue())

old_stdout = sys.stdout
sys.stdout = Logger()

print "Hello"

print u"测试"

#将所有catch的输出到terminal
sys.stdout.output()

"""
Hello
测试
"""
sys.stdout.close()

在print调用write使用, 我们将message进行一次转码. 在通过调用StringIO().getvalue方法的时候, 就不会报错了.

如果你想将写入文件, 只需要在self.log这部分进行一下改动即可.

self.log = open("debug.log", "a")

在git中使用Python打补丁包


有时候, 我们写完一个项目需要对其进行打包. 打完成包非常简单, 我们可以使用git archive这个命令. 但是有时候我们需要对项目的一些特殊文件进行过滤. 这个时候, 可能会说这些文件可以放在.gitingore中. 我需要告诉的时, 在开发项目的时候, 有些文件也需要在git仓中, 以方便其他小组的人使用. 另一种情况, 我们需要打一个差异包出来, 如果没有任何特殊要求, 可以直接使用tar zxvf xxx-patch.tar.gz `git diff –name-only`进行打包. 但是你这个仓中含有很多个submodule时候, 使用这种方法, 会遇到一个很不幸的事, 不管这个子模块修改了一个, 还是多个文件, 他都会将这个子模块全部打包进去. 因此, 我们需要一个好的打包工具, 解决以上问题. 目前我是采用python + pygit2的方式. 同时你系统环境中要有git.

1. 获取git仓中tags列表

pygit2有一个已知的bug, 你给你的项目打tag, 如果打tag时候没有加上message. pygit2是无法读取到的.
读取git仓中的tag非常的简单. 代码如下

def get_tags(self):
 self.tags = [];
 print "=== Parsing package version";
 data = self.repo.listall_references();

 for item in data:
  ref = self.repo.lookup_reference(item);
  if (ref.type == pygit2.GIT_OBJ_COMMIT):
   oid = ref.oid;
   ref_obj = self.repo[oid];
   if isinstance(ref_obj, pygit2.Tag):
    self.tags.append(ref_obj);
   else:
    continue;
 self.tags.sort(lambda x,y: cmp(y.tagger.time, x.tagger.time));

获取tag之后, 我们可以使用tag下属性timer, 根据时间先后次序进行排序. 这样你想取最后一个tag作为目标进行打包 只要self.tags[0](倒序)就能直接获取到. 同样你需要获取两个版本的差异. 只要git diff tag[0]..tag[1]
Read more »

Python mysql fetch_row 手记

在python的mysql中, fetch_row 默认是不传递参数的, 只返回一条且没有column的数据tuple.

但是在仔细看了API文档之后, 发现有两个参数: num_results和display_column

num_result: 默认只显示一条, 当传入0时, 将显示全部数据. 否则根据你的数字显示数据条数
display_column: 默认不显示
1. 只显示column名
2. 显示格式为 table.column

[Python]utf8文本内容无法插入到mysql数据库中

文本头部设置了

#coding: utf-8

但是当你处理这些utf8文本的时候, 还是依然会出现”acsii balabala”之类的问题. 这个时候你需要在文件顶部在加入三行代码

import sys
reload(sys)
sys.setdefaultencoding("utf-8")

这个时候 就能正确处理了. 原因很简单python2.7及以下版本(3.0不知道), 如果不那么申明, 内部依赖还是以ascii码处理参数的.

Create a warcraft world map by PIL

通过使用Python Image Library创建魔兽世界地图
代码如下

#coding: utf-8
import _mysql as mysql
import MySQLdb
import os, io, re, math;
import Image

class WorldMaps(object):
def __init__(self, base, output):
self._basedir = base
self._output = output

self.connectMysql()
self.cacheMaps()

def connectMysql(self):
self.db = mysql.connect(host=”127.0.0.1″, user=”root”, passwd=””, db=”wowdb_ctm”)
self.db.query(“SET NAMES UTF8”)

#cache maps metadata from WorldMapArea.dbc
def cacheMaps(self):
self.MAPS_INFO = {}
sql = “SELECT ID, Map, AreaTable, Icon FROM worldmapareadbc”
self.db.query(sql)
results = self.db.store_result()
data = results.fetch_row()

while (data):
data, = data;
self.MAPS_INFO[int(data[0])] = {
“mapid” : data[1],
“areaid” : data[2],
“mapfilename” : data[3]
}
#fetch now
data = results.fetch_row()

def GetNumberOfDetailTiles(self, dirname, mapfilename):
numOnDetailTiles = 0;
for filename in os.listdir(dirname):
if (re.match(mapfilename+”(\d+)”, filename)):
numOnDetailTiles = numOnDetailTiles + 1;
return numOnDetailTiles;

def GetMapInfo(self, mapid):
mapid = int(mapid)
mapinfo = self.MAPS_INFO[mapid]
mapfilename = mapinfo[“mapfilename”]
return mapfilename, self._basedir + “/” + mapfilename + “/”

def GetMapOverlays(self, mapid):
overlays = []
sql = “SELECT Path, Width, Height, `Left`, `Top` FROM worldmapoverlaydbc WHERE ZoneId = ” + str(mapid);
self.db.query(sql);
results = self.db.store_result();
data = results.fetch_row();
while data:
data, = data
overlays.append({
“path” : data[0],
“width” : int(data[1]),
“height”: int(data[2]),
“left” : int(data[3]),
“top” : int(data[4])
});

data = results.fetch_row();

return overlays

def createWorldMap(self, mapid):
mapfilename, dirname = self.GetMapInfo(mapid)

numOnDetailTiles = self.GetNumberOfDetailTiles(dirname, mapfilename);
texs = {};

#map standard size: 1002 * 668
new_image = Image.new(“RGB”, (1002, 668));
x = 0;
y = 1;
for i in range (1, numOnDetailTiles+1):
texName = dirname + “/” + mapfilename + str(i) + “.png”
# x x x x
# x x x x
# x x x x
texs[i] = Image.open(texName);
x = x + 1;
#print x, y
new_image.paste(texs[i], (x * 256 – 256, y * 256 – 256));
if x == 4:
y = y + 1
x = 0;

#sec, get overlays, and paste
overlays = self.GetMapOverlays(mapid)
textureCount = 0
for overlayinfo in overlays:
textureName = overlayinfo[“path”];
textureWidth = overlayinfo[“width”];
textureHeight = overlayinfo[“height”];
offsetX = overlayinfo[“left”];
offsetY = overlayinfo[“top”];
#print textureWidth, textureHeight, textureName

if (textureName and textureName != “”):
numTexturesWide = math.ceil(float(textureWidth) / float(256))
numTexturesTall = math.ceil(float(textureHeight) / float(256))
#print textureWidth, textureHeight, textureName, numTexturesWide, numTexturesTall
neededTextures = int(textureCount + (numTexturesWide * numTexturesTall))

texturePixelWidth = 0
texturePixelHeight = 0
textureFileWidth = 0
textureFileHeight = 0
for j in range(1, int(numTexturesTall) + 1):
if (j < numTexturesTall): texturePixelHeight = 256 textureFileHeight = 256 else: texturePixelHeight = textureHeight % 256; if (texturePixelHeight == 0): texturePixelHeight = 256 textureFileHeight = 16 while textureFileHeight < texturePixelHeight: textureFileHeight = textureFileHeight * 2for k in range(1, int(numTexturesWide) + 1): textureCount = textureCount + 1 if (k < numTexturesWide): texturePixelWidth = 256 textureFileWidth = 256 else: texturePixelWidth = textureWidth % 256 if (texturePixelWidth == 0): texturePixelWidth = 256 textureFileWidth = 256 while textureFileWidth < texturePixelWidth: textureFileWidth = textureFileWidth * 2_textureName = textureName + str(int((( j - 1) * numTexturesWide) + k)) + ".png"; texture = Image.open(dirname + "/" + _textureName); new_image.paste(texture, ( (offsetX + (256 * (k - 1))) , ((offsetY + (256 * (j - 1)))) ), texture ); new_image.save( self._output + "/" + str(mapid) + ".png"); [/python]生成的效果图: