Spring Boot集成WebMagic爬取京东商品信息
爬取分析
爬取
https://search.jd.com/
上的商品信息。只爬取笔记本电脑
相关的商品信息。
商品列表
访问页面并搜索笔记本电脑
,得到商品列表。
商品列表由
id=J_goodsList
的div包裹,每个商品对应一个li
标签,li
标签中clas=p-name
的元素包含了商品详情页地址
点击分页,得到请求地址:
https://search.jd.com/Search?keyword=%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91&wq=%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91&pvid=19a729aac00f4decbacb5de334b3be4e&page=3&s=56&click=0
。分页规则:分页是以奇数分页,如:page=1、page=3、page=5
商品详情页
进入商品详情页分析需要的数据。例如:商品sku、商品标题、商品图片、商品价格、商品详情地址、商品店铺等
在
class=preview
对应div下的img
元素的data-origin
属性获取图片地址。
标题的获取
规格的获取,通过该规格对应的sku,通过接口请求获取商品价格
执行流程
graph LR
A[解析商品列表页] ----> B[获取商品详情页]
B ----> C(解析页面获取数据)
搭建基础环境
添加依赖
<!--SpringMVC-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--SpringData Jpa-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--MySQL连接包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!--WebMagic核心包-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>0.7.5</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>0.7.5</version>
</dependency>
<!--WebMagic对布隆过滤器的支持-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--工具包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
POJO
@Entity
@Table(name = "item")
@Data
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//商品标题
private String title;
//商品价格
private Double price;
//商品图片
private String pic;
//商品详情地址
private String url;
//规格
private String standard;
//店铺;
private String shop;
//创建时间
private Date created;
}
Dao
public interface ItemDao extends JpaRepository<Item,Long> {
}
Service
public interface ItemService {
void save(Item item);
}
@Service
public class ItemServiceImpl implements ItemService {
@Autowired
private ItemDao itemDao;
@Override
@Transactional
public void save(Item item) {
this.itemDao.save(item);
}
}
开启定时任务
在启动类开启定时任务开关,通过启动项目自动执行爬虫任务。
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
HttpUtils
@Component
public class HttpUtils {
private PoolingHttpClientConnectionManager cm;
public HttpUtils() {
this.cm = new PoolingHttpClientConnectionManager();
//设置最大连接数
this.cm.setMaxTotal(100);
//设置每个主机的最大连接数
this.cm.setDefaultMaxPerRoute(10);
}
/**
* 根据请求地址下载页面数据
*
* @param url
* @return 页面数据
*/
public String doGetHtml(String url) {
//获取HttpClient对象
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(this.cm).build();
//创建httpGet请求对象,设置url地址
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36");
//设置请求信息
httpGet.setConfig(this.getConfig());
CloseableHttpResponse response = null;
try {
//使用HttpClient发起请求,获取响应
response = httpClient.execute(httpGet);
//解析响应,返回结果
if (response.getStatusLine().getStatusCode() == 200) {
//判断响应体Entity是否不为空,如果不为空就可以使用EntityUtils
if (response.getEntity() != null) {
String content = EntityUtils.toString(response.getEntity(), "utf8");
return content;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭response
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//返回空串
return "";
}
/**
* 设置请求信息
*
* @return
*/
private RequestConfig getConfig() {
RequestConfig config = RequestConfig.custom()
//创建连接的最长时间
.setConnectTimeout(1000)
// 获取连接的最长时间
.setConnectionRequestTimeout(500)
//数据传输的最长时间
.setSocketTimeout(10000)
.build();
return config;
}
}
自定义Pipeline保存结果
@Component
public class MyDataPipeline implements Pipeline {
@Autowired
private ItemService itemService;
@Override
public void process(ResultItems resultItems, Task task) {
//获取封装好的商品详情对象
Item itemInfo = resultItems.get("itemInfo");
if (itemInfo != null) {
this.itemService.save(itemInfo);
}
}
}
爬虫逻辑实现
@Component
public class JobProcessor implements PageProcessor {
@Autowired
private MyDataPipeline myDataPipeline;
@Autowired
private HttpUtils httpUtils;
private int currentPage=1;
private String url ="https://search.jd.com/Search?keyword=%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91&wq=%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91&pvid=19a729aac00f4decbacb5de334b3be4e&s=56&click=0&page=";
@Override
public void process(Page page) {
// 解析页面,获取商品信息详情的url地址
Selectable css = page.getHtml().css("div#J_goodsList");
List<Selectable> list = page.getHtml().css("div#J_goodsList li").nodes();
if (list.size() == 0) {
// 表示商品详情页,解析页面,获取商品详情信息,保存数据
this.saveJobInfo(page);
} else {
// 表示商品列表页,解析出详情页的url地址,放到任务队列中
for (Selectable selectable : list) {
String url = selectable.css("div.p-name").links().toString();
// 把获取到的url地址放到任务队列中
page.addTargetRequest(url);
}
// 获取下一页的url
currentPage=currentPage+2;
// 把url放到任务队列中
page.addTargetRequest(url+currentPage);
}
}
//解析页面,获取商品详情信息,保存数据
private void saveJobInfo(Page page) {
// 创建商品详情对象
Item item = new Item();
// 解析页面
Html html = page.getHtml();
// 设置详情页地址
String url = page.getUrl().toString();
item.setUrl(url);
// 商品图片地址
String src = "https://"+html.css("div.preview img", "data-origin").toString();
item.setPic(src);
// 获取名称
item.setTitle(html.css("div.sku-name","text").toString());
// 获取当前选择的sku
String sku = html.css("div#choose-attr-2 div.selected", "data-sku").toString();
// 规格
String standard = html.css("div#choose-attr-2 div.selected", "data-value").toString();
item.setStandard(standard);
// 商品价格,先获取sku,通过接口获取价格
String jsonPrice = httpUtils.doGetHtml("https://" + "p.3.cn/prices/mgets?skuIds=J_" + sku);
JSONObject priceObject = (JSONObject)JSONArray.parseArray(jsonPrice).get(0);
item.setPrice(priceObject.getDouble("p"));
// 店铺名称
String text = html.css("div.popbox-inner a", "text").toString();
// String text2 = Jsoup.parse(html.css("div.popbox-inner a").toString()).text();
item.setShop(text);
item.setCreated(new Date());
// 一个SPU商品里面包含多个SKU商品,每个SKU商品都有自己的价格,所以需要获取每个SKU商品的价格
List<Selectable> nodes = html.css("div#choose-attr-2 div.item").nodes();
for (Selectable node : nodes) {
String nodeSku = node.css("div.item","data-sku").toString();
// 排除上述处理的sku对应商品
if (sku.equals(nodeSku)){
continue;
}
page.addTargetRequest("https://item.jd.com/"+nodeSku+".html");
}
page.putField("itemInfo",item);
}
private Site site = Site.me()
.setCharset("UTF-8")//设置编码
.setTimeOut(10 * 1000)//设置超时时间
.setRetrySleepTime(3000)//设置重试的间隔时间
.setRetryTimes(3)//设置重试的次数
.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36")//设置请求头
;
@Override
public Site getSite() {
return site;
}
/**
* 定时执行
*
* initialDelay:启动项目延迟执行
* fixedDelay:每隔5s执行一次
*/
@Scheduled(initialDelay = 2000, fixedDelay = 5000)
public void process() {
Spider.create(new JobProcessor())
// 初始访问url地址
.addUrl(url+currentPage)
// 使用布隆过滤器
.setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(100000)))
.thread(6)
.addPipeline(this.myDataPipeline)
.run();
}
}
执行测试
启动项目,执行查看结果。