首先先记录一下我自己对这个数据抓取的一些心得:
数据抓取也就是常说的爬虫。
在我没真正去做的时候,我还想爬虫好高大上。
现在学完之后也就怯魅了
其实本质就是在自己的代码中模拟浏览器给后端发请求,后端收到响应之后,返回给前端,这个时候我们进行接收即可。
当然我说的那是最简单的数据抓取,如果别人网站有反爬虫的机制,那可能就需要更多的成本了
说完了心得之后,就开始真正记录了
首先分析的是数据抓取的流程:
数据抓取流程
- 分析数据源,怎么获取?
- 拿到数据后,怎么处理?
- 写入数据库等存储
数据抓取的几种方式
- 直接请求数据接口(最方便),可使用 HttpClient、OKHttp、RestTemplate、Hutool 等客户端发送请求
- 等网页渲染出明文内容后,从前端完整页面中解析出需要的内容
- 有一些网站可能是动态请求的,他不会一次性加载所有的数据,而是要你点某个按钮、输入某个验证码才会显示出数据。可使用无头浏览器:selenium、node.js puppeteer(这篇文章没用到第三种方式)
获取网页的文字信息:
上面说了,本质上就是向后端发送请求
所以,我们第一步就是要知道往哪发请求,并且携带什么参数?
以编程导航网站为例编程导航 - 程序员一站式编程学习交流社区,做您编程学习路上的导航员 (code-nav.cn)
F12就能知道
查看这个携带的参数的时候,最好变成这种源代码模式,用视图好像是会有格式上的错误。
我们复制之后:
@SpringBootTest
public class crawler {
@Resource
private PostService postService;
@Test
public void TestCrawler(){
String json ="{\"pageSize\":12,\"sortOrder\":\"descend\",\"tags\":[\"Java\"],\"current\":1,\"reviewStatus\":1,\"category\":\"文章\",\"hiddenContent\":true,\"priorityList\":[999,9999],\"sorterList\":[{\"field\":\"priority\",\"asc\":false},{\"field\":\"createTime\",\"asc\":false}]}";
String url = "https://api.code-nav.cn/api/course/list/page/vo";
String result = HttpRequest.post(url)
.body(json)
.execute().body();
Map<String,Object> map = JSONUtil.toBean(result,Map.class);
JSONObject data = (JSONObject)map.get("data");
JSONArray records = (JSONArray) data.get("records");
//1:title 2:content 3:tag
List<Post> postList = new ArrayList<>();
for (Object record : records) {
JSONObject tempjson = (JSONObject)record;
Post post = new Post();
post.setTitle(tempjson.getStr("title"));
post.setContent(tempjson.getStr("content"));
Object tags = tempjson.get("tags");
if(!(tags instanceof JSONNull)){
JSONArray jsonArray = (JSONArray) tempjson.get("tags");
List<String> tagList = jsonArray.toList(String.class);
post.setTags(JSONUtil.toJsonStr(tagList));
}else {
post.setTags("");
}
post.setUserId(1L);
postList.add(post);
}
final boolean save = postService.saveBatch(postList);
System.out.println(save);
}
}
上面是编写了一个测试类。
一开始发送请求的时候,我们可以先看一下拿到了那些东西
到这一步,我们就需要思考?
我们需要什么?再针对从获得的资源中去取
我们项目的背景是要往数据库里面插入文章数据
所以我们需要的就是三个元素:
标题title 内容content 标签tags
明白了需求之后,我们就可以一步一步拆解我们拿到的资源
然后我们发现拿到的东西是一个json格式的数据
同样也是用Hutool的JSON解析的工具转换。
获取网页的图片信息:
在说获取网页的图片信息之前
可以先说一下这个项目的搜索图片的原理
这个平台搜索图片并不是将所有图片都存储在自己的数据库中
是自己的前端访问我的后端
然后我的后端通过我自己代码的逻辑去访问其它人的服务器
整体流程就是这样
讲完这个,我们直接开始
在开始看代码和网页之前,需要先介绍一个工具:Jsoup解析库:支持发送请求获取HTML文档,并且从中解析出需要的字段。
第一步肯定也是导入依赖:
<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.3</version>
</dependency>
第二步访问官网然后获取实例代码
jsoup: Java HTML parser, built for HTML editing, cleaning, scraping, and XSS safety
Document doc = Jsoup.connect("https://en.wikipedia.org/").get();
log(doc.title());
Elements newsHeadlines = doc.select("#mp-itn b a");
for (Element headline : newsHeadlines) {
log("%s\n\t%s",
headline.attr("title"), headline.absUrl("href"));
}
-
连接到 Wikipedia 并获取 HTML 文档:
Document doc = Jsoup.connect("https://en.wikipedia.org/").get();
Jsoup.connect("https://en.wikipedia.org/")
:使用 Jsoup 连接到指定的 URL(在这里是 Wikipedia 的主页)。.get()
:发送 HTTP GET 请求以获取该 URL 的 HTML 内容,并将其解析为Document
对象。
-
记录网页标题:
log(doc.title());
doc.title()
:获取网页的标题。log(...)
:假设这是一个自定义方法或库函数,用于打印或记录信息。这里它会输出网页的标题。
-
选择特定的 HTML 元素:
Elements newsHeadlines = doc.select("#mp-itn b a");
doc.select("#mp-itn b a")
:使用 CSS 选择器从 HTML 文档中选择所有符合#mp-itn b a
选择器的元素。Elements
是一个 Jsoup 的集合类,包含了所有符合选择器的Element
对象。
-
遍历并记录每个新闻标题的标题属性和链接:
for (Element headline : newsHeadlines) { log("%s\n\t%s", headline.attr("title"), headline.absUrl("href")); }
for (Element headline : newsHeadlines)
:遍历所有选择器匹配到的Element
对象。headline.attr("title")
:获取当前Element
的title
属性,这通常是链接的悬浮提示文本。headline.absUrl("href")
:获取当前Element
的href
属性的绝对 URL。log("%s\n\t%s", ...)
:记录或打印出每个新闻标题的title
属性和href
链接的绝对 URL。
总的来说,这段代码从 Wikipedia 的主页抓取 "In the news" 区域的所有新闻链接,然后输出每条新闻的标题和链接。
这里有一个点就是这个css选择器,待会也得需要用到这个点。
讲完了步骤
我们下面开始从网页分析:
当我们找不到页面中图片的元素的时候可以点这个检查
然后我们上下找一找
就会发现
这个class叫iusc的css样式选择器,就包含这个图片
然后我们复制这个名字
然后我们鼠标往上一放,就会发现所有的图片都被选中了
ok,那我们图片的资源就找到了
下一个是对应这个图片的标题
这个什么inflnk就能跟踪到这个图片的文字
我们还是和上面一样
最后整合一下,我们直接看代码:
@SpringBootTest
public class crawler {
@Resource
private PostService postService;
@Test
public void JsoupTest() throws IOException {
String current = "1";
String url = "https://cn.bing.com/images/search?q=小黑子&form=HDRSC2&first="+current;
Document doc = Jsoup.connect(url).get();
Elements elements = doc.select(".iusc");
final Elements elements1 = doc.select("a.inflnk");
List<Picture> pictureList = new ArrayList<>();
for (int i = 0; i < elements.size(); i++) {
//获取图片
final String m = elements.get(i).attr("m");
Map<String,Object> map = JSONUtil.toBean(m,Map.class);
final String murl = (String) map.get("murl");
//获取标题
final String title = elements1.get(i).attr("aria-label");
Picture picture = new Picture();
picture.setMurl(murl);
picture.setTitle(title);
pictureList.add(picture);
}
}
}
我们代码的整体逻辑就是:
首先先修改一下这个url
并且和上面Jsoup的select一样,我们将这个图片中我们刚刚获取的这个css样式选择器直接cv上去
接着就可以获取这个返回值了
然后我们也是一步一步进行输出,每个网页包裹的东西不太一样,我们一层一层取出来就行。
最后稍微来看一下在项目中的实现:
首先就是这个文字资源
我将这个这个放到了一个一次性任务中
这个一次性任务怎么保证一次性呢
我们都知道springboot项目启动后会加载所有的bean对象
我们只需要将这个类注册成bean即可,然后用完直接把@Component注销掉即可
下一个是图片的项目实战应用
@Service
public class PictureDataSource implements DataSource{
@Override
public Page doSearch(String searchText, long pageNum, long pageSize) {
String url = String.format("https://cn.bing.com/images/search?q=%s&form=HDRSC2&first=%s",searchText,pageSize);
Document doc = null;
try {
doc = Jsoup.connect(url).get();
} catch (IOException e) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
Elements elements = doc.select(".iusc");
final Elements elements1 = doc.select("a.inflnk");
List<Picture> pictureList = new ArrayList<>();
for (int i = 0; i < elements.size(); i++) {
//获取图片
final String m = elements.get(i).attr("m");
Map<String,Object> map = JSONUtil.toBean(m,Map.class);
final String murl = (String) map.get("murl");
//获取标题
final String title = elements1.get(i).attr("aria-label");
Picture picture = new Picture();
picture.setMurl(murl);
picture.setTitle(title);
pictureList.add(picture);
if(pictureList.size()>=pageSize){
break;
}
}
Page<Picture> picturePage= new Page<>(pageNum,pageSize);
picturePage.setRecords(pictureList);
return picturePage;
}
}
整体的东西是差不多的
我们将这个url修改成动态了
之前在测试类中我们是写死了小黑子
这里用一个searchText就行。