微信号:DataScientistUnion

介绍:数盟(数据科学家联盟)隶属于北京数盟科技有限公司,数盟致力于成为培养与发现“数据科学家”的黄埔军校. 数盟服务包括:线下活动、大数据培训. 官网:http://dataunion.org,合作:contact@dataunion.org

一个被知乎管理员和谐了的“知乎数据抓取程序”(.net、c#数据挖掘)

2015-01-22 21:50 数盟

【数盟致力于成为最卓越的数据科学社区,聚焦于大数据、分析挖掘、数据可视化领域,业务范围:线下活动、在线课程、猎头服务、项目对接】


作者:wuyidexinsheng


问:能利用爬虫技术做到哪些很酷很有趣很有用的事情?


准备学习python爬虫。各位大神都会用爬虫做哪些有趣的事情?今天突然想玩玩爬虫,就提了这个问题。跟着YouTube上的一个tutor写了个简单的程序,爬了一点豆瓣的数据。主要用到request和bs4(BeautifulSoup)模块。虽然简陋,毕竟是人生中的第一只爬虫啊……以示纪念,代码写在博客里了:我的第一只爬虫:爬取豆瓣读书。


没想到第一次答题居然登上了知乎互联网专栏的榜首了,也上了知乎首页,可惜被和谐了。

答:

监测她(他)的知乎,她关注、回答、赞了某个问题立马电脑和手机都弹出提示是不是很酷!先上两张图:

我是个.NET程序猿,有一天女神告诉我有一个很不错的社区叫“知乎”,我经常一过来就看到她在看知乎,但每次我想看她都看了啥啊,她就遮住屏幕不让我看。于是乎,在我心里埋下了一颗强烈的好奇心。知乎中搜了下她的名字,经过各种筛选知道了她的知乎空间。第一时间出现的想法是我要写个监测程序,她关注的所有问题我都想知道。


连续奋战5小时,至凌晨3点程序终于写出来了。主要HttpWebRequest加正则表达式来抓取数据,程序开机自动运行,数据库设在一台24小时开机的服务器上。多个监测客户端同时运行,公司的,家里的,远程服务器上的。每隔5分钟自动循环读取一次数据,如果检测到关注了新的问题,立马将它们发送至我的QQ邮箱和我的163邮箱,大家都知道QQ邮箱有提醒功能,一发过来,立马会弹出一个窗体告诉你有新的邮件。手机qq客户端也有,所以不管我是在上班的路上,还是在电脑旁,只要她有新动态我立马就知道了。是不是很酷?


监测程序已经运行三个多月了,收集了他二三百个关注的问题,我知道她一般都是吃中饭或者晚饭前喜欢看一下知乎,晚上睡前会看会,她睡得早但偶尔凌晨1点多还看知乎。她关注情感类的问题最多,而且那段时间我一直在追她,所以我能根据她关注的问题来推测她的一些想法,包括约会聊天时我可以聊一些她感兴趣的话题。所以实用性还是比较强的。


假如某一天凌晨1点,手机突然响了一下,发现她关注了某个问题。立马给她发一条短信过去,你是不是还没睡啊? 是啊,你怎么知道我没睡的? 凭感觉! 嘿嘿。 然后慢慢靠近她关注的那个话题去聊,这是不是会让她感觉到你特别懂她。好奇你居然知道她睡没睡,好奇你和她聊的话那么符合她的心声。


如此利器,有谁想要的吗? 赞超过一百,程序和源代码都放知乎上共享。

====21日9:37更新=====
没想到第一次答题就上榜了,好不开心,来来来,别停哈。怒上榜首,我开源12306抢票源码。我先赴约去开源我的知乎监测程序吧,一会把链接发过来。谢谢大家的赞!


====21日11:50更新=====


《关于隐私》
先说明下,很多人都说我这样做侵犯隐私?没有吧,这些数据都是公开的啊,她也知道我关注了她呀,但蛋疼的知乎客户端没有这么细致的提醒功能,我甚至在客户端上找不到我都关注了哪些人。知乎手机app开发团队弱爆了,这么强有力的需求居然没有满足? 而且,知乎!你怎么就没有订阅功能呢?邮箱订阅! 我提出来啦哈,采纳了给我大V可好?


《关于匿不匿名》
FK,男子汉大丈夫,匿啥名啊。女神知道了就知道了,又不是做什么伤天害理的事,敢作敢当!之所以写这么一个程序,也并不是完全的偷窥心理。对于一个程序员来说,写出一个新鲜的程序是能给程序员一种很大的乐趣的,这一般人难以理解,想当年在学校时,通宵写俄罗斯方块,白天上课不听课在那研究一个方块当按左键是什么样子什么逻辑,右键又怎样。这是非常有意思的事,编程其实是一个艺术活,好的框架和优质的代码让人一看就感觉特别享受。 所以我写这么个程序同样也是满足自己的一种乐趣。不必匿!


《关于女神追到没?》
追到啦,哈哈!好爽啊,9月份去骑了趟川藏线,路遇佛像及经轮,我就祈祷我要娶她做老婆。且出发前找牛逼大神算了一卦(中国易学协会副会长),说我10月份很有姻缘缘分。于是,我在世界三大冰川之一的-来古冰川的河床上找了一下午的石头,终于找到一颗天然红色心形的爱情石,回来后我就拿着石头跟她表白了,然后就成了!虽然她说我表白像检讨一样,但也很感人!
可惜,我们在一起没多久就分手了。原因一两句话说不完,总之不管以后怎样。都祝福她,虽然在一起不长时间,但那是很美的回忆。我会珍藏!

=====2015-1-21 上午11:16分更新=======

程序开源地址:

下载源程序:http://download.csdn.net/detail/wuyidexinsheng/8382253
程序博文地址:http://blog.csdn.net/wuyidexinsheng/article/details/42964707


步入正题,思路描述:

下来源程序后,先自己去建数据库改配置哈,否则程序运行不起来的。

  1. <span style=“font-size:10px;”><?xml version=“1.0”?>

  2. <configuration>

  3. <appSettings>

  4. <!–数据有两种存储方式,一种存储于本地程序目录下的Ids.txt,但那只存了问题ID,完整的数据存于oracle数据库中–>

  5. <add key=“connStr” value=“Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521)))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=DZZH)));User Id=xxxxx;Password=xxxx”/>

  6. <!– 设置监测循环时间:秒 –>

  7. <add key=“Interval” value=“600”/>

  8. <!–设置自动发送信息机器人邮箱–>

  9. <add key=“smtpAddress” value=“smtp.163.com”/>

  10. <!–用户名–>

  11. <add key=“sendEmailFrom” value=“xxxxxxxx@163.com”/>

  12. <!–密码–>

  13. <add key=“sendEmailFromPwd” value=“xxxxxxxxx”/>

  14. <!–接收邮箱地址–>

  15. <add key=“strMailAddressTo” value=“xxxxxxxx@163.com,xxxxxxxx@qq.com”/>

  16. <!–邮件名称抬头–>

  17. <add key=“EmailName” value=“zhApp-家里电脑”/>

  18. <!–END–>

  19. <!–监测地址–>

  20. <add key=“WatchingURL” value=“http://www.zhihu.com/people/wu-xin-sheng-7″/>

  21. </appSettings>

  22. <startup>

  23. <supportedRuntime version=“v4.0″ sku=“.NETFramework,Version=v4.0″/>

  24. </startup>

  25. </configuration></span>

第一步:通过Cinser.Common.HttpHelper.GetString(“www.zhihu.com/people/wu-xin-sheng-7”)读取给定的一个网址的后台代码:

结果如下图所示:

如我空间的源码中就有这么一段:

  1. <span class=“name”>伍新生</span><span class=“bio” title=“五颜六色的情感,我毕生的追求!”>五颜六色的情感,我毕生的追求!</span>

  2. </div>

  3. </div>

  4. <div class=“body clearfix”>

  5. <div class=“zm-profile-header-avatar-container self”>

  6. <img alt=“伍新生”

  7. src=“http://pic4.zhimg.com/94cc60166_l.jpg”

  8. class=“zm-profile-header-img zg-avatar-big zm-avatar-editor-preview”/>


  9. <span class=“zm-entry-head-avatar-edit-button”>修改头像</span>


第二步:通过调用方法private List<Question> GetQuestions(string source)截取关键数据。


原理很简单,所有知乎的问题都是如下的格式:<aclass=”question_linktarget=”_blankhref=”/question/27621722/answer/37636385“>能利用爬虫技术做到哪些很酷很有趣很有用的事情?</a>


接下来那就是字符串截取呗:调用Cinser.Common.StringPlus.SubString(source, “<a class=\”question_link\”, “</a>”)等依次截取问题的ID,名称等数据。

  1. private List<Question> GetQuestions(string source)

  2. {

  3. List<Question> questions = new List<Question>();

  4. string startStr = “<a class=\”question_link\””;

  5. if (source.IndexOf(startStr) != -1)

  6. {

  7. Question q = new Question();

  8. string content = Cinser.Common.StringPlus.SubString(source, startStr, “</a>”);

  9. q.Id = Cinser.Common.StringPlus.SubString(content, “question/”, “\””);

  10. q.Title = content.Substring(content.IndexOf(“>”) + 1);

  11. q.Time = DateTime.Now;

  12. questions.Add(q);

  13. source = source.Substring(source.IndexOf(startStr) + startStr.Length);

  14. questions.AddRange(GetQuestions(source));

  15. }

  16. return questions;

  17. }

第三步:将取到的问题写入数据库,并写入本地Ids.txt。

因为5分钟读取一次数据,肯定会读取到部分已经度过的数据咯。就是通过取到id然后看Ids.txt里面这个id是不是已经存在了,如果存在了就表示已经抓去过啦。


为什么问题都已经写到oracle数据库了还要往本地Ids.txt写一次呢,因为oracle数据库是部署在远程服务器上的啊。如果这台服务器突然出故障死机了,怎么办?程序还得要运行啊,所以程序往两个地方都写一次数据。如果oracle数据库不能访问,则通过读取和写入本地ids来记录问题。


数据抓取其实就这么简单。


如下是部分源码,也可以直接去下载源程序:http://download.csdn.net/detail/wuyidexinsheng/8382253


如下是程序主窗体源码:

  1. public partial class Form1 : Form

  2. {

  3. DataProvider dal;

  4. string watchingURL = string.Empty;

  5. int LoopCount = 0;

  6. public Form1()

  7. {

  8. InitializeComponent();

  9. dal = new DataProvider();

  10. dal.AddLog(“程序启动”);


  11. base.WindowState = FormWindowState.Minimized;

  12. base.Show();

  13. base.Hide();

  14. base.WindowState = FormWindowState.Normal;

  15. base.ShowInTaskbar = false;

  16. base.TopMost = false;

  17. base.MaximizeBox = false;

  18. base.MinimizeBox = false;

  19. base.ControlBox = false;


  20. //设置循环监测时间

  21. int interval = int.Parse(Cinser.Common.ConfigurationHelper.GetAppSettingsValue(“Interval”));

  22. timer1.Interval = interval * 1000;


  23. watchingURL = Cinser.Common.ConfigurationHelper.GetAppSettingsValue(“WatchingURL”);

  24. RunWhenStart(true, “zhApp.exe”, “\”” + Application.StartupPath + “\\zhApp.exe\” AutoRun”);

  25. Run();

  26. dal.AddLog(“程序初始化成功”);

  27. LoopCount += 1;

  28. }


  29. //设置程序开机自启动

  30. public void RunWhenStart(bool Started, string name, string path)

  31. {

  32. RegistryKey HKLM = Registry.CurrentUser;

  33. RegistryKey Run = HKLM.CreateSubKey(@“SOFTWARE\Microsoft\Windows\CurrentVersion\Run”);

  34. if (Started == true)

  35. {

  36. try

  37. {

  38. Run.SetValue(name, path);

  39. HKLM.Close();

  40. }

  41. catch(Exception ex)//没有权限会异常

  42. {

  43. throw ex;

  44. }

  45. }

  46. else

  47. {

  48. try

  49. {

  50. Run.DeleteValue(name);

  51. HKLM.Close();

  52. }

  53. catch (Exception ex)//没有权限会异常

  54. {

  55. throw ex;

  56. }

  57. }

  58. }


  59. /// <summary>

  60. /// 运行监测流程

  61. /// </summary>

  62. private void Run()

  63. {

  64. List<Question> questions = GetQuestions(Cinser.Common.HttpHelper.GetString(watchingURL));

  65. string ids = dal.GetExistIdsStr();

  66. for (int i = 0; i < questions.Count; i++)

  67. {

  68. if (ids.IndexOf(questions[i].Id) != -1)

  69. {

  70. questions.Remove(questions[i]);

  71. i–;

  72. }

  73. else

  74. {

  75. if (ids == string.Empty)

  76. ids = questions[i].Id;

  77. else

  78. ids += “,” + questions[i].Id;

  79. }

  80. }

  81. if (questions.Count > 0)

  82. {

  83. SendQuestions(questions);

  84. dal.WriteIdStrToTxt(ids);

  85. dal.Add(questions);

  86. dal.AddLog(string.Format(“获取了{0}条新数据。”, questions.Count));

  87. }

  88. }


  89. /// <summary>

  90. /// 从监测站点源数据中抓取问题

  91. /// </summary>

  92. private List<Question> GetQuestions(string source)

  93. {

  94. List<Question> questions = new List<Question>();

  95. string startStr = “<a class=\”question_link\””;

  96. if (source.IndexOf(startStr) != -1)

  97. {

  98. Question q = new Question();

  99. string content = Cinser.Common.StringPlus.SubString(source, startStr, “</a>”);

  100. q.Id = Cinser.Common.StringPlus.SubString(content, “question/”, “\””);

  101. q.Title = content.Substring(content.IndexOf(“>”) + 1);

  102. q.Time = DateTime.Now;

  103. questions.Add(q);

  104. source = source.Substring(source.IndexOf(startStr) + startStr.Length);

  105. questions.AddRange(GetQuestions(source));

  106. }

  107. return questions;

  108. }


  109. private bool SendQuestions(List<Question> questions)

  110. {

  111. bool bSuccess = true;

  112. List<string> strMailAddressTo = Cinser.Common.ConfigurationHelper.GetAppSettingsValue(“strMailAddressTo”).Split(‘,’).ToList();

  113. string smtpAddress = Cinser.Common.ConfigurationHelper.GetAppSettingsValue(“smtpAddress”);

  114. string sendEmailFrom = Cinser.Common.ConfigurationHelper.GetAppSettingsValue(“sendEmailFrom”);

  115. string sendEmailFromPwd = Cinser.Common.ConfigurationHelper.GetAppSettingsValue(“sendEmailFromPwd”);

  116. string emailName = Cinser.Common.ConfigurationHelper.GetAppSettingsValue(“EmailName”);

  117. //密码解密,开源的话就去掉这步吧,这样配置的时候直接配置明文密码就行。

  118. //sendEmailFromPwd = Cinser.Common.Security.DecryptDES(sendEmailFromPwd, “yuiophgf”);

  119. string msg = string.Empty;

  120. SendCompletedEventHandler s = new SendCompletedEventHandler(SendCompleted);

  121. string content = GetQustionsListStr(questions);

  122. content += “\n\n信息来源于:” + watchingURL;

  123. Cinser.Common.SmtpEmailSend.SendEmail(strMailAddressTo, emailName + DateTime.Now.ToString(), content, smtpAddress, 0x19, sendEmailFrom, sendEmailFromPwd, “163测试邮箱”, null, out msg, s);

  124. return bSuccess;

  125. }


  126. private void SendCompleted(object sender, AsyncCompletedEventArgs e)

  127. {

  128. }


  129. private string GetQustionsListStr(List<Question> questions)

  130. {

  131. string content = string.Empty;

  132. if (questions != null && questions.Count > 0)

  133. {

  134. content = string.Format(“名称:{0},url:{1}”, questions[0].Title, questions[0].Url);

  135. for (int i = 1; i < questions.Count; i++)

  136. {

  137. content += string.Format(“\n 名称:{0},url:{1}”, questions[i].Title, questions[i].Url);

  138. }

  139. }

  140. return content;

  141. }


  142. private void timer1_Tick(object sender, EventArgs e)

  143. {

  144. int interval = int.Parse(Cinser.Common.ConfigurationHelper.GetAppSettingsValue(“Interval”));

  145. timer1.Interval = interval * 1000;

  146. Run();

  147. dal.AddLog(string.Format(“程序循环次数:{0}”, LoopCount++));

  148. }

  149. }

知乎问题model:Question.cs

  1. public class Question

  2. {

  3. string id, title, url, type, remark;

  4. DateTime time;


  5. public string Remark

  6. {

  7. get { return remark; }

  8. set { remark = value; }

  9. }


  10. public DateTime Time

  11. {

  12. get { return time; }

  13. set { time = value; }

  14. }


  15. public string Type

  16. {

  17. get { return type; }

  18. set { type = value; }

  19. }


  20. public string Url

  21. {

  22. get

  23. {

  24. if (string.IsNullOrEmpty(url))

  25. {

  26. url = string.Format(“http://www.zhihu.com/question/{0}”, Id);

  27. }

  28. return url;

  29. }

  30. set { url = value; }

  31. }


  32. public string Title

  33. {

  34. get { return title; }

  35. set { title = value; }

  36. }


  37. public string Id

  38. {

  39. get { return id; }

  40. set { id = value; }

  41. }

  42. }

数据操作类DataProvider.cs

  1. /// <summary>

  2. /// 知乎问题数据表操作Provider

  3. /// </summary>

  4. public class DataProvider

  5. {

  6. private string connStr = string.Empty;

  7. Cinser.DBUtility.DAL.OracleDALCommon dal;

  8. string txtPath = “”;

  9. string logPath = “”;

  10. string debugPath = string.Empty;


  11. public string DebugPath

  12. {

  13. get

  14. {

  15. if (debugPath == string.Empty)

  16. debugPath = System.AppDomain.CurrentDomain.BaseDirectory;


  17. if (debugPath.EndsWith(“\\”) == false)

  18. {

  19. debugPath += “\\”;

  20. }

  21. return debugPath;

  22. }

  23. set { debugPath = value; }

  24. }


  25. public string LogPath

  26. {

  27. get

  28. {

  29. if (logPath == string.Empty)

  30. {

  31. logPath = DebugPath + “Log.txt”;

  32. }

  33. return logPath;

  34. }

  35. }


  36. public DataProvider()

  37. {

  38. dal = new Cinser.DBUtility.DAL.OracleDALCommon(this.ConnStr);

  39. }


  40. public string TxtPath

  41. {

  42. get

  43. {

  44. if (txtPath == string.Empty)

  45. {

  46. txtPath = DebugPath + “Ids.txt”;

  47. }

  48. return txtPath;

  49. }

  50. }


  51. public string ConnStr

  52. {

  53. get

  54. {

  55. if (connStr == string.Empty)

  56. {

  57. connStr = Cinser.Common.ConfigurationHelper.GetAppSettingsValue(“connStr”);

  58. }

  59. return connStr;

  60. }

  61. set { connStr = value; }

  62. }


  63. public bool CanConnectOracleServer

  64. {

  65. get

  66. {

  67. return dal.Open();

  68. }

  69. }


  70. /// <summary>

  71. /// 将抓取到的问题写入oracle数据库中

  72. /// </summary>

  73. /// <param name=”questions”></param>

  74. /// <returns></returns>

  75. public bool Add(List<Question> questions)

  76. {

  77. bool bReturn = false;

  78. try

  79. {

  80. for (int i = 0; i < questions.Count; i++)

  81. {

  82. dal.Add(“qustions”, questions[i]);

  83. }

  84. bReturn = true;

  85. }

  86. catch { }

  87. return bReturn;

  88. }


  89. public DataTable GetQustions(string sqlWhere = “1=1″)

  90. {

  91. try

  92. {

  93. DataTable dt = dal.GetDataList(“qustions”, sqlWhere);

  94. return dt;

  95. }

  96. catch

  97. {

  98. return null;

  99. }

  100. }


  101. public bool IsExist(string id)

  102. {

  103. try

  104. {

  105. string sqlWhere = “id='” + id + “‘”;

  106. DataTable dt = dal.GetDataList(“qustions”, sqlWhere);

  107. return dt.Rows.Count > 0;

  108. }

  109. catch

  110. {

  111. return false;

  112. }

  113. }


  114. /// <summary>

  115. /// 获取已经抓取过的问题ID字符串

  116. /// </summary>

  117. /// <returns></returns>

  118. public string GetExistIdsStr()

  119. {

  120. string ids = string.Empty;

  121. //如果能连上远程的oracle服务器则从oracle数据库中取ID字符串

  122. if (CanConnectOracleServer)

  123. {

  124. DataTable dt = GetQustions();

  125. if (dt != null && dt.Rows.Count > 0)

  126. {

  127. ids = dt.Rows[0][“id”].ToString();

  128. for (int i = 1; i < dt.Rows.Count; i++)

  129. {

  130. ids += “,” + dt.Rows[i][“id”].ToString();

  131. }

  132. }

  133. }

  134. else

  135. {//如果能连上远程的oracle服务器关机了,连不上则从本地Ids.txt中取ID字符串

  136. if (File.Exists(TxtPath))

  137. ids = File.ReadAllText(TxtPath);

  138. }

  139. return ids;

  140. }


  141. /// <summary>

  142. /// 将最新取到的问题记录至ids.txt中,以此标记这些问题为已读问题

  143. /// </summary>

  144. /// <param name=”ids”></param>

  145. public void WriteIdStrToTxt(string ids)

  146. {

  147. if (File.Exists(this.TxtPath) == false)

  148. File.Create(TxtPath);

  149. File.WriteAllText(this.TxtPath, ids);

  150. }


  151. /// <summary>

  152. /// 写程序log,方便错误追踪。

  153. /// </summary>

  154. /// <param name=”LogMsg”></param>

  155. public void AddLog(string LogMsg)

  156. {

  157. string logStr = string.Format(“{0}:{1}.\n”, DateTime.Now.ToString(), LogMsg);

  158. if (File.Exists(this.LogPath) == false)

  159. File.Create(LogPath);

  160. string[] logs = File.ReadAllLines(LogPath);

  161. if (logs.Length >= 520)

  162. File.WriteAllText(LogPath, logStr);

  163. else

  164. {

  165. StreamWriter sw = File.AppendText(LogPath);

  166. sw.WriteLine(logStr);

  167. sw.Close();

  168. }

  169. }

  170. }



文章出处:http://blog.csdn.net/wuyidexinsheng/article/details/42964707

—————————————————

数盟网站:www.dataunion.org

数盟微博:@数盟社区

数盟微信:DataScientistUnion

数盟【大数据群】272089418

数盟【数据可视化群】 179287077

数盟【数据分析群】 110875722

—————————————————

点击阅读原文,查看原文代码~

 
数盟 更多文章 【数盟4.13大数据商业沙龙(北京)】赏美景,聚天同~ 倒计时2天! 【数盟4.13大数据商业沙龙(北京)】赏美景,聚天同~ 明天见! 【福利】当eXadoop遇到硬蛋比赛,激情四射~ 【数盟福利】价值418元的免费直通硬蛋比赛门票等你来拿~ 数盟4.26数据分析聚会(北京)【免费】
猜您喜欢 工信部大数据推进开展试点计划 【人人都要学算法】网络流算法远比你想的要好玩 如何让学到的运维知识系统化? 给女朋友的 iOS 开发教程 8 UITableView 你如果这样表白,TA一定会答应你的!