Научитесь создавать веб-скрапер-приложение на Java. Веб-скрапер, разработанный в этом уроке, выполняет следующие действия:
- Считывает файл Excel и корневые URL-адреса из указанного столбца.
- Создает новую задачу Runnable и отправляет ее исполнителю пула потоков.
- Создает BlockingThreadPoolExecutor с семафором для включения регулирования задач
- Задача извлекает первые 20 ссылок из корневого URL
- Задача охватывает все 20 URL-адресов и извлекает заголовок страницы для каждой страницы.
- Периодически проверяет готовые URL-адреса и записывает посещенный URL-адрес и его название в отчет Excel.
Это приложение не должно использоваться в производстве, поскольку есть много областей для улучшения. Используйте этот код как концептуальную ссылку для того, как построить веб-скрапер.
Вы можете свободно заменять или изменять любую часть демонстрационного приложения в соответствии с вашими потребностями.
1. Зависимости Maven
Начните с включения последней версии следующих зависимостей:
- org.apache.poi:poi : Для чтения и записи файлов Excel.
- org.apache.poi:poi-ooxml : поддерживает чтение файла Excel с использованием парсера SAX.
- io.rest-assured:rest-assured : Для вызова URL-адресов и захвата выходных данных.
- org.jsoup:jsoup : Для анализа и извлечения информации из HTML-документа.
<dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>5.2.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.2</version></dependency><dependency><groupId>io.rest-assured</groupId><artifactId>rest-assured</artifactId><version>5.1.1</version></dependency><dependency><groupId>org.jsoup</groupId><artifactId>jsoup</artifactId><version>1.15.3</version></dependency>
2. Чтение входных данных и отправка на обработку
Скраппер считывает корневые URL из файла Excel и создает экземпляр UrlRecord. Для каждого UrlRecord создается новый поток ScrapTask и передается BlockingThreadPoolExecutor для вызова URL и извлечения заголовков.
2.1 Чтение входных данных из Excel
Приложение использует API парсера SAX для чтения Excel, поэтому если файл Excel слишком большой, мы не получим ошибки OutOfMemory. Мы только перечисляем соответствующие фрагменты кода. Полный список можно найти в репозитории Github.
public class WebScrappingApp {public static void main(String[] args) throws Exception {...RowHandler handler = new RowHandler();handler.readExcelFile("C:\\temp\\webscrapping-root-urls.xlsx");...}}
public class RowHandler extends SheetHandler {protected Map<String, String> headerRowMapping = new HashedMap<>();...@Overrideprotected void processRow() throws ExecutionException, InterruptedException {//Assuming that first row is column namesif(rowNumber > 1 && !rowValues.isEmpty()) {String url = rowValues.get("B");if(url != null && !url.trim().equals("")) {UrlRecord entity = new UrlRecord();entity.setRownum((int) rowNumber);entity.setRootUrl(url.trim()); //root URLJobSubmitter.submitTask(entity);}}}}
public class SheetHandler extends DefaultHandler {...public void readExcelFile(String filename) throws Exception {SAXParserFactory factory = SAXParserFactory.newInstance();SAXParser saxParser = factory.newSAXParser();try(OPCPackage opcPackage = OPCPackage.open(filename)) {XSSFReader xssfReader = new XSSFReader(opcPackage);sharedStringsTable =(SharedStringsTable) xssfReader.getSharedStringsTable();stylesTable = xssfReader.getStylesTable();ContentHandler handler = this;Iterator<InputStream> sheets = xssfReader.getSheetsData();if(sheets instanceof XSSFReader.SheetIterator) {XSSFReader.SheetIterator sheetIterator =(XSSFReader.SheetIterator) sheets;while(sheetIterator.hasNext()) {try(InputStream sheet = sheetIterator.next()) {sheetName = sheetIterator.getSheetName();sheetNumber++;startSheet();saxParser.parse(sheet,(DefaultHandler) handler);endSheet();}}}}}}
2.2. Передача на исполнение
Обратите внимание на оператор JobSubmitter.submitTask() в методе processRow() RowHandler. JobSubmitter отвечает за отправку задачи в BlockingThreadPoolExecutor.
public class JobSubmitter {public static List<Future<?>> futures = new ArrayList<Future<?>>();static BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<Runnable>(25000);static BlockingThreadPoolExecutor executor= new BlockingThreadPoolExecutor(5,10, 10000, TimeUnit.MILLISECONDS, blockingQueue);static {executor.setRejectedExecutionHandler(new RejectedExecutionHandler(){...});// Let start all core threads initiallyexecutor.prestartAllCoreThreads();}public static void submitTask(UrlRecord entity) {Future<?> f = executor.submit(new ScrapTask(entity));futures.add(f);}}
BlockingThreadPoolExecutor — это пользовательская реализация ThreadPoolExecutor, которая поддерживает регулирование задач, благодаря чему мы не превышаем количество доступных ресурсов, таких как HTTP-соединения.
public class BlockingThreadPoolExecutor extends ThreadPoolExecutor {private final Semaphore semaphore;public BlockingThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);semaphore = new Semaphore(10);}@Overrideprotected void beforeExecute(Thread t, Runnable r) {super.beforeExecute(t, r);}@Overridepublic void execute(final Runnable task) {boolean acquired = false;do {try {Thread.sleep(1000);semaphore.acquire();acquired = true;} catch(final InterruptedException e) {System.out.println("InterruptedException whilst aquiring semaphore" + e);}} while(!acquired);try {super.execute(task);} catch(final RejectedExecutionException e) {System.out.println("Task Rejected");semaphore.release();throw e;}}@Overrideprotected void afterExecute(Runnable r, Throwable t) {super.afterExecute(r, t);if(t != null) {t.printStackTrace();}semaphore.release();}}
3. Удаление URL-адресов
Фактическая обработка происходит в классе ScrapTask, который является потоком и выполняется исполнителем. ScrapTask вызывает URL-адреса и удаляет содержимое. Вся пользовательская логика для удаления находится здесь.
public class ScrapTask implements Runnable {public ScrapTask(UrlRecord entity) {this.entity = entity;}private UrlRecord entity;public UrlRecord getEntity() {return entity;}public void setEntity(UrlRecord entity) {this.entity = entity;}static String[] tokens = new String[]{" private", "pvt.", " pvt"," limited", "ltd.", " ltd"};static RestAssuredConfig config = RestAssured.config().httpClient(HttpClientConfig.httpClientConfig().setParam(CoreConnectionPNames.CONNECTION_TIMEOUT, 30000).setParam(CoreConnectionPNames.SO_TIMEOUT, 30000));@Overridepublic void run() {try {loadPagesAndGetTitles(entity.getRootUrl());} catch(Exception e) {e.printStackTrace();}}private void loadPagesAndGetTitles(String rootUrl) {try{Response response = given().config(config).when().get(rootUrl).then().log().ifError().contentType(ContentType.HTML).extract().response();Document document = Jsoup.parse(response.getBody().asString());Elements anchors = document.getElementsByTag("a");if(anchors != null && anchors.size() > 0) {for(int i = 0; i < anchors.size() && i < 20; i++) {String visitedUrl = anchors.get(i).attributes().get("href");if(visitedUrl.startsWith("/")) {String title = getTitle(rootUrl + visitedUrl);UrlRecord newEntity = new UrlRecord();newEntity.setRownum(WebScrappingApp.rowCounter++);newEntity.setRootUrl(entity.getRootUrl());newEntity.setVisitedUrl(rootUrl + visitedUrl);newEntity.setTitle(title);System.out.println("Fetched Record: " + newEntity);WebScrappingApp.processedRecords.add(newEntity);}}}} catch(Exception e) {e.printStackTrace();}}private String getTitle(String url) {try {Response response = given().config(config).when().get(url).then().log().ifError().contentType(ContentType.HTML).extract().response();Document document = Jsoup.parse(response.getBody().asString());Elements titles = document.getElementsByTag("title");if(titles != null && titles.size() > 0) {return titles.get(0).text();}}catch(Exception e) {e.printStackTrace();}return "Not Found";}}
4. Запись удаленной записи на выход
ScrapTask создает новые экземпляры UrlRecord для каждого URL и помещает их в BlockingQueue. Новый поток продолжает следить за этой очередью и удаляет все записи из очереди каждые 30 секунд. Затем поток-писатель отчетов продолжает добавлять новые записи в выходной файл Excel.
Периодическая запись записей помогает в случае сбоев и сохраняет память JVM свободной.
public class WebScrappingApp {public static void main(String[] args) throws Exception {...File outFile = new File("C:\\temp\\webscrapping-output-"+ random.nextLong() +".xlsx");Map<Integer, Object[]> columns = new HashMap<Integer, Object[]>();columns.put(rowCounter++, new Object[] {"URL", "Title"});ReportWriter.createReportFile(columns, outFile); //Create report one time//periodically, append rows to reportScheduledExecutorService es = Executors.newScheduledThreadPool(1);es.scheduleAtFixedRate(runAppendRecords(outFile), 30000, 30000, TimeUnit.MILLISECONDS);...}private static Runnable runAppendRecords(File file) {return new Runnable() {@Overridepublic void run() {if(processedRecords.size() > 0) {List<UrlRecord> recordsToWrite = new ArrayList<>();processedRecords.drainTo(recordsToWrite);Map<Integer, Object[]> data = new HashMap<>();for(UrlRecord entity : recordsToWrite) {data.put(rowCounter++, new Object[] {entity.getVisitedUrl(), entity.getTitle()});}System.out.println("###########Writing "+data.size()+" records to excel file############################");try {ReportWriter.appendRows(data, file);} catch(IOException e) {e.printStackTrace();} catch(InvalidFormatException e) {e.printStackTrace();}} else {System.out.println("===========Nothing to write. Waiting.============================");}}};}}
public class ReportWriter {public static void createReportFile(Map<Integer, Object[]> columns, File file){XSSFWorkbook workbook = new XSSFWorkbook();XSSFSheet sheet = workbook.createSheet("URL Titles");Set<Integer> keyset = columns.keySet();int rownum = 0;for(Integer key : keyset){Row row = sheet.createRow(rownum++);Object [] objArr = columns.get(key);int cellnum = 0;for(Object obj : objArr){Cell cell = row.createCell(cellnum++);if(obj instanceof String)cell.setCellValue((String)obj);else if(obj instanceof Integer)cell.setCellValue((Integer)obj);}}try{//Write the workbook in file systemFileOutputStream out = new FileOutputStream(file);workbook.write(out);out.close();}catch(Exception e){e.printStackTrace();}}public static void appendRows(Map<Integer, Object[]> data, File file) throws IOException, InvalidFormatException {XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(file));Sheet sheet = workbook.getSheetAt(0);int rownum = sheet.getLastRowNum() + 1;Set<Integer> keyset = data.keySet();for(Integer key : keyset){Row row = sheet.createRow(rownum++);Object [] objArr = data.get(key);int cellnum = 0;for(Object obj : objArr){Cell cell = row.createCell(cellnum++);if(obj instanceof String)cell.setCellValue((String)obj);else if(obj instanceof Integer)cell.setCellValue((Integer)obj);}}try{FileOutputStream out = new FileOutputStream(file);workbook.write(out);out.close();}catch(Exception e){e.printStackTrace();}}}
5. Демонстрация
Для демонстрации создайте входной файл Excel во временном каталоге следующим образом.

Теперь запустите приложение и ожидайте появления отчета во временной папке.

6. Заключение
В этом уроке Java мы научились создавать веб-скрапер. Мы научились создавать различные компоненты, необходимые для создания работающего веб-скрапера, хотя он далек от совершенства. Вы можете использовать этот случай в качестве отправной точки и настроить его в соответствии со своими потребностями.