京东自营笔记本的爬虫项目C#
==================
目录
简介
本文只提供了一种简单的爬虫思路,因为初学C#和正则,语法不那么规范,甚至会有错误,请大佬们发现以后以后提醒一下我。
本程序用到了System.Data.SQLite和Newtonsoft.Json,前者提供SQLite的操作支持,后者用于处理京东接口返回的Json数据。这两个包在VS包管理中搜索安装即可。
(工具 -> NuGet 包管理器 -> 管理解决方案的 NuGet 程序包)对于爬下来的数据,我选择用数据库来存储,因为方便操作,易于分析,比如每隔一天爬取一次比较价格,当天数过多以后数据量会明显上升,此时数据库在时间上明显优于XML和Json。
XML和数据库的性能比较可以看这位大佬的博客
数据库和XML数据读取性能比较
大体思路
因为京东基本没有什么反爬虫,这里不需要用一些惊天地泣鬼神的接口。首先通过一个入口url获取其源代码然后从中筛选出所有自营商品的url列表,然后再获取每个商品页面的源码然后通过正则表达式匹配获取所需要的数据。(获取商品价格和评论数需要接口)
商品价格:http://p.3.cn/prices/mgets?type=1&skuIds=J_JID (JID为商品链接中的一串数字,如下图)
商品评论情况:https://club.jd.com/comment/productCommentSummaries.action?referenceIds=JID
替换JID就可以获得所需要的信息,返回数据为JSON格式。
PS:这两个接口经常访问会被京东ban掉IP,返回 {'error':'pdos_captcha'}
,建议做一个代理地址池或者爬慢一点,被ban掉过一段时间就会恢复。
一个获取网页代码的类
因为京东基本没有什么反爬虫技术,这里我们就用最低级的方法,通过 HttpWebRequest 来请求网页的源代码,然后再通过正则表达式来匹配我们所需要的数据。
首先我们先新建一个TestCrawler类来通过Url爬取网页源代码
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace JDLaptop_Spider.Codes
{
public class TestCrawler
{
public TestCrawler() { }
}
}
由于爬虫需要开多个线程,所以我们需要先定义三个事件(启动,完成,出错)
public class TestCrawler
{
public event EventHandler<OnStartEventArgs> OnStart;//爬虫启动事件
public event EventHandler<OnCompletedEventArgs> OnCompleted;//爬虫结束事件
public event EventHandler<Exception> OnError;//爬虫出错事件
public TestCrawler() { }
}
/****************爬虫启动事件*********************/
public class OnStartEventArgs
{
public Uri Uri { get; set; }
public OnStartEventArgs(Uri uri)
{
this.Uri = uri;
}
}
/****************爬虫完成事件*********************/
public class OnCompletedEventArgs
{
public Uri Uri { get; private set; } //爬虫地址
public String PageSource { get; private set; } //页面源码
public OnCompletedEventArgs(Uri uri, string pageSource)
{
this.Uri = uri;
this.PageSource = pageSource;
}
}
基本工作准备完成,接下来就开始写Start方法
其中 关键在于通过 request.UserAgent 方法来伪装成浏览器还有 通过 Encoding.Default 来 指定 PageSource 的编码(有时用UTF-8会乱码Default不会,而有时Default会乱码,所以这里要多尝试几次)。
代码中注释齐全,基本上学过C#的都可以看懂
public async Task<String> Start(Uri uri)
{
return await Task.Run(() =>
{
var pageSource = String.Empty;
try
{
if (this.OnStart != null) this.OnStart(this, new OnStartEventArgs(uri));
var request = (HttpWebRequest)WebRequest.Create(uri);
request.Accept = "*/*";
request.ContentType = "application/x-www-form-urlencoded";
request.AllowAutoRedirect = false;
request.UserAgent = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36"; //伪装成浏览器
request.Timeout = 5000; //定义超时时间
request.KeepAlive = true; //保持链接
request.Method = "GET";
request.ServicePoint.ConnectionLimit = int.MaxValue;//定义最大连接数
var respone = (HttpWebResponse)request.GetResponse();
var stream = respone.GetResponseStream();
var reader = new StreamReader(stream, Encoding.Default);
pageSource = reader.ReadToEnd();
reader.Close();
stream.Close();
request.Abort();
respone.Close();
if (this.OnCompleted != null)
{
this.OnCompleted(this, new OnCompletedEventArgs(uri,pageSource));
}
}
catch (Exception ex)
{
if (this.OnError != null) this.OnError(this, ex);
}
return pageSource;
}
);
}
京东官网代码的分析
首先我们打开京东官网的笔记本页面,并且勾选京东物流(京东自营产品基本全用京东物流,也有少部分第三方店用京东物流)笔记本 电脑整机 电脑、办公【行情 价格 评价 图片】- 京东
从图中可以看到,每一个商品被京东用一个无序列表”< li>”来存放,再进一步展开代码,可以看到< a>标签,标签内部就是需要的商品详情页的URL。
只有第一页还不够,我们还需要进入下一页,继续再这个页面检查元素,就可以发现下一页所对应的< a>标签的格式
至此,我们就可以写出正则表达式来匹配我们所需要的链接。
因为没学过正则,所以这里的表达式可能显得很白痴,如果有大佬看到这篇博客,希望大佬可以留下更好的写法。
获取商品详情链接列表的方法
public List<Uri> getUriList()
{
List<Uri> urilist = new List<Uri>();
Uri u = null;
String uri;
String regular = "\\ba\\b\\s+\\S+\\s*(href){1}=\"\\S+(item.jd.com){1}\\S{1}[0-9]{7}(.html){1}\\S+\""; //匹配商品详情页 正则
foreach (Match i in Regex.Matches(PageSource, regular))
{
uri = i.Value;
uri = uri.Replace("a target=\"_blank\" href=\"//","https://");
uri = uri.Replace("\"", "");
u = new Uri(uri);
urilist.Add(u);
}
return urilist;
}
获取下一页链接的方法
public Uri getNextPage()
{
Uri u = null;
String regular = "\\ba\\b\\s+(class){1}=\"(pn-next){1}\"\\s+(href){1}=\"\\S+\""; //匹配下一页 正则
Match m = Regex.Match(PageSource, regular);
String uri = m.Value;
uri = uri.Replace("a class=\"pn-next\" href=\"/", "https://list.jd.com/");
uri = uri.Replace("\"", "");
if (uri == null || uri == "") return null;
else
{
u = new Uri(uri);
return u;
}
}
爬虫的控制类
首先新建一个TestCrawControl类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JDLaptop_Spider.Codes
{
class TestCrawControl
{
public TestCrawControl() { }
}
}
接下来,构造一个调用TestCrawer获取url列表的方法
private void getList(Uri starUri)
{
Uri uri = starUri;
TestCrawler tc = new TestCrawler(); //新建一个testcrawler对象
tc.OnCompleted += (s, ev) => //绑定完成事件触发后的回调方法
{
/*这里类的名字不规范,getUriList类实际上封装了获取下一页链接和获取商品详情链接列表两个方法*/
GetUriList getUriList = new GetUriList(ev.PageSource); //获取详情列表的封装的方法(上面写的)
uri = getUriList.getNextPage();//获取下一页的url
if(uri != null)
{
uris.AddRange(getUriList.getUriList());//将获取的商品详情页的列表拼接到全局变量的列表上
getList(uri); //递归调用该方法,直到下一页链接为空
}
else
{
if (this.onGetListCompleted != null)
{
this.onGetListCompleted(this, new OnGetListCompletedEventArgs(uris));
}
}
};
tc.Start(uri); //执行TestCrawer的Start方法。
}
为了方便主线程执行回调函数,我们还要写一个完成事件。
class OnGetListCompletedEventArgs
{
public List<Uri> uris { get; private set; }
public OnGetListCompletedEventArgs(List<Uri> uris)
{
this.uris = uris;
}
}
至此,TestCrawControl类完成,全部代码如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JDLaptop_Spider.Codes
{
class TestCrawControl
{
public event EventHandler<OnGetListCompletedEventArgs> onGetListCompleted;
List<Uri> uris = new List<Uri>();
public TestCrawControl() { }
public async Task<List<Uri>> Start(Uri uri)
{
getList(uri);
return uris;
}
private void getList(Uri starUri)
{
Uri uri = starUri;
TestCrawler tc = new TestCrawler();
tc.OnCompleted += (s, ev) =>
{
GetUriList getUriList = new GetUriList(ev.PageSource);
uri = getUriList.getNextPage();
if(uri != null)
{
uris.AddRange(getUriList.getUriList());
getList(uri);
}
else
{
if (this.onGetListCompleted != null)
{
this.onGetListCompleted(this, new OnGetListCompletedEventArgs(uris));
}
}
};
tc.Start(uri);
}
}
class OnGetListCompletedEventArgs
{
public List<Uri> uris { get; private set; }
public OnGetListCompletedEventArgs(List<Uri> uris)
{
this.uris = uris;
}
}
}
在主线程调用
这里我创建的是winform,命令行在main方法中同理
private void Form1_Load(object sender, EventArgs e)
{
Uri uri = new Uri("https://list.jd.com/list.html?cat=670,671,672&page=1&delivery=1&sort=sort_totalsales15_desc&trans=1&JL=6_0_0#J_main"); //定义入口url
TestCrawControl testCrawControl = new TestCrawControl();//新建一个TestCrawControl类
TestCrawler testCrawler = new TestCrawler();//新建一个TestCrawler类
testCrawler.OnCompleted += (s, ev) => //绑定TestCrawler完成时的回调方法
{
/*这里写获取商品详细信息的代码*/
};
testCrawControl.onGetListCompleted += (s, ev) => //绑定TestCrawlControl完成时的回调方法。
{
List<Uri> uris = ev.uris; //获取商品详情页url列表
foreach (var i in uris) //逐一访问列表中的链接并获取其源代码
{
testCrawler.Start(i);
}
};
testCrawControl.Start(uri);
}
其他
在这里我没有给出获取详细信息的代码,因为思路和方法与上面讲的差不多,唯一的区别就是获取商品价格时需要直接调用接口读取json数据。
所以在这里,我给出获取json数据的方法和获取商品价格的方法
public double GetPrice(String JID)
{
String Price;
String url = @"http://p.3.cn/prices/mgets?type=1&skuIds=%JID%"; //商品价格接口
url = url.Replace("%JID%",JID);//替换字符串结尾为商品id
String Json = GetJSON(url); //调用下面的方法获取Json字符串
/* 这里需要开头说过的两个库
* using Newtonsoft.Json;
* using Newtonsoft.Json.Linq;
*/
JArray ja = (JArray)JsonConvert.DeserializeObject(Json);
JObject jObject = (JObject)ja[0];
Price = jObject["op"].ToString();
return double.Parse(Price);
}
private String GetJSON(String url)
{
//就是调用了一个Request方法
var request = (HttpWebRequest)WebRequest.Create(new Uri(url));
var respone = (HttpWebResponse)request.GetResponse();
var stream = respone.GetResponseStream();
var reader = new StreamReader(stream, Encoding.Default);
String Json = reader.ReadToEnd();
reader.Close();
stream.Close();
request.Abort();
respone.Close();
return Json;
}
接口返回的json如图
数据库操作
这里我选择了SQLite数据库,因为方便嵌入,至于教程网上一搜一大把,这里我也不给具体代码了,这里给出两个大佬博客可供参考
SQLite之C#连接SQLite
C#操作SQLite数据库
如果选择其他数据库或者XML,Json存储的话,请自行查阅相关知识。