梦-开始的地方

Frederick

博客园 首页 新随笔 联系 订阅 管理
  26 Posts :: 0 Stories :: 143 Comments :: 0 Trackbacks

2010年11月21日 #

这是一个C#语言编写的多线程网页自动采集程序。下面展示了主要类的代码。完整代码请点此下载

/**
软件工程过程实践:
--------------------------------------------
用例->数据模型->描述系统功能的接口->实际编码->测试->交付
文字  逻辑设计   顺序图(通讯图)                     定制
图    sql实现    类图(接口方法)                     配置
      c#实现



用例UC1: 网页采集
----------------------------------------------
范围: WSE应用
级别: 用户目标
主要参与者: 采集员
涉众及其关注点:
  ——采集员: 希望能够增加,删除监控URL,启动,停止监控URL,指定监控类型,查看监控URL的列表
  ——站长: 希望采集活动不要影响正常用户访问
前置条件: 采集员必须经过确认和认证
成功保证(或后置条件): 存储采集数据,确保没有重复的URL
主成功场景(或基本流程):
  1. 采集员增加新的监控URL,设定采集方式(本页,历遍本页,历遍网站),设定采集速度
  2. 采集员启动采集
  3. 采集员停止采集(如果有必要)
  4. 采集员删除某监控URL(如果有必要)
  5. 采集员浏览监控URL列表(如果有必要)



PageInfo数据模型
----------------------------
标识符        | id
创建时间      | createdTime
修改时间      | modifiedTime
创建人        | createdUser
修改人        | modifiedUser
              |
网址          | URL
网址128位MD5  | UrlMD5
IP地址        | IP
采集内容      | content
页面类型      | type
              |


采集流程图
-------------------------------

 初始化:载入已经采集的UrlMD5
|-------------------------------------------------------------------------------------------------------|
|Spider类                                                                                               |
|         |---------------------------------------------------------------------|    |----------------| |
|  |--|   |采集通道(线程)                         ------------------------------|--->| 已采集UrlMD5池 | |
|  |种|   |      |---------|    |--------|    |---|------|     |--------|       |    |----------------| |
|  |子|---|---|->| URL队列 |--->| 采集器 |--->|   |  |   |---->| 分析器 |---|   |                       |
|  |队|   |   |  |---------|    |--------|    |------|---|     |--------|   |   |                       |
|  |列|   |   |                                      |                      |   |                       |
|  |--|   |   |--------------------------------------|----------------------|   |    |----------------| |
|    |---<|                                          ---------------------------|--->| 已采集内容队列 | |
|    |    |---------------------------------------------------------------------|    |--------|-------| |
|    |                                                                                        |         |
|    |    |---------|                                                                         |         |
|    |---<|存储线程 |<------------------------------------------------------------------------|         |
|    |    |---------|                                                                                   |
|    |                                                                                                  |
|    |    |---------|                                                                                   |
|    |--->|日志线程 |                                                                                   |
|         |---------|                                                                                   |
|                                                                                                       |
|-------------------------------------------------------------------------------------------------------|


类设计
-------------------------------
控制器类
  SpiderHandler -- 控制台入口

采集核心类
  Spider     核心
  PageInfo   基础--数据结构
  Gatherer   基础--网页采集器
  Analyser   基础--url分析器

外部接口
  IStorage   数据存储接口
  ISpiderUI  用户界面
  ILogger    日志接口
*/

//==================================================================
//
//    该软件是一个由C#编写的基于控制台的多线程网页自动采集程序。
//    又称之为蜘蛛,机器人,爬行器等。
//
//    Copyright(C) 2009 themz.cn All rights reserved
//    author:   xml
//    email:    191081370@qq.com
//    blog:     http://programmingcanruinyourlife.themz.cn/
//    since:    .net2.0
//    version:  1.0
//    created:  2009-08-06
//    modified: 2009-10-10
//
//  版权与免责声明:本软件所有权归原作者,用户可自由免费使用于任何非商业环境。
//                  如果转载本文代码请不要删除这段版权声明。
//                  如果由于使用本软件而造成自己或他人的任何损失,均与本软件作者无关。
//                  特此声明!
//
//==================================================================
//    简单使用帮助:
//         1. 将下面代码保存到一个.cs后缀的文件中
//         2. 用.net2.0的编译环境编译成一个exe文件后,双击打开
//         3. 用 addSeeds命令添加采集种子, 例如: addSeeds http://url/
//         4. 用 start 命令开始采集
//         5. 反复使用 getContents 命令查看已采集到的内容
//         6. pause 命令可暂停采集, start 命令继续
//         7. stop 命令停止采集
//         8. exit 命令退出本软件
//
//

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;

//using System.Configuration;
//using System.Diagnostics;
//[Serializable()]
namespace My.WSE.Spider
{
  #region 线程模式接口
    /**
    线程类模式

    接口  参数

    队列  属性
    线程  属性

    入队  方法
    出队  方法

    增加/启动  方法
    暂停       方法
    停止       方法
    */
    public interface IThread
    {
        //T Queue{ get; }
        //List<Thread> Threads{ get; }
        //
        //void Enqueue( T t );
        //T Dequeue();

        Thread AddThread();
        void RemoveThread();
        void RequestThreadPause();
        void RequestThreadPause( bool pauseOrContinue );
        void RequestThreadStop();
    }
  #endregion

  #region  外部接口
    // 采集接口
    public interface IGatherer
    {
        void Download( ref PageInfo info,string contentType,int timeout );
        void Download( ref PageInfo info,int timeout );
    }

    // 存储接口
    public interface IStorage
    {
        List<string> GetIndexeds();                      //取得所有已下载的URL的MD5值

        List<SeedInfo> GetSeeds();
        int AddSeed( SeedInfo info );
        void RemoveSeed( SeedInfo info );

        void SaveContents( List<PageInfo> info );        //保存采集到的内容
    }

    // 日志接口
    public interface ILogger
    {
        void Write( string content );
        string Read( string filename );

        string ToString( Exception ex );
    }

  #endregion

  #region 异常类
    public class ContentTypeException : Exception
    {
        public ContentTypeException( string message ) : base( message ){}
    }

    public class ContentSizeException : Exception
    {
        public ContentSizeException( string message ) : base( message ){}
    }

    public class NotOnlyException : Exception
    {
        public NotOnlyException( string message ) : base( message ){}
    }

    public class KeyHasExistsException : Exception
    {
        public KeyHasExistsException( string message ) : base( message ){}
    }
  #endregion

  #region PageInfo队列
    public class PageQueue
    {
        // 构造函数1
        public PageQueue()
        {
            _queue = new LinkedList<string>();
        }
        // 构造函数2
        public PageQueue( ref LinkedList<string> queue ) : this()
        {
            if( null != queue ){
                _queue = queue;
            }
        }


      #region 队列方法
        public int Count
        {
            get{  return _queue.Count;  }
        }
        public bool Contains( PageInfo info )
        {
            return _queue.Contains( info.UrlMD5 );
        }
        public void Enqueue( PageInfo info )   //等同于AddLast
        {
            AddLast( info );
        }
        public PageInfo Dequeue()              //等同于RemoveFirst
        {
            return RemoveFirst();
        }

        public void AddFirst( PageInfo info )
        {
            lock( _queue ){
                _queue.AddFirst( info.UrlMD5 );
                AddData( info );
                Monitor.Pulse( _queue );
            }
        }
        public void AddLast( PageInfo info )
        {
            lock( _queue ){
                _queue.AddLast( info.UrlMD5 );
                AddData( info );
                Monitor.Pulse( _queue );
            }
        }
        public PageInfo RemoveFirst()
        {
            PageInfo info = null;
            lock( _queue ){
                LinkedListNode<string> node = _queue.First;
                if( null == node ){
                    Monitor.Wait( _queue );
                    node = _queue.First;
                }

                string key = node.Value;
                _queue.RemoveFirst();
                info = GetData(key);
                RemoveData(key);    // 释放内存中的数据
            }
            return info;
        }
        public PageInfo RemoveLast()
        {
            PageInfo info = null;
            lock( _queue ){
                LinkedListNode<string> node = _queue.First;
                if( null == node ){
                    Monitor.Wait( _queue );
                }
                else{
                    string key = node.Value;
                    _queue.RemoveFirst();
                    info = GetData(key);
                    RemoveData(key);    // 释放内存中的数据
                }
            }
            return info;
        }
        public PageInfo Remove( PageInfo info )
        {
            lock( _queue ){
                if( _queue.Remove(info.UrlMD5) ){
                    info = GetData(info.UrlMD5);
                    RemoveData(info.UrlMD5);    // 释放内存中的数据
                }
                else{
                    info = null;
                }
            }
            return info;
        }

        public Dictionary<string,PageInfo> ToDictionary()
        {
            Dictionary<string,PageInfo> dict = new Dictionary<string,PageInfo>();

            lock( _queue ){
                LinkedListNode<string> node = _queue.First;
                while( null != node ){
                    dict[node.Value] = GetData(node.Value);
                    node = node.Next;
                }
            }
            return dict;
        }
      #endregion

      #region 词典方法
        public PageInfo GetData( string key )
        {
            lock( _s_pages ){
                if( _s_pages.ContainsKey(key) ){
                    return _s_pages[key];
                }else{
                    _log.Enqueue( string.Format( "wse.spider.cs GetData,Dictionary键{0}没有找到",key) );
                    return null;
                }
            }
        }
        public void AddData( PageInfo info )
        {
            lock( _s_pages ){
                _s_pages[info.UrlMD5] = info;
            }
        }
        public void RemoveData( string key )
        {
            lock( _s_pages ){
                if( _s_pages.ContainsKey(key) ){
                    _s_pages.Remove(key);
                }
            }
        }
        public bool ContainsData( PageInfo info )
        {
            return _s_pages.ContainsKey(info.UrlMD5);
        }
      #endregion

      #region Private Members

        private LinkedList<string> _queue = null;
        private static Dictionary<string,PageInfo> _s_pages = new Dictionary<string,PageInfo>();

        private EventLogger _log = new EventLogger();
      #endregion
    }
  #endregion

  #region 采集线程类
    public class PageGatherer : IThread
    {
      #region 构造函数
        // 构造函数1
        public PageGatherer(){}

        // 构造函数2
        public PageGatherer( IGatherer gather )
        {
            _log = new EventLogger();
            _store = new PageStorage();

            _gather = gather;
            _queue = new PageQueue();        // 每个队列可以
            _threads = new List<Thread>();   // 有多个线程

            _shouldPause = new ManualResetEvent(true);
            _shouldStop = false;
        }
      #endregion

      #region Public Property
        // 静态成员公开
        public Dictionary<string,string> IndexedPool
        {
            get{ return _s_indexedPool; }
        }
        public PageQueue SeedQueue
        {
            get{  return _s_seedQueue;  }
        }

        // 当前采集队列
        public PageQueue Queue
        {
            get{  return _queue;  }
        }
        public List<Thread> Threads
        {
            get{  return _threads;  }
        }
        // 线程总数
        public int ThreadCount
        {
            get{  return _threadCount;  }
        }
      #endregion

      #region 线程方法(Thread Method)
        // 增加线程
        public Thread AddThread()
        {
            Thread t = new Thread( new ThreadStart(ThreadRun) );
            t.IsBackground = true;
            t.Start();
            _threads.Add(t);
            _threadCount++;
            return t;
        }
        // 减少线程
        public void RemoveThread()
        {
            // 尚未实现
        }
        // 请求线程暂停
        public void RequestThreadPause()
        {

        }
        // 请求线程继续
        public void RequestThreadPause( bool pauseOrContinue )
        {
            if( !pauseOrContinue ){
                _shouldPause.Set();
            }else{
                _shouldPause.Reset();
            }
        }
        // 请求线程停止
        public void RequestThreadStop()
        {
            _shouldStop = true;
        }
      #endregion

      #region Private Methods
        // 采集线程方法
        private void ThreadRun()
        {
            PageInfo info = null;

            // 循环: URL->下载->存储->分析->|URL->下载....
            while( !_shouldStop )
            {
                _shouldPause.WaitOne();              // 是否暂停
                if( _queue.Count < 1 ){
                    _queue.Enqueue( _s_seedQueue.Dequeue() );     // 自动取得种子
                }

                info = _queue.Dequeue();
                if( null == info ){  continue;  }

                //1 下载
                string url = info.URL;
                try{
                    _gather.Download(ref info,"text/html",90000);
                }
                catch( Exception ex ){
                    _log.Enqueue( info.URL + " " + ex.ToString() );
                    continue;
                }

                //2 把当前url加入_s_indexedPool
                AddIndexed( info.UrlMD5 );

                //3 保存:加入_dataPool
                _store.Queue.Enqueue( info );

                //4 分析:加入下载队列queue
                AnalyzeToQueue( info, ref _queue );
            }
        }
        // 分析出页面中的url,并把它们加进队列中
        private void AnalyzeToQueue( PageInfo info, ref PageQueue queue )
        {
            PageQueue _queue = queue;

            List<string[]> urls = Analyzer.ParseToURLs(info);
            PageInfo newInfo = null;

            for( int i=0,len=urls.Count; i<len; i++ ){
                newInfo = new PageInfo( urls[i][0],info.SeedID );

                if( !_queue.ContainsData(newInfo) && !_s_indexedPool.ContainsKey(newInfo.UrlMD5) ){
                    newInfo.Title = urls[i][1];
                    newInfo.Referer = info.URL;

                    _queue.Enqueue( newInfo );
                }
            }
        }
        // 加入已采集队列
        private void AddIndexed( string urlMD5 )
        {
            lock( _s_indexedPool ){
                if( !_s_indexedPool.ContainsKey(urlMD5) ){
                    _s_indexedPool.Add( urlMD5, null );
                }
            }
        }
      #endregion

      #region Private Members
        private EventLogger _log = null;
        private PageStorage  _store = null;

        private IGatherer _gather = null;       // 接口
        private PageQueue _queue;               // 每个队列可以
        private List<Thread> _threads;          // 有多个线程

        private ManualResetEvent _shouldPause;  // 暂停
        private bool _shouldStop;               // 停止

        private static Dictionary<string,string> _s_indexedPool = new Dictionary<string,string>();      // 已采集的URL
        private static PageQueue _s_seedQueue = new PageQueue();   // 种子队列

        private static int _threadCount = 0;     // 运行的线程的总数
      #endregion
    }
  #endregion

  #region 存储线程类
    public class PageStorage  : IThread
    {
      #region 构造函数
        // 构造函数1
        public PageStorage(){}

        // 构造函数2
        public PageStorage( IStorage store )
        {
            _log = new EventLogger();

            _store = store;
            _shouldStop = false;
        }
      #endregion

      #region Public Property
        // 对列对象
        public PageQueue Queue
        {
            get{ return _s_queue;  }
        }
        // 线程对象集合
        public List<Thread> Threads
        {
            get{ return _threads;  }
        }
      #endregion

      #region 线程方法(Thread Method)
        // 增加线程
        public Thread AddThread()
        {
            Thread t = new Thread( new ThreadStart(ThreadRun) );
            t.IsBackground = true;
            t.Start();
            return t;
        }
        // 减少线程
        public void RemoveThread()
        {
            // 尚未实现
        }
        // 请求线程暂停
        public void RequestThreadPause()
        {
            // 尚未实现
        }
        // 请求线程继续
        public void RequestThreadPause( bool pauseOrContinue )
        {
            // 尚未实现
        }
        // 请求线程停止
        public void RequestThreadStop()
        {
            _shouldStop = true;
        }
      #endregion

      #region Private Methods
        // 线程方法
        private void ThreadRun()
        {
            if( null == _store ){ return;  }

            int count = 10;
            List<PageInfo> infos = null;

            while( !_shouldStop )
            {
                infos = DequeueSome( count );
                try{
                    _store.SaveContents( infos );
                }
                catch( Exception ex ){
                    _log.Enqueue( ex.ToString() );
                }
            }
        }
        // 队列方法
        private List<PageInfo> DequeueSome( int count )
        {
            List<PageInfo> infos = new List<PageInfo>();

            for( int i=0; i<count; i++ )  // 按每10条记录一组进行存储
            {
                infos.Add( _s_queue.Dequeue() );
            }

            return infos;
        }
      #endregion

      #region Private Members
        private EventLogger _log;    //日志

        private IStorage _store;                              //接口
        private static PageQueue _s_queue = new PageQueue();  //队列
        private List<Thread> _threads = new List<Thread>();   //线程

        private bool _shouldStop;
      #endregion
    }
  #endregion

  #region 日志线程类
    public class EventLogger : IThread
    {
        // 构造函数1
        public EventLogger(){}

        // 构造函数2
        public EventLogger( ILogger logger )
        {
            _logger = logger;
            _shouldStop = false;
            _selfCheckInterval = 300000;    // 5分钟
        }
      #region Public Properties
        public Queue<string> Queue
        {
            get{  return _s_queue;  }
        }
        public List<Thread> Threads
        {
            get{  return _threads;  }
        }
      #endregion

      #region 队列方法(Queue Method)
        public void Enqueue( string s )
        {
            lock( _s_queue ){
                _s_queue.Enqueue( s );
                Monitor.Pulse( _s_queue );
            }
        }
        public string Dequeue()
        {
            lock( _s_queue )
            {
                if( 1 > _s_queue.Count ){
                    Monitor.Wait( _s_queue );
                }
                return _s_queue.Dequeue();
            }
        }
      #endregion

      #region 线程方法(Thread Method)
        //
        public Thread AddThread()
        {
            Thread t = new Thread( new ThreadStart(ThreadRun) );
            t.IsBackground = true;
            t.Start();
            _threads.Add(t);
            return t;
        }
        // 减少线程
        public void RemoveThread()
        {
            // 尚未实现
        }
        // 请求线程暂停
        public void RequestThreadPause()
        {
            // 尚未实现
        }
        // 请求线程继续
        public void RequestThreadPause( bool pauseOrContinue )
        {
            // 尚未实现
        }
        // 请求线程停止
        public void RequestThreadStop()
        {
            _shouldStop = true;
        }
        // 增加自检线程
        public void AddSelfCheckThread()
        {
            if( false == _isSelfCheckRun ){
                Thread t = new Thread( new ThreadStart(SelfCheck) );
                t.IsBackground = true;
                t.Start();
                _isSelfCheckRun = true;
            }
        }
      #endregion

      #region Private Methods
        // 日志主线程函数
        private void ThreadRun()
        {
            if( null == _logger ){ return;  }

            while( !_shouldStop )
            {
                try{
                    _logger.Write( Dequeue() );
                }
                catch( Exception ex ){
                    Console.WriteLine( string.Format( "警告:日志写入发生错误{0}",ex.ToString() ) );
                }
            }
        }
        // 日志自检子线程函数
        private void SelfCheck()
        {
            if( null == _logger ){ return;  }

            while( !_shouldStop )
            {
                try{
                    _logger.Write( "日志自检完成" );
                    Thread.Sleep( _selfCheckInterval );
                }
                catch( Exception ex ){
                    Console.WriteLine( string.Format( "警告:日志自检发生错误{0}",ex.ToString() ) );
                }
            }
        }
      #endregion

      #region Private Members
        private ILogger _logger = null;                               // 接口
        private static Queue<string> _s_queue = new Queue<string>();  // 一些标志性事件(异常或成功)
        private List<Thread> _threads = new List<Thread>();           // 一个队列可以有多个线程

        private bool _shouldStop;

        private int _selfCheckInterval;   // 日志模块自检间隔
        private static bool _isSelfCheckRun = false;
      #endregion
    }
  #endregion

} // end namespace My.WSE
posted @ 2010-11-21 20:56 Frederick 阅读(382) 评论(0) 编辑

2009年7月13日 #

要求:
1.2年以上的项目开发经验
2.有大型项目开发经验。
3.熟悉.NET平台框架,精通VB.Net语言 
4.熟悉面向对象的系统分析和设计
5.有良好的项目管理经验,能有效地管理开发流程和进度控制
6.熟悉各类软件开发文档的编写
7.会日语优先

 工作地点:天津

有意者请联系 02283788288 李小姐

posted @ 2009-07-13 18:27 Frederick 阅读(47) 评论(0) 编辑

2006年4月9日 #

编者的话:
    大家好:首先要向大家道歉,本期杂志的出版延期至了4月份,首先责任在我,因为本人近期比较忙,下半月又去上海出差,导致期刊延误,再次向大家道歉。
    其次,本月只收到了Vitoria和yiyegufan两位编辑的投稿,在这里要向其他编辑提出批评,希望大家能踊跃投稿,期刊团队本身已经尽最大努力简单编辑人员的投稿工作量,希望大家能团建合作,共同建立好我们自己的网络杂志。

--------------------------------------------------------------------------------

招聘通知:
    博客园期刊团队继续招聘编辑人员,欢迎有兴趣有毅力的网友报名,报名地址:http://magazine.cnblogs.com/board.aspx?boardid=6 同时也欢迎大家点击这里提出对期刊的意见,您的每条建议都是我们的宝贵财富,我们都会一一回答。本期杂志因为采用1680*1050的分辨率编辑,可能会在排版上出现问题,希望大家能理解,并请有能力的朋友提供技术上帮助。

下载地址

http://magazine.cnblogs.com/topic.aspx?topicid=13
posted @ 2006-04-09 21:14 Frederick 阅读(808) 评论(6) 编辑

2006年3月20日 #

很抱歉通知大家一下,3月份的期刊因为本人最近工作非常忙,为了保证质量所以要延期几天,我相信大家宁愿看到高质量的期刊也不会喜欢赶工出来的作品。

另外感谢听棠兄的SPL,确实使工作进度快了很多。
posted @ 2006-03-20 00:50 Frederick 阅读(681) 评论(5) 编辑

2006年2月21日 #



          2006年博客园期刊的发布会放在 http://magazine.cnblogs.com/index.aspx  中。同时在博客园首页公布。

          欢迎大家对期刊的工作提出宝贵的意见和建议(http://magazine.cnblogs.com/board.aspx?boardid=5

          同时也欢迎大家加入期刊团队 http://magazine.cnblogs.com/topic.aspx?topicid=2
         新的一年中博客园期刊团队的编辑投稿方式也有所改变,为了减轻各位编辑的工作量采取文章推荐的方式,即将文章的URL和推荐理由发表到投稿区即可。


现团队成员列表

  • 1. Frederick (rss) (16)
  • 2. 提示符 (rss) (7)
  • 3. 青青子衿,悠悠我心 (rss) (5)
  • 4. shalala (rss) (3)
  • 5. 萝卜皮 (rss) (2)
  • 6. 网管人生 (rss) (1)
  • 7. 梵星 (rss) (1)
  • 8. Toddy (rss) (1)
  • 9. 杨利 (rss) (0)
  • 10. YS (rss) (0)
  • 11. 烙饼 (rss) (0) 

  •        

    posted @ 2006-02-21 10:57 Frederick 阅读(825) 评论(3) 编辑

    2006年2月16日 #

    摘要: 下载地址:http://magazine.cnblogs.com/topic.aspx?topicid=1可能需要注册后才能下载,这个程序还没找到游客权限的地方,给大家带来不便,在此道歉了。阅读全文
    posted @ 2006-02-16 14:53 Frederick 阅读(1157) 评论(18) 编辑

    2006年1月1日 #

    摘要: 2006年度博客圆期刊1月份第一期感谢:1. Frederick 2. 提示符 3. 维多利亚的飞翔 4. 米小波 5. shalala 6. 梵星 7. Toddy 8. 网管人生 9. xiaotao 10. 杨利 11. YS 12. didasoft 13. 萝卜皮 14. 烙饼 的辛勤工作(排名不分先后)下载地址(已经修正) :http://magazine.cnblogs.com/up...阅读全文
    posted @ 2006-01-01 20:05 Frederick 阅读(2020) 评论(18) 编辑

    2005年12月21日 #

    摘要: 下载地址:http://frederick-liu.cnblogs.com/Files/frederick-liu/博客园期刊第三期.rar感谢各位编辑的辛勤工作。阅读全文
    posted @ 2005-12-21 09:35 Frederick 阅读(1046) 评论(7) 编辑

    2005年12月20日 #

    摘要: 请大家提出建议。/Files/frederick-liu/博客园期刊第三期.rar阅读全文
    posted @ 2005-12-20 08:55 Frederick 阅读(116) 评论(1) 编辑

    2005年12月18日 #

    摘要: 访问地址:http://magazine.cnblogs.com/mag/n20051201/index.html让我们感谢为此付出劳动的编辑们。12月第二期期刊预览[05-11-25]Windows Form 2.0 自动完成[Frederick].doc [05-11-25]模仿AppSettings进行web.config的自定义节读取[简单实用型][yiyegufan].doc [05-1...阅读全文
    posted @ 2005-12-18 19:48 Frederick 阅读(1055) 评论(8) 编辑