在Sphinx中,如果你调用的是C的api,并使用更新属性的功能,而此时,你想将更新后的索引冲刷到硬盘,你就会发现C的api中没有提供这个功能。而在Java,PHP,Python中,都提供了FlushAttributes这个接口来完成这个功能,于是你不得不另外在写一个程序来调用这个接口。

仔细想想,Sphinx都是用C++写的,而C的API中竟然没有提供这个接口,反倒是其它语言有提供,真是匪夷所思。所幸,代码都是开源的,想要自己有这个接口,自己动手写一个就好了,也许这就是开源的好处。

代码如下:

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
int sphinx_flush_attributes(sphinx_client * client) {
char *buf, *req, *p;
int req_len = 0;
if (!client) {
printf("not valid client\n");
return -1;
}
buf = malloc ( 12 + req_len ); // request body length plus 12 header bytes
if ( !buf ) {
set_error ( client, "malloc() failed (bytes=%d)", req_len );
return -1;
}

req = buf;

send_word ( &req, SEARCHD_COMMAND_FLUSHATTRS );
send_word ( &req, VER_COMMAND_FLUSHATTRS );
send_int ( &req, req_len );

// send query, get response
if ( !net_simple_query ( client, buf, req_len ) )
return -1;

// parse response
if ( client->response_len < 4 ) {
set_error ( client, "incomplete reply" );
return -1;
}

p = client->response_start;
return unpack_int ( &p );
}

之后还要添加
SEARCHD_COMMAND_FLUSHATTRS = 7,
VER_COMMAND_FLUSHATTRS = 0x100,
以及在头文件中添加int sphinx_flush_attributes(sphinx_client * client);即可。

联系作者

很早之前就知道MMSEG分词算法,网上也有各种语言的实现。最近了解Sphinx-for-Chinese的分词后,才知道它也是使用的MMSEG,并且CoreSeek也是使用的MMSEG。也许MMSEG是互联网上使用知名度最高的分词算法了吧,因为它简单并且高效。

更进一步了解后,知道MMSEG是台湾人蔡志浩提出来的。蔡志浩是一位心理学教师,在美国伊利诺伊读博士期间,选修了语言学,在这个过程中,随手写了MMSEG。看蔡志浩网站,总是很舒心,因为蔡老师的文笔很好,总会用通俗的语言把问题讲清楚,而且蔡老师的博客涉及范围极广,设计,心理,写作,社会观察,旅游等等。也正是因为他的博客,我才用实名建立自己的博客。以下回归正题。

MMSEG总的说来就是四个规则。
1.最长匹配原则
2.最大平均长度
3.最小长度方差
4.最大单字单词的语素自由度

算法步骤:
1.选定一个分词个数,得到可行的分词情形
2.利用4条原则得到最优分词可能
3.得到最优分词的第一个词,回到步骤1继续分词

举个例子最好理解。下面是要对“研究生命起源的原因主要是因为它的重要性”进行分词。
1.首先选定分词个数为3,则可以得到可行的分词情形如下:
研 究 生
研 究 生命
研究 生命 起
研究 生命 起源
研究生 命 起
研究生 命 起源
2.利用4条原则得到最优分词可能
运用第1条原则后,可以得到最优分词可能为一下两条
研究 生命 起源
研究生 命 起源
运用第2条原则,这两个结果相同
运用第3条原则,可以得到最优的结果为
研究 生命 起源
3.从最优结果中得到第一个词,也就是“研究”,之后对“生命起源的原因主要是因为它的重要性”运用相同的步骤进行分词

有必要对原则4进行解释,这条原则说的是单字的成为语素的自由度。当分到”主要是因为“就会用到。对于”主要是因为“
第1步骤中得到:
主 要 是
主 要是 因为
主要 是 因
主要 是 因为
第2步骤中,由前三条原则,只剩下一下两个
主要 是 因为
主 要是 因为
之后再运用第4条原则,这里单字”是“为独立语素的可能比”主“要大,所以最优结果为
主要 是 因为

见过的MMSEG算法实现中,素心如何天上月的http://yongsun.me/2013/06/simple-implementation-of-mmseg-with-python/无疑是最简明清晰的。Python确实不错,短短100行就把算法的精髓展示出来,并且几乎可以不用写注释了。模仿他的实现,写了一遍。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#coding:utf-8
from collections import defaultdict
import codecs
from math import log

class Trie(object):
class TrieNode():
def __init__(self):
self.value = 0
self.trans = {}
def __init__(self):
self.root = self.TrieNode()
def add(self, word, value=1):
cur = self.root
for ch in word:
try:
cur = cur.trans[ch]
except:
cur.trans[ch] = self.TrieNode()
cur = cur.trans[ch]
cur.value = value
def _walk(self, node, ch):
if ch in node.trans:
node = node.trans[ch]
return node, node.value
else:
return None, 0
def match_all(self, s):
ret = []
cur = self.root
for ch in s:
cur, value = self._walk(cur, ch)
if not cur:
break
if value:
ret.append(value)
return ret

class Dict(Trie):
def __init__(self, filename):
super(Dict, self).__init__()
self.load(filename)

def load(self, filename):
with codecs.open(filename, "r", "utf-8") as f:
for line in f:
word = line.strip()
self.add(word, word)
class CharFreq(defaultdict):
def __init__(self, filename):
super(CharFreq, self).__init__(lambda: 1)
self.load(filename)
def load(self, filename):
with codecs.open(filename, "r", "utf-8") as f:
for line in f:
line = line.strip()
word, freq = line.split(' ')
self[word] = freq
class MMSEG():
class Chunk():
def __init__(self, words, chars):
self.words = words
self.lens = map(lambda x: len(x), words)
self.length = sum(self.lens)
self.average = self.length * 1.0 / len(words)
self.variance = sum(map(lambda x: (x - self.average) ** 2, self.lens)) / len(words)
self.free = sum(log(float(chars[w])) for w in self.words if len(w) == 1)
def __lt__(self, other):
return (self.length, self.average, -self.variance, self.free) < (other.length, other.average, -other.variance, other.free)
def __init__(self, dic, chars):
self.dic = dic
self.chars = chars
def __get_chunks(self, s, depth=3):
ret = []
def __get_chunk(self, s, num, seg):
if not num or not s:
if seg:
ret.append(self.Chunk(seg, self.chars))
return
else:
m = self.dic.match_all(s)
if not m:
__get_chunk(self, s[1:], num - 1, seg + [s[0]])
else:
for w in m:
__get_chunk(self, s[len(w):], num - 1, seg + [w])
__get_chunk(self, s, depth, [])
return ret
def segment(self, s):
while s:
chunks = self.__get_chunks(s)
best = max(chunks)
yield best.words[0]
s = s[len(best.words[0]):]

if __name__ == "__main__":
dic = Dict("dict.txt")
chars = CharFreq('chars.txt')
mmseg = MMSEG(dic, chars)
print ' '.join(mmseg.segment(u"北京欢迎你"))
print ' '.join(mmseg.segment(u"研究生命起源的原因主要是因为它的重要性"))
print ' '.join(mmseg.segment(u'开发票'))
print ' '.join(mmseg.segment(u'武松杀嫂雕塑是艺术,还是恶俗?大家怎么看的?'))
print ' '.join(mmseg.segment(u'陈明真做客《麻辣天后宫》的那期视频哪里有?'))
print ' '.join(mmseg.segment(u'压缩技术是解决网络传输负担的 有效技术。数据压缩有无损压缩和有损压缩两种。在搜索引擎中用到的压缩技术属于无损压缩。接下来,我们将先讲解各种倒排索引压缩算法,然后来分析搜索引擎技术中词典和倒排表的压缩。'))

用到的两个文件dict.txtchars.txt

联系作者

Sphinx-for-Chinese的分词细粒度问题中说过,为了解决分词的粒度问题,我们对Sphinx-for-Chinese的代码进行了一些修改,而针对精确匹配我们也写了一些额外的代码,虽然这一部分的代码并不是很好看,但毕竟解决了问题,所以也想对这一部分进行说明,因为相信其他人也会遇到类似的问题,这里可以提供一个参考的解决方案。

所谓精确匹配,也就是搜索的词语搜索的字段完全相同。例如假设有三个标题,中大,中大酒店,中大假日酒店,则搜索中大时,与中大完全匹配。一般情况下,我们都希望精确匹配的内容排在前面,此时还需要设置排序方法为SPH_RANK_SPH04。

依然以sphinx-for-chinese-2.2.1-dev-r4311为例,在sphinxsearch.cpp中6282行附近,找到RankerState_ProximityBM25Exact_fn,这里就是sph04的实现。看到数据成员m_uExactHit,知道这个与精确匹配有关,在这段代码里看到HITMAN::IsEnd,于是猜测在某个地方有SetEnd,在sphinx.cpp中27144行附近找到CSphSource_Document::BuildRegularHits方法,在这里找到了,
CSphWordHit pHit = const_cast < CSphWordHit > ( m_tHits.Last() );
HITMAN::SetEndMarker ( &pHit->m_iWordPos );
于是我们想,在进行细粒度分词时,中大将被分成,中大、中、大三个词。只要有某种办法,将中大这个词也使用SetEndMarker就可以达到所要的目的,于是增加了一些代码。

这之后,搜索中大时,中大这个标题确实排在了前面,可是问题又出现了,在搜索中大酒店时,中大酒店这个标题并没有排在前面,中大酒店与中大假日酒店的权重是相同的。分析了原因,搜索中大酒店时,将被分成中大+酒店,而中大假日酒店中,正好也包含中大和酒店,并且酒店也是排在末尾,于是这两个的权重是一样的。于是我们只好再看看m_uExactHit的计算,发现IsEnd并不是唯一的条件,于是相信为细分以前,索引中大酒店时,分词的词是中大、酒店,而细分后变成了中大、中、大、大酒店、酒店、酒、店,于是我们猜测,如果将分词按照原先的方法分一次,之后再一起返回细粒度的分词,可能可以达到目的。这样的结果就是分词返回的是中大、酒店、中、大、大酒店、酒、店。于是按照这个想法,又增加了一些代码。果然这次搜索中大酒店时,中大酒店排在了前面,并且权重比中大假日酒店高。

联系作者

假如使用Sphinx来做搜索引擎,就一定会遇到分词问题。对于中文,有两个选择,选择1是使用Sphinx自带的一元分词,选择2是使用CoreSeek或者Sphinx-for-Chinese,这两个都使用了mmseg来进行分词。据我了解,CoreSeek在支持细粒度的分词,而Sphinx-for-Chinese不支持。而公司使用的是Sphinx-for-Chinese,所以就遇到了分词的粒度问题。

根据产品人员的反馈,有许多这样的例子。例如搜索西海或者海岸时,搜不到大华西海岸酒店,搜索兵马俑时,搜不到秦始皇兵马俑博物馆,搜索肯尼亚时搜不到肯尼亚山。这都是因为Sphinx-for-Chinese使用mmseg得到最优结果后,就不在进行细分的结果。拿大华西海岸酒店这个例子来说,词典里有大华,西海岸,酒店,华西,西海,海岸这些词,根据mmseg得到的最优分词结果,分成大华+西海岸+酒店,这个分词的结果也是正确的,可是搜索西海,海岸就搜不到它的。问过Sphinx-for-Chinese的开发人员后,要想支持更细粒度的分词,只有修改源码。

在组长划出一条线,需要在哪一部分代码后,一个最简单的想法是,对于mmseg中每一步得到的最优结果,都进行更细粒度的划分。例如上面的例子,对于西海岸进行更细粒度划分后,就可以得到西海和海岸,这样搜索西海和海岸时,就可以搜索到。于是立马动手写,折腾一个上午后,果然可以搜到了,这样就达到了同城旅游中酒店搜索的效果了。可是搜索华西还是搜不到,而携程则可以搜到,在携程里,搜索西也可以搜到它。仔细考虑后,在原来的代码里只需要很少的修改就可以做到搜索华西是也可以搜到它,搜索效果已经超过了同程旅游。在增加单字索引后,搜索效果和携程相当接近。

相信许多使用Sphinx-for-Chinese都会遇到类似的问题,也都将用各自的办法解决这个问题。这里将这一部分代码开源,也算是对开源事业的一点点贡献。事实上,需要修改的地方并不是很多。这里我使用的是sphinx-for-chinese-2.2.1-dev-r4311版本,相信其它版本也可以进行类似的修改。需要修改的文件只有一个,那就是sphinx.cpp。

在2244行附近,class CSphTokenizer_UTF8Chinese : public CSphTokenizer_UTF8_Base这个类中,增加以下数据成员

1
2
3
4
5
6
7
int m_totalParsedWordsNum; //总共得到的分词结果
int m_processedParsedWordsNum; //已经处理的分词个数
int m_isIndexer; //标示是否是indexer程序
bool m_needMoreParser; //标示是否需要更细粒度分词
const char * m_pTempCur; //标示在m_BestWord中的位置
char m_BestWord[3 * SPH_MAX_WORD_LEN + 3]; //记录使用mmseg得到的最优分词结果
int m_iBestWordLength; //最优分词结果的长度

在6404行附近CSphTokenizer_UTF8Chinese::CSphTokenizer_UTF8Chinese ()这个构造函数中,增加以下语句进行初始化。

1
2
3
4
5
6
7
char *penv = getenv("IS_INDEXER");
if (penv != NULL) {
m_isIndexer = 1;
} else {
m_isIndexer = 0;
}
m_needMoreParser = false;

在6706行附近BYTE * CSphTokenizer_UTF8Chinese::GetToken ()函数中int iNum;语句后面增加如下语句

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
if(m_isIndexer && m_needMoreParser) { //对最优结果进行进一步细分
while (m_pTempCur < m_BestWord + m_iBestWordLength) {
if(m_processedParsedWordsNum == m_totalParsedWordsNum) {
size_t minWordLength = m_pResultPair[0].length;
for(int i = 1; i < m_totalParsedWordsNum; i++) {
if(m_pResultPair[i].length < minWordLength) {
minWordLength = m_pResultPair[i].length;
}
}
m_pTempCur += minWordLength;
m_pText=(Darts::DoubleArray::key_type *)(m_pCur + (m_pTempCur - m_BestWord));
iNum = m_tDa.commonPrefixSearch(m_pText, m_pResultPair, 256, m_pBufferMax-(m_pCur+(m_pTempCur-m_Best
Word)));
m_totalParsedWordsNum = iNum;
m_processedParsedWordsNum = 0;
} else {
iWordLength = m_pResultPair[m_processedParsedWordsNum].length;
m_processedParsedWordsNum++;
if (m_pTempCur == m_BestWord && iWordLength == m_iBestWordLength) { //是最优分词结果,跳过
continue;
}
memcpy(m_sAccum, m_pText, iWordLength);
m_sAccum[iWordLength]='\0';

m_pTokenStart = m_pCur + (m_pTempCur - m_BestWord);
m_pTokenEnd = m_pCur + (m_pTempCur - m_BestWord) + iWordLength;
return m_sAccum;
}
}
m_pCur += m_iBestWordLength;
m_needMoreParser = false;
iWordLength = 0;
}

在 iNum = m_tDa.commonPrefixSearch(m_pText, m_pResultPair, 256, m_pBufferMax-m_pCur);语句后面,增加如下语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(m_isIndexer && iNum > 1) {
m_iBestWordLength=getBestWordLength(m_pText, m_pBufferMax-m_pCur); //使用mmseg得到最优分词结果
memcpy(m_sAccum, m_pText, m_iBestWordLength);
m_sAccum[m_iBestWordLength]='\0';
m_pTokenStart = m_pCur;
m_pTokenEnd = m_pCur + m_iBestWordLength;

m_totalParsedWordsNum = iNum;
m_needMoreParser = true;
m_processedParsedWordsNum = 0;
memcpy(m_BestWord, m_pText, m_iBestWordLength);
m_BestWord[m_iBestWordLength]='\0';
m_pTempCur = m_BestWord;
return m_sAccum;
}

需要修改的地方就这么多。重新编译,生成后indexer后,设置环境变量,export IS_INDEXER=1,重建索引即可。这里需要注意的一点是,必须使用修改代码之前的searchd,这样才会符合我们的需求,如果使用修改代码之后的searchd,搜索西海时,会分成西海,西,海,然后去搜索,这就不是我们想要的。

对于代码,有几个关键的地方需要分明的。
1.GetToken函数
这个行数每次返回一个词,也就是分词的结果,返回前,需要设置m_pTokenStart和m_pTokenEnd,标示这个词在内容中的开始位置和结束位置。当返回值为NULL时,标示分词结束

2.m_pCur
这个用来标示当前的指针在内容的偏移位置,前面说到的设置m_pTokenStart和m_pTokenEnd就需要用到这个值

3.commonPrefixSearch函数
调用这个函数会返回所有共同前缀的词,结果保存在m_pResultPair中。例如m_pText当前位置是西,则会返回西,西海,西海岸这三个有共同前缀的词。

4.getBestWordLength函数
这个函数使用mmseg算法,得到下次分词最优结果的长度。例如m_pText当前位置是西,最优分词结果是西海岸,而在utf-8中,一个字为三个字节,所以函数返回8。

因为代码简单,所以就不细说了。这个修改,唯一不足的是,无法做到精确匹配。也是说,假设两个地点,一个是北京,一个是北京大学,搜索北京时,无法保证北京是排在第一个,即使它和搜索词精确匹配。这是因为在对北京进行更细粒度分词时,将北京分成北京,北,京这个三个词,这样破坏了Sphinx用来判断精确匹配的一些设置。为了纠正这个错误,组长和我又写了一些代码,这部分新增的代码就没有上面那部分好理解了,同时写的也有一些别扭。

联系作者

在论坛和网上找了一下,发现django-pagedown可以满足需求,搜索之后,按照https://pypi.python.org/pypi/django-pagedown/0.1.0 进行添加。

首先是安装

  1. Get the code: pip install django-pagedown
  2. Add pagedown to your INSTALLED_APPS
  3. Make sure to collect the static files: python manage.py collectstatic --noinput

1.首先pip install django-pagedown下载

2.之后添加pagedown到项目的’INSTALLED_APPS’中,

3.执行命令python manage.py collectstatic –noinput,收集js,css等django-pagedown用到的静态文件。

之后开始添加,我的博客是在blog目录下,在目录里创建forms.py,添加如下内容

1
2
3
4
5
6
7
8
from pagedown.widgets import AdminPagedownWidget
from django import forms
from blog.models import Post

class PostForm(forms.ModelForm):
content = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Post

这里是将content字段设置为markdown编辑,之后在admin.py中添加如下内容:

1
2
3
4
5
6
from django.contrib import admin
from blog.models import Post
from blog.forms import PostForm
class PostAdmin(admin.ModelAdmin):
form = PostForm
admin.site.register(Post, PostAdmin)

搞定。这样之后,在编辑content时,在它的下方就会有一个markdown的解析成HTML的结果。这里,我在数据库中只保存了markdown的原始内容,显示时还需要将它解析成HTML,这个另外再说。

联系作者

在开发环境中,使用Django自带的server,css这些静态资源都能够找到,可是用了gunicorn后,就找不到css等静态资源了,不知道如何是好,只记得之前看到在什么地方提到开发环境和生产环境是存在一些差异的。问过志容,志容说是要配置Nginx,可是我用gunicorn,根本都没有通过nginx。

今天正好看文档,正好看到,于是记下来。在https://docs.djangoproject.com/en/dev/howto/static-files/ 这个页面里有说到如何设置。

  • Set the STATIC_ROOT setting to the directory from which you’d like to serve these files, for example:

    1
    STATIC_ROOT = "/var/www/example.com/static/"
  • Run the collectstatic management command:

    1
    $ python manage.py collectstatic

为了避免硬编码,将STATIC_ROOT设置为os.path.join(BASE_DIR, ‘static’)后,

运行python manage.py collectstatic即可

联系作者

知道这个题目,是在任晓祎的博客里,据悉是加德纳改编的。题目如下:

某天, 老师召集了他最聪明的两个学生P和S, 递给每人一张纸条, 然后说, 有两个不小于2的整数x和y,满足x != y, 且x+y < 100. 给P的纸条上写有两个数的乘积p = x * y, 给S的纸条上写有两个数的和s = x+y, 请他们确定这两个数具体的值是多少. 于是P和S进行对话:

  1. P: 我无法确定这两个数是多少.
  2. S: 我知道你无法确定这两个数是多少.
  3. P: 既然这样, 那我知道这两个数是多少了.
  4. S: 既然这样, 那我也知道这两个数是多少了.
    请读者根据以上信息确定这两个数是多少.

当时看到这个题目后,和绍祝师兄一起做这道题,同时告诉海龙同学。和师兄讨论后,知道了题意,于是着手写代码,师兄用C++,我用C,结果两人都没写出来。很快海龙同学就得到了一个答案,是用手算得到的。我和师兄两人都汗颜了,有趣的是,这个答案就是唯一解。

回顾这道题,理清题意,大致如下:

1.P:我无法确定这两个数是多少。从这里可以得到,乘积p的分解不只一种,如12,可以分解成2 6, 3 4。

2.S:我知道你无法确定这两个数是多少。从这里可以得到,和s的分解中,x和y得到的乘积的分解不只一种,所有分解都满足条件1.如s为11时,可以分成2 + 9, 3 + 8, 4 + 7, 5 + 6,其中2和9的乘积为18,可以分解成2 9, 3 6; 对于3和8,4和7,5和6也是类似。

3.P: 既然这样, 那我知道这两个数是多少了。从这里可以得到乘积p的所有分解中,只有一个分解满足条件2。如18,18可以分解成2 9, 3 6,只有2和9的和11满足条件2,3和6的乘积不满足条件2.类似的还有24,28.

4.S: 既然这样, 那我也知道这两个数是多少了. 从这里可以得到s的所有分解中只有一组满足条件3.所以11不满足这个条件,因为11的分解中2 + 9, 3 + 8, 4 + 7,分别得到的18,24,28都满足条件3.

对于这种题目,还是用Python写比较方便。写成代码如下:

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
40
41
#coding:utf-8
from math import sqrt

def pone(p, u):
c = 0
for x in xrange(2, int(sqrt(p)) + 1):
if p % x == 0 and x + p / x < u:
c += 1
return c >= 2

def sone(s, u):
for x in xrange(2, s / 2):
y = s - x
if not pone(x * y, u):
return False
return True

def ptwo(p, u):
c = 0
for x in xrange(2, int(sqrt(p)) + 1):
if p % x == 0 and x + p / x < u:
y = p / x
if sone(x + y, u):
c += 1
return c == 1
def stwo(s, u):
c = 0
for x in xrange(2, s / 2):
y = s - x
if ptwo(x * y, u):
c += 1
return c == 1

if __name__ == "__main__":
u = 100
for x in xrange(2, u / 2):
for y in xrange(x + 1, u - x):
p = x * y
s = x + y
if pone(p, u) and sone(s, u) and ptwo(p, u) and stwo(s, u):
print "x:%d, y:%d, p:%d, s:%d " % (x, y, p, s)

联系作者

使用sed -i ‘s/text/test/‘ *.sh将当前目录下(不包括子目录),所有shell脚本的text替换为test, 其中-i参数是指示需要进行文件内替换,也就是改变文件的内容。

如果需要将子目录的也替换,则可以与find命令结合使用,使用find . -name “*.sh” | xargs sed -i ‘s/text/test/‘ 将当前目录下(包括子目录),所有shell脚本的text替换为test,

联系作者

在登陆后台的时候,要输入用户名和密码,此时希望打开页面,焦点就直接停留在用户名输入框,这样就可以省去移动鼠标的麻烦。

如以下一个登陆表单。

1
2
3
4
5
<form action="login.php" method="post" name="login">
用户名:<input name="username" type="text" value="" />
密码:<input name="password" type="password" />
<input type="submit" value="登陆" />
</form>

此时可以编写如下javascript:

1
2
3
4
5
6
7
window.onload = function() {
if (document.forms.login.username.value == "") {
document.forms.login.username.focus();
} else {
document.forms.login.password.focus();
}
}

联系作者