虽然是一个被玩烂了的话题,不过python作为脚本日常使用的话,模拟登陆用到的可能性还是非常高的。方法就是urllib2配合urllib老一套,不过还是有些细节值得注意的,正好拿来凑个短篇。

本文中python==2.7.10

Http协议

首先,不管你打算用模拟登陆做什么,都得把Http协议这一层封装好,也就是cookie的处理,getpost这两个方法,以及可能还用的到的download方法。所以可以准备一个HttpClient的类,只用写一次,以后再有类似的场合就可以直接改改再用。

HttpClient

首先是cookie,一般要登陆,肯定少不了cookie的处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class HttpClient:
  __cookie = cookielib.CookieJar()
  __req = urllib2.build_opener(urllib2.HTTPCookieProcessor(__cookie))
  __req.addheaders = [
    #('Accept', 'application/javascript, */*;q=0.8'),
    ('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'),
    ('User-Agent', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36")
  ]
  urllib2.install_opener(__req)

  def Get(self, url, refer=None):
    pass

  def Post(self, url, data, refer=None):
    pass

cookie一般这么写,之后使用urllib2Requesturlopen会自己带上cookie。非常的科学。

Get和Post

以下是get,就是Request之后再urlopen

1
2
3
4
5
6
7
8
def Get(self, url, refer=None):
    try:
      req = urllib2.Request(url.replace(' ','%20'))
      if not (refer is None):
          req.add_header('Referer', refer)
      return urllib2.urlopen(req).read()
    except urllib2.HTTPError, e:
      return e.read()

然后是Post,data是个字典:

1
2
3
4
5
6
7
8
def Post(self, url, data, refer=None):
    try:
      req = urllib2.Request(url, urllib.urlencode(data))
      if not (refer is None):
        req.add_header('Referer', refer)
      return urllib2.urlopen(req, timeout=180).read()
    except urllib2.HTTPError, e:
      return e.read()

Download

简单的download,就是urlopen之后再接个read就可以了。

1
2
3
4
5
6
7
def Download(self, url, file):
    output = open(file, 'wb')
    parsed_link = urlparse.urlsplit(url.encode('utf8'))
    parsed_link = parsed_link._replace(path=urllib.quote(parsed_link.path))
    url = parsed_link.geturl()
    output.write(urllib2.urlopen(url).read())
    output.close()

如果想要带有显示下载速度的download的话,urlliburlretrieve()由于不支持cookie是不能用的,所以只能自己写一个类似的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def Download(self, url, file):
    def chunk_report(bytes_so_far, chunk_size, total_size):
      percent = int(bytes_so_far*100 / total_size)
      sys.stdout.write( "\r" + "Downloading" + '  ' + os.path.basename(file) + " ...(%.1f KB/%.1f KB)[%d%%]" % (bytes_so_far/1024.0, total_size/1024.0, percent))
      sys.stdout.flush()
      if bytes_so_far >= total_size:
         sys.stdout.write('\n')
         sys.stdout.flush()

    def chunk_read(response, chunk_size=8192, report_hook=None):
        try:
            total_size = response.info().getheader('Content-Length').strip()
        except:
            return response.read()

        total_size = int(total_size)
        bytes_so_far = 0
        ret = ''

        while 1:
            chunk = response.read(chunk_size)
            bytes_so_far += len(chunk)
            ret += chunk

            if not chunk:
                break

            if report_hook:
                report_hook(bytes_so_far, chunk_size, total_size)
        return ret

    output = open(file, 'wb')
    parsed_link = urlparse.urlsplit(url.encode('utf8'))
    parsed_link = parsed_link._replace(path=urllib.quote(parsed_link.path))
    url = parsed_link.geturl()
    response = urllib2.urlopen(url)
    content = chunk_read(response, report_hook=chunk_report)
    output.write(content)
    output.close()

注意,经常有些网站喜欢在url里面放一些汉字或者空格,可以用urlparse来处理:

1
2
3
parsed_link = urlparse.urlsplit(url.encode('utf8'))
parsed_link = parsed_link._replace(path=urllib.quote(parsed_link.path))
url = parsed_link.geturl()

如果只有空格,也可以偷懒把空格replace成%20

上层逻辑

抓包分析

就用Chrome或者firefox抓,按下F12,然后点选Network,然后点击登陆,就可以看到,提交了那些信息。有时候网页可能会调用js,其实js最后还是会调用post,一样也可以用python来处理。

正则处理

人登陆靠眼睛识别要点击的按钮,机器靠pattern正则识别要访问的url。下面这个函数返回html第一次出现的rex,ex控制允不允许为空,er是你希望的错误logging内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def get_revalue(html, rex, er, ex):
    v = re.search(rex, html)
    if v is None:
        if ex:
            logging.error(er)
            raise TypeError(er)
        else:
            logging.warning(er)
        return ''
    return v.group(1)

上面还用到了logging,这对不知道什么时候就挂掉的python脚本来说很有帮助。

1
2
3
4
5
6
logging.basicConfig(
    filename='UCS.log',
    level=logging.DEBUG,
    format='%(asctime)s  %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
    datefmt='%a, %d %b %Y %H:%M:%S',
)

用熟之后,你大概会经常做下面的事情:

1
2
3
4
5
session = get_revalue(html, r'session=(.+?)&', 'get session error', 1)
_mid = get_revalue(html, r'_mid=(.+?)"', 'get mid error', 1)
guid = get_revalue(html, r'guid=(.+?)"', 'get guid error', 1)

html = self.req.Get("http://xxx.xxx.xxx/xxx?session={0}&_mid={1}".format(session, _mid))

另外附上正则测试

其他

你可以从实际的脚本中获得更多内容,下面一个是某学校的网站登陆下载,一个是webqq登陆接受消息。
参见UCS
或者GnomeQQ