2007年4月4日星期三
谈谈Web Mining研究者掌握动态脚本语言的必要性发信站: 水木社区 (Fri Mar 23 17:58:30 2007),
2007年3月29日星期四
Worker Threads in C#(http://www.codeproject.com/csharp/workerthread.asp)
Introduction
The .NET framework provides a lot of ways to implement multithreading
programs. I want to show how we can run a worker thread which makes syncronous
calls to a user interface (for example, a thread that reads a long recordset and
fills some control in the form).
To run thread I use:
- Thread instance and main thread function
- Two events used to stop thread. First event is set when main thread
wants to stop worker thread; second event is set by worker thread when it really
stops.
.NET allows you to call System.Windows.Forms.Control functions only from the thread in
which the control was created. To run them from another thread we need to use
the Control.Invoke (synchronous call) or Control.BeginInvoke (asynchronous call) functions. For tasks like
showing database records we need Invoke.
To implement this we will use:
- A Delegate type for calling the form function. Delegate instance and
function called using this delegate - The
Invokecall from the worker thread.
The next problem is to stop the worker thread correctly. The steps to
do this are:
- Set the event "Stop Thread"
- Wait for the event "Thread is stopped"
- Wait for the event process messages using the
Application.DoEventsfunction. This prevents deadlocks because
the worker thread makesInvokecalls which are processed in
the main thread.
The thread function checks every iteration whether the "Stop Thread"
event has been set. If the event is set the function invokes clean-up
operations, sets the event "Thread is stopped" and returns.
Demo project has two classes: MainForm and LongProcess. The LongProcess.Run function
runs in a thread and fills the list box with some lines. The worker thread may
finish naturally or may be stopped when user presses the "Stop Thread" button or
closes the form.
Code fragments
// MainForm.cs
namespace WorkerThread
{
// delegates used to call MainForm functions from worker thread
public delegate void DelegateAddString(String s);
public delegate void DelegateThreadFinished();
public class MainForm : System.Windows.Forms.Form
{
// ...
// worker thread
Thread m_WorkerThread;
// events used to stop worker thread
ManualResetEvent m_EventStopThread;
ManualResetEvent m_EventThreadStopped;
// Delegate instances used to call user interface functions
// from worker thread:
public DelegateAddString m_DelegateAddString;
public DelegateThreadFinished m_DelegateThreadFinished;
// ...
public MainForm()
{
InitializeComponent();
// initialize delegates
m_DelegateAddString = new DelegateAddString(this.AddString);
m_DelegateThreadFinished = new DelegateThreadFinished(this.ThreadFinished);
// initialize events
m_EventStopThread = new ManualResetEvent(false);
m_EventThreadStopped = new ManualResetEvent(false);
}
// ...
// Start thread button is pressed
private void btnStartThread_Click(object sender, System.EventArgs e)
{
// ...
// reset events
m_EventStopThread.Reset();
m_EventThreadStopped.Reset();
// create worker thread instance
m_WorkerThread = new Thread(new ThreadStart(this.WorkerThreadFunction));
m_WorkerThread.Name = "Worker Thread Sample"; // looks nice in Output window
m_WorkerThread.Start();
}
// Worker thread function.
// Called indirectly from btnStartThread_Click
private void WorkerThreadFunction()
{
LongProcess longProcess;
longProcess = new LongProcess(m_EventStopThread, m_EventThreadStopped, this);
longProcess.Run();
}
// Stop worker thread if it is running.
// Called when user presses Stop button or form is closed.
private void StopThread()
{
if ( m_WorkerThread != null && m_WorkerThread.IsAlive ) // thread is active
{
// set event "Stop"
m_EventStopThread.Set();
// wait when thread will stop or finish
while (m_WorkerThread.IsAlive)
{
// We cannot use here infinite wait because our thread
// makes syncronous calls to main form, this will cause deadlock.
// Instead of this we wait for event some appropriate time
// (and by the way give time to worker thread) and
// process events. These events may contain Invoke calls.
if ( WaitHandle.WaitAll(
(new ManualResetEvent[] {m_EventThreadStopped}),
100,
true) )
{
break;
}
Application.DoEvents();
}
}
}
// Add string to list box.
// Called from worker thread using delegate and Control.Invoke
private void AddString(String s)
{
listBox1.Items.Add(s);
}
// Set initial state of controls.
// Called from worker thread using delegate and Control.Invoke
private void ThreadFinished()
{
btnStartThread.Enabled = true;
btnStopThread.Enabled = false;
}
}
}
// LongProcess.cs
namespace WorkerThread
{
public class LongProcess
{
// ...
// Function runs in worker thread and emulates long process.
public void Run()
{
int i;
String s;
for (i = 1; i <= 10; i++)
{ // make step s = "Step number " + i.ToString() + " executed";
Thread.Sleep(400); // Make synchronous call to main form.
// MainForm.AddString function runs in main thread.
// (To make asynchronous call use BeginInvoke)
m_form.Invoke(m_form.m_DelegateAddString, new Object[] {s}); // check if thread is cancelled if ( m_EventStop.WaitOne(0, true) )
{ // clean-up operations may be placed here // ... // inform main thread that this thread stopped m_EventStopped.Set();
return;
}
} // Make synchronous call to main form // to inform it that thread finished
m_form.Invoke(m_form.m_DelegateThreadFinished, null);
}
}
}
2007年3月6日星期二
用Visual C#调用Windows API函数(ZZ)
下面以C#为例简单介绍调用API的基本过程:
动态链接库函数的声明
动态链接库函数使用前必须声明,相对于VB,C#函数声明显得更加罗嗦,前者通过 Api Viewer粘贴以后,可以直接使用,而后者则需要对参数作些额外的变化工作。
动态链接库函数声明部分一般由下列两部分组成,一是函数名或索引号,二是动态链接库的文件名。
譬如,你想调用User32.DLL中的MessageBox函数,我们必须指明函数的名字MessageBoxA或MessageBoxW,以及库名字User32.dll,我们知道Win32 API对每一个涉及字符串和字符的函数一般都存在两个版本,单字节字符的ANSI版本和双字节字符的UNICODE版本。
下面是一个调用API函数的例子:
[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW", SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.StdCall)]
public static extern bool MoveFile(String src, String dst);
其中入口点EntryPoint标识函数在动态链接库的入口位置,在一个受管辖的工程中,目标函数的原始名字和序号入口点不仅标识一个跨越互操作界限的函数。而且,你还可以把这个入口点映射为一个不同的名字,也就是对函数进行重命名。重命名可以给调用函数带来种种便利,通过重命名,一方面我们不用为函数的大小写伤透脑筋,同时它也可以保证与已有的命名规则保持一致,允许带有不同参数类型的函数共存,更重要的是它简化了对ANSI和Unicode版本的调用。CharSet用于标识函数调用所采用的是Unicode或是ANSI版本,ExactSpelling=false将告诉编译器,让编译器决定使用Unicode或者是Ansi版本。其它的参数请参考MSDN在线帮助.
在C#中,你可以在EntryPoint域通过名字和序号声明一个动态链接库函数,如果在方法定义中使用的函数名与DLL入口点相同,你不需要在EntryPoint域显示声明函数。否则,你必须使用下列属性格式指示一个名字和序号。
[DllImport("dllname", EntryPoint="Functionname")]
[DllImport("dllname", EntryPoint="#123")]
值得注意的是,你必须在数字序号前加“#”
下面是一个用MsgBox替换MessageBox名字的例子:
[C#]
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("user32.dll", EntryPoint="MessageBox")]
public static extern int MsgBox(int hWnd, String text, String caption, uint type);
}
许多受管辖的动态链接库函数期望你能够传递一个复杂的参数类型给函数,譬如一个用户定义的结构类型成员或者受管辖代码定义的一个类成员,这时你必须提供额外的信息格式化这个类型,以保持参数原有的布局和对齐。
C#提供了一个StructLayoutAttribute类,通过它你可以定义自己的格式化类型,在受管辖代码中,格式化类型是一个用StructLayoutAttribute说明的结构或类成员,通过它能够保证其内部成员预期的布局信息。布局的选项共有三种:
布局选项
描述
LayoutKind.Automatic
为了提高效率允许运行态对类型成员重新排序。
注意:永远不要使用这个选项来调用不受管辖的动态链接库函数。
LayoutKind.Explicit
对每个域按照FieldOffset属性对类型成员排序
LayoutKind.Sequential
对出现在受管辖类型定义地方的不受管辖内存中的类型成员进行排序。
传递结构成员
下面的例子说明如何在受管辖代码中定义一个点和矩形类型,并作为一个参数传递给User32.dll库中的PtInRect函数,
函数的不受管辖原型声明如下:
BOOL PtInRect(const RECT *lprc, POINT pt);
注意你必须通过引用传递Rect结构参数,因为函数需要一个Rect的结构指针。
[C#]
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point {
public int x;
public int y;
}
[StructLayout(LayoutKind.Explicit]
public struct Rect {
[FieldOffset(0)] public int left;
[FieldOffset(4)] public int top;
[FieldOffset(8)] public int right;
[FieldOffset(12)] public int bottom;
}
class Win32API {
[DllImport("User32.dll")]
public static extern Bool PtInRect(ref Rect r, Point p);
}
类似你可以调用GetSystemInfo函数获得系统信息:
? using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEM_INFO {
public uint dwOemId;
public uint dwPageSize;
public uint lpMinimumApplicationAddress;
public uint lpMaximumApplicationAddress;
public uint dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public uint dwProcessorLevel;
public uint dwProcessorRevision;
}
[DllImport("kernel32")]
static extern void GetSystemInfo(ref SYSTEM_INFO pSI);
SYSTEM_INFO pSI = new SYSTEM_INFO();
GetSystemInfo(ref pSI);
类成员的传递
同样只要类具有一个固定的类成员布局,你也可以传递一个类成员给一个不受管辖的动态链接库函数,下面的例子主要说明如何传递一个sequential顺序定义的MySystemTime类给User32.dll的GetSystemTime函数, 函数用C/C++调用规范如下:
void GetSystemTime(SYSTEMTIME* SystemTime);
不像传值类型,类总是通过引用传递参数.
[C#]
[StructLayout(LayoutKind.Sequential)]
public class MySystemTime {
public ushort wYear;
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
}
class Win32API {
[DllImport("User32.dll")]
public static extern void GetSystemTime(MySystemTime st);
}
回调函数的传递:
从受管辖的代码中调用大多数动态链接库函数,你只需创建一个受管辖的函数定义,然后调用它即可,这个过程非常直接。
如果一个动态链接库函数需要一个函数指针作为参数,你还需要做以下几步:
首先,你必须参考有关这个函数的文档,确定这个函数是否需要一个回调;第二,你必须在受管辖代码中创建一个回调函数;最后,你可以把指向这个函数的指针作为一个参数创递给DLL函数,.
回调函数及其实现:
回调函数经常用在任务需要重复执行的场合,譬如用于枚举函数,譬如Win32 API 中的EnumFontFamilies(字体枚举), EnumPrinters(打印机), EnumWindows (窗口枚举)函数. 下面以窗口枚举为例,谈谈如何通过调用EnumWindow 函数遍历系统中存在的所有窗口
分下面几个步骤:
1. 在实现调用前先参考函数的声明
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARMAM IParam)
显然这个函数需要一个回调函数地址作为参数.
2. 创建一个受管辖的回调函数,这个例子声明为代表类型(delegate),也就是我们所说的回调,它带有两个参数hwnd和lparam,第一个参数是一个窗口句柄,第二个参数由应用程序定义,两个参数均为整形。
当这个回调函数返回一个非零值时,标示执行成功,零则暗示失败,这个例子总是返回True值,以便持续枚举。
3. 最后创建以代表对象(delegate),并把它作为一个参数传递给EnumWindows 函数,平台会自动地 把代表转化成函数能够识别的回调格式。
[C#]
using System;
using System.Runtime.InteropServices;
public delegate bool CallBack(int hwnd, int lParam);
public class EnumReportApp {
[DllImport("user32")]
public static extern int EnumWindows(CallBack x, int y);
public static void Main()
{
CallBack myCallBack = new CallBack(EnumReportApp.Report);
EnumWindows(myCallBack, 0);
}
public static bool Report(int hwnd, int lParam) {
Console.Write("窗口句柄为");
Console.WriteLine(hwnd);
return true;
}
}
指针类型参数传递:
在Windows API函数调用时,大部分函数采用指针传递参数,对一个结构变量指针,我们除了使用上面的类和结构方法传递参数之外,我们有时还可以采用数组传递参数。
下面这个函数通过调用GetUserName获得用户名
BOOL GetUserName(
LPTSTR lpBuffer, // 用户名缓冲区
LPDWORD nSize // 存放缓冲区大小的地址指针
);
[DllImport("Advapi32.dll",
EntryPoint="GetComputerName",
ExactSpelling=false,
SetLastError=true)]
static extern bool GetComputerName (
[MarshalAs(UnmanagedType.LPArray)] byte[] lpBuffer,
[MarshalAs(UnmanagedType.LPArray)] Int32[] nSize );
这个函数接受两个参数,char * 和int *,因为你必须分配一个字符串缓冲区以接受字符串指针,你可以使用String类代替这个参数类型,当然你还可以声明一个字节数组传递ANSI字符串,同样你也可以声明一个只有一个元素的长整型数组,使用数组名作为第二个参数。上面的函数可以调用如下:
byte[] str=new byte[20];
Int32[] len=new Int32[1];
len[0]=20;
GetComputerName (str,len);
MessageBox.Show(System.Text.Encoding.ASCII.GetString(str));
最后需要提醒的是,每一种方法使用前必须在文件头加上:
using System.Runtime.InteropServices;
2007年3月5日星期一
利用Lucene搜索Java源代码 (ZZ)
编写类JavaSourceCodeIndexer
第二步生成索引。用来建立索引的非常重要的类有IndexWriter、Analyzer、Document和Field。对每一个源代码文件建立Lucene的一个Document实例。解析源代码文件并且摘录出与代码相关的语法元素,主要包括:导入声明、类名称、所继承的类、实现的接口、实现的方法、方法使用的参数和每个方法的代码等。然后把这些句法元素添加到Document实例中每个独立的Field实例中。然后使用存储索引的IndexWriter实例将Document实例添加到索引中。
下面列出了JavaSourceCodeIndexer类的源代码。该类使用了JavaParser类解析Java文件和摘录语法元素,也可以使用Eclipse3.0 ASTParser。这里就不探究JavaParser类的细节了,因为其它解析器也可以用于提取相关源码元素。在源代码文件提取元素的过程中,创建Filed实例并添加到Document实例中。
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import com.infosys.lucene.code.JavaParser.*;
public class JavaSourceCodeIndexer {
private static JavaParser parser = new JavaParser();
private static final String IMPLEMENTS = "implements";
private static final String IMPORT = "import";
...
public static void main(String[] args) {
File indexDir = new File("C:\\Lucene\\Java");
File dataDir = new File("C:\\JavaSourceCode ");
IndexWriter writer = new IndexWriter(indexDir,
new JavaSourceCodeAnalyzer(), true);
indexDirectory(writer, dataDir);
writer.close();
}
public static void indexDirectory(IndexWriter writer, File dir){
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++) {
File f = files[i];
// Create a Lucene Document
Document doc = new Document();
// Use JavaParser to parse file
parser.setSource(f);
addImportDeclarations(doc, parser);
addComments(doc, parser);
// Extract Class elements Using Parser
JClass cls = parser.getDeclaredClass();
addClass(doc, cls);
// Add field to the Lucene Document
doc.add(Field.UnIndexed(FILENAME, f.getName()));
writer.addDocument(doc);
}
}
private static void addClass(Document doc, JClass cls) {
//For each class add Class Name field
doc.add(Field.Text(CLASS, cls.className));
String superCls = cls.superClass;
if (superCls != null)
//Add the class it extends as extends field
doc.add(Field.Text(EXTENDS, superCls));
// Add interfaces it implements
ArrayList interfaces = cls.interfaces;
for (int i = 0; i < interfaces.size(); i++)
doc.add(Field.Text(IMPLEMENTS, (String) interfaces.get(i)));
//Add details on methods declared
addMethods(cls, doc);
ArrayList innerCls = cls.innerClasses;
for (int i = 0; i < innerCls.size(); i++)
addClass(doc, (JClass) innerCls.get(i));
}
private static void addMethods(JClass cls, Document doc) {
ArrayList methods = cls.methodDeclarations;
for (int i = 0; i < methods.size(); i++) {
JMethod method = (JMethod) methods.get(i);
// Add method name field
doc.add(Field.Text(METHOD, method.methodName));
// Add return type field
doc.add(Field.Text(RETURN, method.returnType));
ArrayList params = method.parameters;
for (int k = 0; k < params.size(); k++)
// For each method add parameter types
doc.add(Field.Text(PARAMETER, (String)params.get(k)));
String code = method.codeBlock;
if (code != null)
//add the method code block
doc.add(Field.UnStored(CODE, code));
}
}
private static void addImportDeclarations(Document doc, JavaParser parser) {
ArrayList imports = parser.getImportDeclarations();
if (imports == null) return;
for (int i = 0; i < imports.size(); i++)
//add import declarations as keyword
doc.add(Field.Keyword(IMPORT, (String) imports.get(i)));
}
}
Lucene有四种不同的字段类型:Keyword,UnIndexed,UnStored和Text,用于指定建立最佳索引。
Keyword字段是指不需要分析器解析但需要被编入索引并保存到索引中的部分。JavaSourceCodeIndexer类使用该字段来保存导入类的声明。
UnIndexed字段是既不被分析也不被索引,但是要被逐字逐句的将其值保存到索引中。由于我们一般要存储文件的位置但又很少用文件名作为关键字来搜索,所以用该字段来索引Java文件名。
UnStored字段和UnIndexed字段相反。该类型的Field要被分析并编入索引,但其值不会被保存到索引中。由于存储方法的全部源代码需要大量的空间。所以用UnStored字段来存储被索引的方法源代码。可以直接从Java源文件中取出方法的源代码,这样作可以控制我们的索引的大小。
Text字段在索引过程中是要被分析、索引并保存的。类名是作为Text字段来保存。下表展示了JavaSourceCodeIndexer类使用Field字段的一般情况。
1. 用Lucene建立的索引可以用Luke预览和修改,Luke是用于理解索引很有用的一个开源工具。图1中是Luke工具的一张截图,它显示了JavaSourceCodeIndexer类建立的索引。
图1:在Luke中索引截图
如图所见,导入类的声明没有标记化或分析就被保存了。类名和方法名被转换为小写字母后,才保存的。
查询Java源代码
建立多字段索引后,可以使用Lucene来查询这些索引。它提供了这两个重要类分别是IndexSearcher和QueryParser,用于搜索文件。QueryParser类则用于解析由用户输入的查询表达式,同时IndexSearcher类在文件中搜索满足查询条件的结果。下列表格显示了一些可能发生的查询及它的含义:
用户通过索引不同的语法元素组成有效的查询条件并搜索代码。下面列出了用于搜索的示例代码。
public class JavaCodeSearch {
public static void main(String[] args) throws Exception{
File indexDir = new File(args[0]);
String q = args[1]; //parameter:JGraph code:insert
Directory fsDir = FSDirectory.getDirectory(indexDir,false);
IndexSearcher is = new IndexSearcher(fsDir);
PerFieldAnalyzerWrapper analyzer = new
PerFieldAnalyzerWrapper( new
JavaSourceCodeAnalyzer());
analyzer.addAnalyzer("import", new KeywordAnalyzer());
Query query = QueryParser.parse(q, "code", analyzer);
long start = System.currentTimeMillis();
Hits hits = is.search(query);
long end = System.currentTimeMillis();
System.err.println("Found " + hits.length() +
" docs in " + (end-start) + " millisec");
for(int i = 0; i < hits.length(); i++){
Document doc = hits.doc(i);
System.out.println(doc.get("filename")
+ " with a score of " + hits.score(i));
}
is.close();
}
}
IndexSearcher实例用FSDirectory来打开包含索引的目录。然后使用Analyzer实例分析搜索用的查询字符串,以确保它与索引(还原词根,转换小写字母,过滤掉,等等)具有同样的形式。为了避免在查询时将Field作为一个关键字索引,Lucene做了一些限制。Lucene用Analyzer分析在QueryParser实例里传给它的所有字段。为了解决这个问题,可以用Lucene提供的PerFieldAnalyzerWrapper类为查询中的每个字段指定必要的分析。因此,查询字符串import:org.w3c.* AND code:Document将用KeywordAnalyzer来解析字符串org.w3c.*并且用JavaSourceCodeAnalyzer来解析Document。QueryParser实例如果查询没有与之相符的字段,就使用默认的字段:code,使用PerFieldAnalyzerWrapper来分析查询字符串,并返回分析后的Query实例。IndexSearcher实例使用Query实例并返回一个Hits实例,它包含了满足查询条件的文件。
结束语
这篇文章介绍了Lucene——文本搜索引擎,其可以通过加载分析器及多字段索引来实现源代码搜索。文章只介绍了代码搜索引擎的基本功能,同时在源码检索中使用愈加完善的分析器可以提高检索性能并获得更好的查询结果。这种搜索引擎可以允许用户在软件开发社区搜索和共享源代码。
资源
这篇文章的示例Sample code
Matrix:http://www.matrix.org.cn/
Onjava:http://www.onjava.com/
Lucene home page
"Introduction to Text Indexing with Apache Jakarta Lucene:" 这是篇简要介绍使用Lucene的文章。
Lucene in Action: 在使用Lucene方面进行了深入地讲解。
Nutch爬虫工作流程及文件格式详细分析 (ZZ)
Crawler和Searcher两部分尽量分开的目的主要是为了使两部分可以分布式配置在硬件平台上,例如将Crawler和Searcher分别放在两个主机上,这样可以提升性能。
爬虫,Crawler:
Crawler的重点在两个方面,Crawler的工作流程和涉及的数据文件的格式和含义。数据文件主要包括三类,分别是web database,一系列的segment加上index,三者的物理文件分别存储在爬行结果目录下的db目录下webdb子文件夹内,segments文件夹和index文件夹。那么三者分别存储的信息是什么呢?
Web database,也叫WebDB,其中存储的是爬虫所抓取网页之间的链接结构信息,它只在爬虫Crawler工作中使用而和Searcher的工作没有任何关系。WebDB内存储了两种实体的信息:page和link。Page实体通过描述网络上一个网页的特征信息来表征一个实际的网页,因为网页有很多个需要描述,WebDB中通过网页的URL和网页内容的MD5两种索引方法对这些网页实体进行了索引。Page实体描述的网页特征主要包括网页内的link数目,抓取此网页的时间等相关抓取信息,对此网页的重要度评分等。同样的,Link实体描述的是两个page实体之间的链接关系。WebDB构成了一个所抓取网页的链接结构图,这个图中Page实体是图的结点,而Link实体则代表图的边。
一次爬行会产生很多个segment,每个segment内存储的是爬虫Crawler在单独一次抓取循环中抓到的网页以及这些网页的索引。Crawler爬行时会根据WebDB中的link关系按照一定的爬行策略生成每次抓取循环所需的fetchlist,然后Fetcher通过fetchlist中的URLs抓取这些网页并索引,然后将其存入segment。Segment是有时限的,当这些网页被Crawler重新抓取后,先前抓取产生的segment就作废了。在存储中。Segment文件夹是以产生时间命名的,方便我们删除作废的segments以节省存储空间。
Index是Crawler抓取的所有网页的索引,它是通过对所有单个segment中的索引进行合并处理所得的。Nutch利用Lucene技术进行索引,所以Lucene中对索引进行操作的接口对Nutch中的index同样有效。但是需要注意的是,Lucene中的segment和Nutch中的不同,Lucene中的segment是索引index的一部分,但是Nutch中的segment只是WebDB中各个部分网页的内容和索引,最后通过其生成的index跟这些segment已经毫无关系了。
Crawler工作流程:
在分析了Crawler工作中设计的文件之后,接下来我们研究一下Crawler的抓取流程以及这些文件在抓取中扮演的角色。Crawler的工作原理主要是:首先Crawler根据WebDB生成一个待抓取网页的URL集合叫做Fetchlist,接着下载线程Fetcher开始根据Fetchlist将网页抓取回来,如果下载线程有很多个,那么就生成很多个Fetchlist,也就是一个Fetcher对应一个Fetchlist。然后Crawler根据抓取回来的网页WebDB进行更新,根据更新后的WebDB生成新的Fetchlist,里面是未抓取的或者新发现的URLs,然后下一轮抓取循环重新开始。这个循环过程可以叫做“产生/抓取/更新”循环。
指向同一个主机上Web资源的URLs通常被分配到同一个Fetchlist中,这样的话防止过多的Fetchers对一个主机同时进行抓取造成主机负担过重。另外Nutch遵守Robots Exclusion Protocol,网站可以通过自定义Robots.txt控制Crawler的抓取。
在Nutch中,Crawler操作的实现是通过一系列子操作的实现来完成的。这些子操作Nutch都提供了子命令行可以单独进行调用。下面就是这些子操作的功能描述以及命令行,命令行在括号中。
1. 创建一个新的WebDb (admin db -create).
2. 将抓取起始URLs写入WebDB中 (inject).
3. 根据WebDB生成fetchlist并写入相应的segment(generate).
4. 根据fetchlist中的URL抓取网页 (fetch).
5. 根据抓取网页更新WebDb (updatedb).
6. 循环进行3-5步直至预先设定的抓取深度。
7. 根据WebDB得到的网页评分和links更新segments (updatesegs).
8. 对所抓取的网页进行索引(index).
9. 在索引中丢弃有重复内容的网页和重复的URLs (dedup).
10. 将segments中的索引进行合并生成用于检索的最终index(merge).
Crawler详细工作流程是:在创建一个WebDB之后(步骤1), “产生/抓取/更新”循环(步骤3-6)根据一些种子URLs开始启动。当这个循环彻底结束,Crawler根据抓取中生成的segments创建索引(步骤7-10)。在进行重复URLs清除(步骤9)之前,每个segment的索引都是独立的(步骤8)。最终,各个独立的segment索引被合并为一个最终的索引index(步骤10)。
其中有一个细节问题,Dedup操作主要用于清除segment索引中的重复URLs,但是我们知道,在WebDB中是不允许重复的URL存在的,那么为什么这里还要进行清除呢?原因在于抓取的更新。比方说一个月之前你抓取过这些网页,一个月后为了更新进行了重新抓取,那么旧的segment在没有删除之前仍然起作用,这个时候就需要在新旧segment之间进行除重。
另外这些子操作在第一次进行Crawler运行时可能并不用到,它们主要用于接下来的索引更新,增量搜索等操作的实现。这些在以后的文章中我再详细讲。
个性化设置:
Nutch中的所有配置文件都放置在总目录下的conf子文件夹中,最基本的配置文件是conf/nutch-default.xml。这个文件中定义了Nutch的所有必要设置以及一些默认值,它是不可以被修改的。如果你想进行个性化设置,你需要在conf/nutch-site.xml进行设置,它会对默认设置进行屏蔽。
Nutch考虑了其可扩展性,你可以自定义插件plugins来定制自己的服务,一些plugins存放于plugins子文件夹。Nutch的网页解析与索引功能是通过插件形式进行实现的,例如,对HTML文件的解析与索引是通过HTML document parsing plugin, parse-html实现的。所以你完全可以自定义各种解析插件然后对配置文件进行修改,然后你就可以抓取并索引各种类型的文件了。
2007年3月2日星期五
Nutch初体验(ZZ)
Nutch vs Lucene
Lucene 不是完整的应用程序,而是一个用于实现全文检索的软件库。
Nutch 是一个应用程序,可以以 Lucene 为基础实现搜索引擎应用。
Nutch vs GRUB
http://www.wespoke.com/archives/000879.php">这里]
Nutch 则还可以存储到数据库并建立索引。
[引自
Nutch 的早期版本不支持中文搜索,而最新的版本(2004-Aug-04 发布了 http://www.dbanotes.net)为例,先进行一下针对企业内部网的测试。
在 nutch 目录中创建一个包含该网站顶级网址的文件 urls ,包含如下内容:
http://www.dbanotes.net/
然后编辑conf/crawl-urlfilter.txt 文件,设定过滤信息,我这里只修改了MY.DOMAIN.NAME:
# accept hosts in MY.DOMAIN.NAME+^http://([a-z0-9]*\.)*dbanotes.net/
运行如下命令开始抓取分析网站内容:
[root@fc3 nutch]# bin/nutch crawl urls -dir crawl.demo -depth 2 -threads 4 >& crawl.log
depth 参数指爬行的深度,这里处于测试的目的,选择深度为 2 ;
threads 参数指定并发的进程 这是设定为 4 ;
在该命令运行的过程中,可以从 crawl.log 中查看 nutch 的行为以及过程:
......050102 200336 loading file:/u01/nutch/conf/nutch-site.xml050102 200336 crawl started in: crawl.demo 050102 200336 rootUrlFile = urls 050102 200336 threads = 4050102 200336 depth = 2050102 200336 Created webdb at crawl.demo/db......050102 200336 loading file:/u01/nutch/conf/nutch-site.xml050102 200336 crawl started in: crawl.demo050102 200336 rootUrlFile = urls050102 200336 threads = 4050102 200336 depth = 2050102 200336 Created webdb at crawl.demo/db050102 200336 Starting URL processing050102 200336 Using URL filter: net.nutch.net.RegexURLFilter...... 050102 200337 Plugins: looking in: /u01/nutch/plugins 050102 200337 parsing: /u01/nutch/plugins/parse-html/plugin.xml 050102 200337 parsing: /u01/nutch/plugins/parse-pdf/plugin.xml 050102 200337 parsing: /u01/nutch/plugins/parse-ext/plugin.xml 050102 200337 parsing: /u01/nutch/plugins/parse-msword/plugin.xml 050102 200337 parsing: /u01/nutch/plugins/query-site/plugin.xml 050102 200337 parsing: /u01/nutch/plugins/protocol-http/plugin.xml 050102 200337 parsing: /u01/nutch/plugins/creativecommons/plugin.xml050102 200337 parsing: /u01/nutch/plugins/language-identifier/plugin.xml050102 200337 parsing: /u01/nutch/plugins/query-basic/plugin.xml 050102 200337 logging at INFO 050102 200337 fetching http://www.dbanotes.net/ 050102 200337 http.proxy.host = null 050102 200337 http.proxy.port = 8080 050102 200337 http.timeout = 10000 050102 200337 http.content.limit = 65536 050102 200337 http.agent = NutchCVS/0.05 (Nutch; http://www.nutch.org/docs/en/bot.html; nutch-agent@lists.sourceforge.net)050102 200337 fetcher.server.delay = 1000 050102 200337 http.max.delays = 100 050102 200338 http://www.dbanotes.net/: setting encoding to GB18030 050102 200338 CC: found http://creativecommons.org/licenses/by-nc-sa/2.0/ in rdf of http://www.dbanotes.net/050102 200338 CC: found text in http://www.dbanotes.net/ 050102 200338 status: 1 pages, 0 errors, 12445 bytes, 1067 ms 050102 200338 status: 0.9372071 pages/s, 91.12142 kb/s, 12445.0 bytes/page050102 200339 Updating crawl.demo/db 050102 200339 Updating for crawl.demo/segments/20050102200336 050102 200339 Finishing update 64,1 7%050102 200337 parsing: /u01/nutch/plugins/query-basic/plugin.xml050102 200337 logging at INFO050102 200337 fetching http://www.dbanotes.net/050102 200337 http.proxy.host = null050102 200337 http.proxy.port = 8080050102 200337 http.timeout = 10000050102 200337 http.content.limit = 65536050102 200337 http.agent = NutchCVS/0.05 (Nutch; http://www.nutch.org/docs/en/bot.html; nutch-agent@lists.sourceforge.net)050102 200337 fetcher.server.delay = 1000050102 200337 http.max.delays = 100......
之后配置 Tomcat (我的 tomcat 安装在 /opt/Tomcat) ,
[root@fc3 nutch]# rm -rf /opt/Tomcat/webapps/ROOT*[root@fc3 nutch]# cp nutch*.war /opt/Tomcat/webapps/ROOT.war[root@fc3 webapps]# cd /opt/Tomcat/webapps/[root@fc3 webapps]# jar xvf ROOT.war[root@fc3 webapps]# ../bin/catalina.sh start
浏览器中输入 http://localhost:8080/ 查看结果(远程查看需要将 localhost 换成相应的IP):
搜索测试:
可以看到,Nutch 亦提供快照功能。下面进行中文搜索测试:
注意结果中的那个“评分详解”,是个很有意思的功能(Nutch 具有一个链接分析模块),通过这些数据可以进一步理解该算法。
考虑到带宽的限制,暂时不对整个Web爬行的方式进行了测试了。值得一提的是,在测试的过程中,nutch 的爬行速度还是不错的(相对我的糟糕带宽)。
Nutch 目前还不支持 PDF(开发中,不够完善) 与 图片 等对象的搜索。中文分词技术还不够好,通过“评分详解”可看出,对中文,比如“数据库管理员”,是分成单独的字进行处理的。但作为一个开源搜索引擎软件,功能是可圈可点的。毕竟,主要开发者 < color="#0000cc">Doug Cutting 就是开发 http://hedong.3322.org/archives/000247.html">
2007年2月26日星期一
Eclipse 开发环境的调试功能总结(ZZ)
本文概述了怎样使用 Eclipse 平台的内置调试功能来调试您的软件项目。调试是程序员无法回避的工作。调试方法有许多种,但归根结底,就是找到引发错误的代码。举例来说,在 Linux 应用程序中,分段故障被认为是最常见的错误之一。当程序尝试访问未分配给它的内存并因为分段违例而终止时,将产生这种错误。要修正这种错误,您需要找到引发该行为的那行代码。一旦找到有问题的代码行,这对于知道引发错误的上下文及其相关的值、变量和方法也是有所帮助的。使用调试器将使查找这些信息变得相当简单。Eclipse 调试器及 Debug 视图Eclipse 平台的特色在于内置了 Java 调试器,该调试器提供所有标准调试功能,包括进行单步执行、设置断点和值、检查变量和值以及暂挂和恢复线程的能力。此外,您还可以调试在远程机器上运行的应用程序。Eclipse 平台主要是一个 Java 开发环境,但其体系结构同时也向其它编程语言开放。如以下您将看到的,同一个 Eclipse 的 Debug 视图也可用于 C 和 C++ 编程语言。Eclipse 平台工作台(Eclipse Platform Workbench)及其工具是基于 Java 开发工具(JDT)组件所构建的。这些组件向 Eclipse 提供以下功能:* 项目管理工具* 透视图和视图* 构建器、编辑器、搜索和构建功能* 调试器Eclipse 调试器本身是作为 Eclipse 二进制文件中包含的标准插件而存在的。Eclipse 还有一个特别的 Debug 视图,允许您在工作台中管理程序的调试和运行。它为调试中的每个目标显示其暂挂线程的堆栈帧。程序中的各个线程以作为树的节点出现,而 Debug 视图则显示运行中的各目标的进程。如果暂挂一个线程,则其堆栈帧显示为子元素。在您开始使用 Eclipse 调试器之前,假定您已经安装了适当的 Java SDK/JRE(我推荐您使用 Java VM 1.4)和 Eclipse 平台 SDK 2.0/2.1,且两者都工作正常。一般来说,先使用 Eclipse 样本来测试一下调试选项是一个好主意。如果您要开发和调试 C/C++ 项目,您还需要得到并安装 C/C++ 开发工具(C/C++ Development Tool,CDT)。有关 Java SDK/JRE、Eclipse 平台和样本以及 CDT 的链接,请参阅本文后面的参考资料。图 1 显示了 Debug 用户界面的常规视图。图 1. Eclipse Debug 视图用户界面的常规视图[myimg]upload/debug.png[/myimg]调试 Java在您能调试您的项目之前,需要先完整地编译和运行代码。您首先需要为您的应用程序创建运行配置并确认其正常启动。之后,您需要使用 Run > Debug... 菜单,以同样的方式设置调试配置。您还需要选择作为主 Java 类的由调试器使用的类(也请参阅图 2)。对一个项目,您希望有几种调试配置就可以有几种。当调试器启动后(通过 Run > Debug...),会在一个新窗口打开它,您可以准备开始调试。图 2. 在调试配置中设置项目的主 Java 类[myimg]upload/debug-config.png[/myimg]以下是最常见的 Eclipse 调试操作的示例指示信息:设置断点当您启动应用程序以进行调试时,Eclipse 自动切换到 Debug 透视图。无庸置疑,最常见的调试过程就是设置断点,以允许检查在条件语句和循环中的变量和值。要在 Java 透视图的 Package Explorer 视图中设置断点,双击所选的源代码文件,在编辑器中打开它。遍历全部代码,将光标放置在含有可疑代码的那一行的标记栏上(在编辑器区域的左侧)。双击以设置断点(也请参阅图 3)。图 3. 在编辑器左侧边缘可看到两个断点标记[myimg]upload/breakpoints2.png[/myimg]现在通过 Run > Debug... 菜单启动调试会话。有一点很重要,不要把数条语句放在同一行,因为您不能在同一行的多条语句上单步跳过或设置行断点(也请参阅图 4)。图 4. 视图通过左侧边缘的箭头指出当前正在执行的行[myimg]upload/pointer.png[/myimg]条件断点一旦您找到出错的地方,您会想要了解在崩溃前程序在干些什么。完成该工作的一种方法是单步执行程序中的每条语句,一次一句,直到到达出问题的地方。有时候更好的方法是仅运行某段代码并在出问题的地方终止其执行,这样就可以检查该位置上的数据。要实现这一点,可能要声明每当表达式的值更改时就被触发的条件断点(请参阅图 5)。此外,在输入条件表达式时还可以使用代码辅助。图 5. 设置条件断点触发器[myimg]upload/conditional.png[/myimg]对表达式求值要在 Debug 透视图的编辑器中求表达式的值,选中设置有断点的一整行,并在上下文菜单中选择 Inspect 选项(请参阅图 6)。表达式是在当前堆栈帧的上下文中求值的,其结果显示在 Display 窗口的 Expressions 视图中。图 6. 用 Inspect 选项求表达式的值[myimg]upload/debug-menu1.png[/myimg]查看变量Variables 视图(在 Display 窗口中)显示了选中的堆栈帧中的变量值(请参阅图 7)。要查看所请求的变量,只需展开 Variables 视图中的树直到您看到所请求的元素为止。您也可以在 Debug 视图中单步执行代码的同时,在 Variables 视图中查看变量。图 7. 在 Display 窗口中查看变量[myimg]upload/variables.png[/myimg]当调试器在断点上停止时,您可以通过在 Run > Debug... 菜单上选择 Step Over 选项来继续调试器会话(请参阅图 8)。这将单步跳过突出显示的代码行并执行同一个方法中的下一行(或者它在调用当前方法的方法中继续)。作为最后一步的结果而发生更改的变量用颜色突出显示(缺省值是红色),所用颜色可在“Changed Variable Value Color”首选项(由 Debug Variable Views 指定)中指定。图 8. Run... 菜单中的调试器命令[myimg]upload/debug-menu.png[/myimg]要在 Debug 视图中暂挂线程的执行,选择一个运行中的线程并单击 Debug 视图工具栏中的 Suspend 按钮。会显示该线程的当前调用堆栈,并且在 Debug 透视图的编辑器中突出显示当前执行的行。暂挂线程时,将光标放置到 Java 编辑器中的变量上,该变量的值显示在一个小悬浮窗口中。同样,该线程的顶部堆栈帧被自动选中,该堆栈帧中的可视变量显示在 Variables 视图中。可以通过在 Variables 视图中单击变量名来检查相应的变量。热交换错误修正:实时代码修正如果您运行的是 JVM 1.4(Java 虚拟机,Java Virtual Machine),Eclipse 2.0.2 和 2.1 提供一个叫做热交换错误修正(Hotswap Bug Fixing)的新功能(无法用于 JVM 1.3 或更低版本 - 也请参阅图 9)。它允许在调试器会话过程中更改源代码,这要比“退出应用程序,更改源代码,重新编译,再启动另一个调试会话”的一系列步骤好多了。要使用该功能,只需在编辑器中更改代码并恢复调试。由于 JVM 1.4 与 Java 平台调试器体系结构(Java Platform Debugger Architecture,JPDA)兼容,所以能使用该功能。JPDA 实现了在运行中的应用程序中用经过修改的代码进行替换的能力。当然,当启动您的应用程序或找到出错点需要花费很长时间的时候,该功能极其有用。图 9. 热交换错误修正功能不能在 JVM 1.3 及更低版本中使用[myimg]upload/hot-code.png[/myimg]如果您完成调试时程序还没有被完整地执行过,请在 Debug 视图中的上下文菜单中选择 Terminate 选项。一个常见的错误是您在调试器会话中使用了 Debug 或 Run 而不是 Resume。这样将会启动另一个调试器会话,而不是继续当前的会话。远程调试Eclipse 调试器提供了一个有趣的选项,可用于调试远程应用程序。它可以连接到一个运行 Java 应用程序的远程 VM 上,并将其连接到内部调试器上。处理远程调试会话非常类似于本地调试。不过,远程调试配置要求对 Run > Debug... 窗口进行不同的设置。您首先要选择左侧视图中的 Remote Java Application 项,单击 New 按钮。这样就创建了一个新的远程启动配置,并显示三个选项卡:Connect、Source 和 Common。在 Connect 选项卡的 Project 域中,选择用作启动首选项的项目(用于查找源代码)。在 Connect 选项卡的 Host 域中,输入运行 Java 程序的远程主机的 IP 地址或域名。在 Connect 选项卡的 Port 域中,输入远程 VM 接受连接的端口。一般来说,该端口是在远程 VM 启动时指定的。当您想让调试器确定 Terminate 命令在远程会话中是否可用,可以选择 Allow termination of remote VM 选项。如果您希望能终止所连接的 VM,则选择该选项。现在当您选择 Debug 选项时,调试器将尝试按指定的地址和端口连接远程 VM,并在 Debug 视图中显示结果。如果启动器无法连接至指定位置上的 VM,将显示错误消息。一般来说,远程调试功能的可用性完全取决于远程主机上运行的 Java VM(虚拟机,Virtual Machine)。图 10 显示了远程调试会话的连接属性的设置。图 10. 设置远程调试会话的连接属性[myimg]upload/remote-debug.png[/myimg]调试其它语言Java 是 Eclipse 平台的主语言。然而,Eclipse 平台同时也是一个可支持许多其它语言的可扩展平台,而其中最重要的就是支持 C/C++(因为其流行性)。Eclipse 通过用 C/C++ 开发工具(CDT)支持 C/C++。请参阅参考资料以获取相关链接。CDT 通过调试 C/C++ 代码的功能扩展了标准的 Eclipse Debug 视图,同时 CDT Debug 视图允许您在工作台中管理 C/C++ 项目的调试。CDT 不包含其内部调试器,但它向必须可在本地使用的 GNU GDB 调试器提供了一个前端。下载并安装了 CDT 之后,只需切换到 Debug 视图,您就可以开始调试当前的 C/C++ 项目了(请参阅参考资料,以获取一篇介绍如何安装 CDT 的文章的链接)。这样您可以设置(并在执行过程中任何时候更改)代码中的断点,并且跟踪变量和寄存器。Eclipse 调试器显示您调试中各个目标的暂挂线程的堆栈帧。程序中的各个线程作为树的节点出现。它显示了运行中各目标的进程。请记住当 GNU GDB 调试一个带有调试符号链接的程序时,它最有效。这是在编译过程中由命令行参数 -g 来实现的。需要更多的信息请使用 -ggdb 开关,该参数包含有特定于 GNU GDB 的调试符号。如果您要调试 servlet,使用 Sysdeo Eclipse Tomcat Launcher。该插件使您能够管理 Tomcat 4.x/3.3 servlet 容器(通过创建和导入一个 Tomcat WAR 项目)。它同时在一个内部 Java Eclipse 调试器中注册一个 Tomcat 进程,这样您就能方便地调试 Tomcat 应用程序了。还有其它几个 Eclipse 插件,使我们能够对 servlet 使用内部 Eclipse 调试器,比如 Cactus 的 Eclipse 插件,Resin 插件和 X-Parrots ServletExec 插件。在下面的参考资料中可获得这些插件的链接。结束语Eclipse 平台提供了内置的 Java 调试器,该调试器具有标准调试功能,包括进行单步执行、设置断点和值、检查变量和值以及暂挂和恢复线程的能力。它还可以用于调试在远程机器上运行的应用程序。Eclipse 平台主要是一个 Java 开发环境,但是同一个 Eclipse 的 Debug 视图也可用于 C 和 C++ 编程语言。参考资料* 在 eclipse.org 加入 Eclipse 平台社区并下载该平台。Eclipse 平台源代码是通过普通公共许可证(Common Public License)进行许可的。您还能在 eclipse.org 找到 Eclipse 项目的术语词汇表及描述,以及技术文章和新闻组。Eclipse 平台白皮书详细描述了 Eclipse 的主要组件和功能。* 在 eclipse.org 下载 C/C++ 开发工具。* 下载 Sysdeo Eclipse Tomcat Launcher。* 以下是另外一些用于 servlet 的 Eclipse 插件的链接:o Cactus 的 Eclipse 插件o Resin 插件o X-Parrots ServletExec 插件* 查看 Eclipse 插件的分类注册表。* 以下文章可以帮助您开始学习用 Eclipse 进行调试:o 请参阅由 Greg Adams 和 Marc Erickson 撰写的 developerWorks 文章“Working the Eclipse Platform”,介绍了 Eclipse 平台及其工作方式。o 要着手使用 Eclipse 平台开发应用程序,请参阅由 David Gallardo 撰写的 developerWorks 文章“Getting started with the Eclipse Platform”。o 要学习 Eclipse 中的启动框架(Launching Framework),请参阅 eclipse.org 文章“We Have Lift-off: The Launching Framework in Eclipse”。o 要获取关于产品构建器和特性的更多信息,请参阅 eclipse.org 文章“Project Builders and Natures”。* 如果您是一名开发人员,且希望参与和实现不同 Eclipse 组件相关的讨论,请访问为该项目中各组件所创建的开发人员邮件列表。* 在 developerWorks 查找更多为 Eclipse 用户准备的参考资料。关于作者Paul Leszek,是 Studio B 的一名作者,同时也是专长于 Linux/Win/Mac OS 系统体系结构和管理的独立软件顾问。在许多操作系统、编程语言和网络协议,尤其是 Lotus Domino 和 DB2 方面,他都有经验。Paul 同时还是“LinuxWorld”系列文章的作者,以及波兰语版“PC World”的 Linux 专栏作家。