Compare commits

..

2 Commits

Author SHA1 Message Date
Naida Vatric
f56cd5b549 More elegant scrape of lat and long. 2020-02-17 21:55:24 +01:00
Naida Vatric
addd8c1344 Saljic crawler changed substring call. 2020-02-14 23:42:19 +01:00
15 changed files with 101 additions and 157 deletions

View File

@@ -41,13 +41,6 @@ const PROSTOR_LOGIN = {
PASSWORD: process.env.PROSTOR_LOGIN_PASS PASSWORD: process.env.PROSTOR_LOGIN_PASS
}; };
const USER_AGENT =
process.env.USER_AGENT ||
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36";
const USE_SCRAPER_API = process.env.USE_SCRAPER_API || 1; //Default to use
const SCRAPER_API_KEY = process.env.SCRAPER_API_KEY || "";
module.exports = { module.exports = {
APP_PORT, APP_PORT,
APP_URL, APP_URL,
@@ -61,8 +54,5 @@ module.exports = {
API_MAP_KEY, API_MAP_KEY,
STAGING, STAGING,
CHECK_UP_DAYS, CHECK_UP_DAYS,
PROSTOR_LOGIN, PROSTOR_LOGIN
USER_AGENT,
USE_SCRAPER_API,
SCRAPER_API_KEY
}; };

View File

@@ -1,6 +1,6 @@
"use strict"; "use strict";
const fetch = require("../../helpers/fetchWrapper"); const fetch = require("node-fetch");
const cheerio = require("cheerio"); const cheerio = require("cheerio");
const Promise = require("bluebird"); const Promise = require("bluebird");
const moment = require("moment-timezone"); const moment = require("moment-timezone");

View File

@@ -1,6 +1,6 @@
"use strict"; "use strict";
const fetch = require("../../helpers/fetchWrapper"); const fetch = require("node-fetch");
const cheerio = require("cheerio"); const cheerio = require("cheerio");
const Promise = require("bluebird"); const Promise = require("bluebird");
const moment = require("moment-timezone"); const moment = require("moment-timezone");

View File

@@ -1,6 +1,6 @@
"use strict"; "use strict";
const fetch = require("../../helpers/fetchWrapper"); const fetch = require("node-fetch");
const cheerio = require("cheerio"); const cheerio = require("cheerio");
const moment = require("moment-timezone"); const moment = require("moment-timezone");
const FormData = require("form-data"); const FormData = require("form-data");
@@ -191,7 +191,13 @@ class ProstorCrawler {
const { lat, lng, property_name, price, size, link, status } = realEstate; const { lat, lng, property_name, price, size, link, status } = realEstate;
//Status information is given already in realestate list //Status information is given already in realestate list
const adStatus = ProstorCrawler.getStatusId(status); //For VIP Ads status ='' canot be used, but no VIP ads are crawled
//We will make "fake" vip ad for RE that have size=55
//It is weird because yesterday it said 'VIP ponuda' ???
const adStatus =
size === "55"
? ProstorCrawler.getStatusId("VIP ponuda")
: ProstorCrawler.getStatusId(status);
const url = `https://prostor.ba${link}`; const url = `https://prostor.ba${link}`;

View File

@@ -1,6 +1,6 @@
"use strict"; "use strict";
const fetch = require("../../helpers/fetchWrapper"); const fetch = require("node-fetch");
const cheerio = require("cheerio"); const cheerio = require("cheerio");
const Promise = require("bluebird"); const Promise = require("bluebird");
const moment = require("moment-timezone"); const moment = require("moment-timezone");
@@ -399,9 +399,7 @@ class RentalCrawler {
); );
if (!publishedDateMoment.isValid()) { if (!publishedDateMoment.isValid()) {
throw { throw {
message: `Invalid published date : ${ message: `Invalid published date : ${extractedData["re_realEstates_inserted"]}`
extractedData["re_realEstates_inserted"]
}`
}; };
} }
@@ -412,9 +410,7 @@ class RentalCrawler {
); );
if (!renewedDateMoment.isValid()) { if (!renewedDateMoment.isValid()) {
throw { throw {
message: `Invalid renewed date : ${ message: `Invalid renewed date : ${extractedData["re_realEstates_edited"]}`
extractedData["re_realEstates_edited"]
}`
}; };
} }

View File

@@ -1,6 +1,6 @@
"use strict"; "use strict";
const fetch = require("../../helpers/fetchWrapper"); const fetch = require("node-fetch");
const cheerio = require("cheerio"); const cheerio = require("cheerio");
const moment = require("moment-timezone"); const moment = require("moment-timezone");
@@ -218,7 +218,7 @@ class SaljicCrawler {
} }
async scrapeAd(url, adType) { async scrapeAd(url, adType) {
// console.log("[SALJIC] Scraping : ", url); console.log("[SALJIC] Scraping : ", url);
try { try {
const adPageSource = await fetch(url); const adPageSource = await fetch(url);
const body = await adPageSource.text(); const body = await adPageSource.text();
@@ -227,7 +227,9 @@ class SaljicCrawler {
// No information for status ex. PRODAN // No information for status ex. PRODAN
const status = AD_STATUS.STATUS_NORMAL; const status = AD_STATUS.STATUS_NORMAL;
//Extracting agency ID from url //Extracting agency ID from url
const agencyObjectId = parseInt(url.substring(46, url.length)); const agencyObjectId = url
? parseInt(url.substring(46, url.length))
: null;
//Extracting main properties //Extracting main properties
const propertySelectors = { const propertySelectors = {
@@ -272,14 +274,10 @@ class SaljicCrawler {
.trim(); .trim();
const latAndLongSrc = $(propertySelectors.latAndLong).attr("src"); const latAndLongSrc = $(propertySelectors.latAndLong).attr("src");
const latText = latAndLongSrc.substring( const tmpLatLong = latAndLongSrc.split("marker=")[1];
latAndLongSrc.indexOf("marker=") + 7, const latText = tmpLatLong.split("%2C")[0];
latAndLongSrc.indexOf("%2C", latAndLongSrc.indexOf("marker=")) const longText = tmpLatLong.split("%2C")[1];
);
const longText = latAndLongSrc.substring(
latAndLongSrc.indexOf("%2C", latAndLongSrc.indexOf("marker=")) + 3,
latAndLongSrc.length
);
const locationLat = parseFloat(latText) || null; const locationLat = parseFloat(latText) || null;
const locationLong = parseFloat(longText) || null; const locationLong = parseFloat(longText) || null;
@@ -328,11 +326,10 @@ class SaljicCrawler {
let numberOfViewsKivi = null; let numberOfViewsKivi = null;
let streetNumber = 0; let streetNumber = 0;
let adStatus = status; let adStatus = status;
let shortDescription = descriptions.substring( let shortDescription = descriptions
0, ? descriptions.substring(0, descriptions.indexOf("."))
descriptions.indexOf(".") : "";
); let longDescription = descriptions || "";
let longDescription = descriptions;
//Extracting data - Glavne karakteristike //Extracting data - Glavne karakteristike
let mainFieldIndex = 1; let mainFieldIndex = 1;
do { do {
@@ -343,10 +340,14 @@ class SaljicCrawler {
.replace(/[\n\r\t]/gm, "") .replace(/[\n\r\t]/gm, "")
.trim(); .trim();
const mainFieldTitle = mainField.substring(0, mainField.indexOf(" ")); const mainFieldTitle = mainField
? mainField.substring(0, mainField.indexOf(" "))
: "";
const mainFieldValue = mainField const mainFieldValue = mainField
.substring(mainField.indexOf(" "), mainField.length) ? mainField
.trim(); .substring(mainField.indexOf(" "), mainField.length)
.trim()
: "";
switch (mainFieldTitle) { switch (mainFieldTitle) {
case "Površina": case "Površina":

View File

@@ -332,14 +332,10 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => {
}; };
} }
//When includeIncompleteAds are not defined - null it will consider it true
const order = [["updatedAt", "desc"]]; const order = [["updatedAt", "desc"]];
return db.RealEstate.findAll({ return db.RealEstate.findAll({
where: where: includeIncompleteAds ? queryIncludeIncomplete : query,
includeIncompleteAds || includeIncompleteAds == null
? queryIncludeIncomplete
: query,
limit: maxResults, limit: maxResults,
order order
}); });

View File

@@ -3,7 +3,6 @@ const db = require("../../models/index");
const sequelize = require("sequelize"); const sequelize = require("sequelize");
const Op = sequelize.Op; const Op = sequelize.Op;
const { AD_CATEGORY } = require("../../common/enums"); const { AD_CATEGORY } = require("../../common/enums");
const { CHECK_UP_DAYS } = require("../../config/appConfig");
const getSearchRequest = async searchRequestId => { const getSearchRequest = async searchRequestId => {
try { try {
@@ -17,22 +16,6 @@ const getSearchRequest = async searchRequestId => {
const createSearchRequest = async (searchRequestFields = {}) => { const createSearchRequest = async (searchRequestFields = {}) => {
return await db.SearchRequest.create(searchRequestFields); return await db.SearchRequest.create(searchRequestFields);
}; };
const findAllRequestsForCheckUp = async () => {
const checkUpOffset = 24 * 60 * 60 * 1000 * CHECK_UP_DAYS; //in miliseconds
const checkupDate = new Date();
checkupDate.setTime(checkupDate.getTime() - checkUpOffset);
const dateQuery = {
notifiedAt: {
[Op.lte]: checkupDate
}
};
const allRequestsForCheckUp = await db.SearchRequest.findAll({
where: dateQuery
});
return allRequestsForCheckUp;
};
const findSearchRequestsForRealEstate = async realEstate => { const findSearchRequestsForRealEstate = async realEstate => {
const { const {
@@ -174,7 +157,7 @@ const findSearchRequestsForRealEstate = async realEstate => {
} else { } else {
// If real estate dont have defined number of rooms ex. null // If real estate dont have defined number of rooms ex. null
//It returns requests that didn't choose number of rooms - also null //It returns requests that didn't choose number of rooms - also null
//Or ones that picked some values but also picked to includeIncomplete ads (or default) //Or ones that picked some values but also picked to includeIncomplete ads
numberOfRoomsQuery = { numberOfRoomsQuery = {
[Op.or]: [ [Op.or]: [
{ {
@@ -193,10 +176,7 @@ const findSearchRequestsForRealEstate = async realEstate => {
}, },
{ {
includeIncompleteAds: { includeIncompleteAds: {
[Op.or]: { [Op.eq]: true
[Op.eq]: true,
[Op.is]: null
}
} }
} }
] ]
@@ -246,10 +226,7 @@ const findSearchRequestsForRealEstate = async realEstate => {
}, },
{ {
includeIncompleteAds: { includeIncompleteAds: {
[Op.or]: { [Op.eq]: true
[Op.eq]: true,
[Op.is]: null
}
} }
} }
] ]
@@ -298,10 +275,7 @@ const findSearchRequestsForRealEstate = async realEstate => {
}, },
{ {
includeIncompleteAds: { includeIncompleteAds: {
[Op.or]: { [Op.eq]: true
[Op.eq]: true,
[Op.is]: null
}
} }
} }
] ]
@@ -339,10 +313,7 @@ const findSearchRequestsForRealEstate = async realEstate => {
}, },
{ {
includeIncompleteAds: { includeIncompleteAds: {
[Op.or]: { [Op.eq]: true
[Op.eq]: true,
[Op.is]: null
}
} }
} }
] ]
@@ -376,10 +347,7 @@ const findSearchRequestsForRealEstate = async realEstate => {
}, },
{ {
includeIncompleteAds: { includeIncompleteAds: {
[Op.or]: { [Op.eq]: true
[Op.eq]: true,
[Op.is]: null
}
} }
} }
] ]
@@ -413,10 +381,7 @@ const findSearchRequestsForRealEstate = async realEstate => {
}, },
{ {
includeIncompleteAds: { includeIncompleteAds: {
[Op.or]: { [Op.eq]: true
[Op.eq]: true,
[Op.is]: null
}
} }
} }
] ]
@@ -458,13 +423,10 @@ const findSearchRequestsForRealEstate = async realEstate => {
[Op.eq]: "ANY" [Op.eq]: "ANY"
}; };
} }
//Tag to check if incomplete ads are accepted in query which is default //Tag to check if incomplete ads are accepted in query
if (checkForIncompleteWanted) { if (checkForIncompleteWanted) {
query.includeIncompleteAds = { query.includeIncompleteAds = {
[Op.or]: { [Op.eq]: true
[Op.eq]: true,
[Op.is]: null
}
}; };
} }
@@ -476,6 +438,5 @@ const findSearchRequestsForRealEstate = async realEstate => {
module.exports = { module.exports = {
getSearchRequest, getSearchRequest,
createSearchRequest, createSearchRequest,
findSearchRequestsForRealEstate, findSearchRequestsForRealEstate
findAllRequestsForCheckUp
}; };

View File

@@ -2,6 +2,7 @@
const db = require("../../models/index"); const db = require("../../models/index");
const sequelize = require("sequelize"); const sequelize = require("sequelize");
const Op = sequelize.Op; const Op = sequelize.Op;
const { CHECK_UP_DAYS } = require("../../config/appConfig");
const findRealEstatesForSearchRequest = async searchRequestId => { const findRealEstatesForSearchRequest = async searchRequestId => {
const query = { const query = {
@@ -42,6 +43,42 @@ const findNotNotifiedMatches = async () => {
return matchingRecords; return matchingRecords;
}; };
const findAllRequestsForCheckUp = async () => {
//First we find IDs of search request that don't need to be emailed for check up - to EXCLUDE
//The ones that received notification for real estate CHECK_UP_DAYS days from now
const date = new Date();
const checkUpDate = date.getDate() - CHECK_UP_DAYS;
date.setDate(checkUpDate);
const dateQuery = {
createdAt: {
[Op.gte]: date
}
};
const excludedMatches = await db.SearchRequestMatch.findAll({
attributes: ["searchRequestId"],
where: dateQuery,
order: [["searchRequestId", "ASC"]]
});
const excludedRequestsAll = excludedMatches.map(match => {
return match.dataValues.searchRequestId;
});
//Removing duplicate search request id-s for optimization
const excludedRequests = [...new Set(excludedRequestsAll)];
const query = {
subscribed: true,
id: {
[Op.notIn]: excludedRequests
}
};
const allRequestsForCheckUp = await db.SearchRequest.findAll({
where: query
});
return allRequestsForCheckUp;
};
const addMatches = async matchingRecords => { const addMatches = async matchingRecords => {
return await db.SearchRequestMatch.bulkCreate(matchingRecords, { return await db.SearchRequestMatch.bulkCreate(matchingRecords, {
@@ -52,5 +89,6 @@ const addMatches = async matchingRecords => {
module.exports = { module.exports = {
findRealEstatesForSearchRequest, findRealEstatesForSearchRequest,
addMatches, addMatches,
findNotNotifiedMatches findNotNotifiedMatches,
findAllRequestsForCheckUp
}; };

View File

@@ -1,21 +0,0 @@
const nodeFetch = require("node-fetch");
const {
USER_AGENT,
USE_SCRAPER_API,
SCRAPER_API_KEY
} = require("../config/appConfig");
const fetch = async (url, options = {}) => {
const newOptions = Object.assign({}, options);
if (!newOptions["headers"]) {
newOptions["headers"] = {};
}
newOptions["headers"]["User-Agent"] = USER_AGENT;
const urlAdaptedForScraping = USE_SCRAPER_API
? `http://api.scraperapi.com/?api_key=${SCRAPER_API_KEY}&url=${url}`
: url;
return nodeFetch(urlAdaptedForScraping, newOptions);
};
module.exports = fetch;

View File

@@ -1,14 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("SearchRequests", "notifiedAt", {
type: Sequelize.DATE,
defaultValue: new Date()
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn("SearchRequests", "notifiedAt");
}
};

View File

@@ -15,7 +15,15 @@ module.exports = (sequelize, DataTypes) => {
allowNull: false, allowNull: false,
defaultValue: { defaultValue: {
type: "Polygon", type: "Polygon",
coordinates: [[[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]], coordinates: [
[
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
]
],
crs: { type: "name", properties: { name: "EPSG:4326" } } crs: { type: "name", properties: { name: "EPSG:4326" } }
} }
}, },
@@ -82,11 +90,7 @@ module.exports = (sequelize, DataTypes) => {
floorMin: DataTypes.INTEGER, floorMin: DataTypes.INTEGER,
floorMax: DataTypes.INTEGER, floorMax: DataTypes.INTEGER,
accessRoadType: DataTypes.TEXT, accessRoadType: DataTypes.TEXT,
heatingType: DataTypes.TEXT, heatingType: DataTypes.TEXT
notifiedAt: {
type: DataTypes.DATE,
defaultValue: new Date()
}
}); });
return SearchRequest; return SearchRequest;

View File

@@ -15,10 +15,9 @@ const {
} = require("../helpers/emailContentGenerator"); } = require("../helpers/emailContentGenerator");
const { const {
findNotNotifiedMatches, findNotNotifiedMatches,
findAllRequestsForCheckUp,
findRealEstatesForSearchRequest findRealEstatesForSearchRequest
} = require("../helpers/db/searchRequestMatch"); } = require("../helpers/db/searchRequestMatch");
const { findAllRequestsForCheckUp } = require("../helpers/db/searchRequest");
const { sendEmail } = require("../services/emailService"); const { sendEmail } = require("../services/emailService");
const notifyForNewRealEstates = async newRealEstates => { const notifyForNewRealEstates = async newRealEstates => {
@@ -36,7 +35,7 @@ const notifyForNewSearchRequest = async searchRequest => {
matchingRealEstates matchingRealEstates
); );
const { email } = searchRequest; const { email } = searchRequest;
//In case of the new search req, notifiedAt column is populated with default value - now (moment of creation)
await sendEmail( await sendEmail(
email, email,
`${stagingTag} Kivi - novi zahtjev za pretragu`, `${stagingTag} Kivi - novi zahtjev za pretragu`,
@@ -77,10 +76,6 @@ const notifyMatches = async (matches, dailyNotification = false) => {
sendEmailPromise.catch(err => sendEmailPromise.catch(err =>
console.log("[Email Sending Failed]", err) console.log("[Email Sending Failed]", err)
); );
//Change time of notified At for searchReq
searchRequest.notifiedAt = new Date();
searchRequest.save();
} }
} }
} }
@@ -136,7 +131,7 @@ const notifyRequestsWithDailyOption = async () => {
}; };
const checkUpNotify = async () => { const checkUpNotify = async () => {
const searchRequestsForCheckUp = await findAllRequestsForCheckUp(); /* const searchRequestsForCheckUp = await findAllRequestsForCheckUp();
const asyncSendEmailActions = []; const asyncSendEmailActions = [];
@@ -148,12 +143,8 @@ const checkUpNotify = async () => {
const sendEmailPromise = sendEmail(email, emailSubject, emailContent); const sendEmailPromise = sendEmail(email, emailSubject, emailContent);
asyncSendEmailActions.push(sendEmailPromise); asyncSendEmailActions.push(sendEmailPromise);
sendEmailPromise.catch(err => console.log("[Email Sending Failed]", err)); sendEmailPromise.catch(err => console.log("[Email Sending Failed]", err));
//Change time of notified At for searchReq
searchRequest.notifiedAt = new Date();
searchRequest.save();
} }
await Promise.all(asyncSendEmailActions); await Promise.all(asyncSendEmailActions); */
}; };
module.exports = { module.exports = {

View File

@@ -61,8 +61,9 @@
<p class="distinguished"> <p class="distinguished">
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" class="filled-in" name="includeIncompleteAds" <input type="checkbox" class="filled-in" name="includeIncompleteAds"
<% if (includeIncompleteAds) { %>
checked checked
> <% } %>>
<span>Uključi i oglase bez potpunih informacija</span> <span>Uključi i oglase bez potpunih informacija</span>
</label> </label>
</p> </p>

View File

@@ -10,7 +10,6 @@ APP_BASE_URL=base url for the app
ENVIRONMENT=Variable to denote development, staging and production ENVIRONMENT=Variable to denote development, staging and production
USER_AGENT=User agent header to send in fetch requests
MAX_REAL_ESTATES_IN_EMAIL=Max number of real estates that will be shown in email, others will be truncated and URL with full list will be shwon MAX_REAL_ESTATES_IN_EMAIL=Max number of real estates that will be shown in email, others will be truncated and URL with full list will be shwon
MAX_REAL_ESTATES_IN_FIRST_EMAIL=Max number of real estates that will be shown in first (welcome) email MAX_REAL_ESTATES_IN_FIRST_EMAIL=Max number of real estates that will be shown in first (welcome) email
@@ -22,10 +21,6 @@ GA_ID=Google Analytics ID
#=============== GOOGLE MAPS =============# #=============== GOOGLE MAPS =============#
API_MAP_KEY=(your-key-here) API_MAP_KEY=(your-key-here)
#=============== SCRAPER API SUPORT =============#
USE_SCRAPER_API= To turn it on (1) or off (0)
SCRAPER_API_KEY= Key for Scraper api
#=============== AWS SDK EMAIL SETTINGS =======# #=============== AWS SDK EMAIL SETTINGS =======#
AWS_KEY_ID=(your-key-here) AWS_KEY_ID=(your-key-here)
AWS_SECRET_ACCESS_KEY=(your-key-here) AWS_SECRET_ACCESS_KEY=(your-key-here)