[ad_1]
Implementing Pure Language Processing and Graph Principle to check and advocate various kinds of paperwork
Lots of the initiatives folks develop in the present day usually start with the primary essential step: Energetic Analysis. Investing in what different folks have performed and constructing on their work is necessary on your mission’s skill so as to add worth. Not solely must you be taught from the sturdy conclusions of what different folks have performed, however you additionally will need to work out what you shouldn’t do in your mission to make sure its success.
As I labored by my thesis, I began amassing plenty of various kinds of analysis recordsdata. For instance, I had collections of various tutorial publications I learn by in addition to excel sheets with info containing the outcomes of various experiments. As I accomplished the analysis for my thesis, I questioned: Is there a approach to create a advice system that may evaluate all of the analysis I’ve in my archive and assist information me in my subsequent mission?
In actual fact, there’s!
Observe: Not solely would this be for a repository of all the analysis you could be amassing from varied engines like google, however it can additionally work for any listing you may have containing varied kinds of completely different paperwork.
I developed this advice with my staff utilizing Python 3.
There are many APIs that help this advice system and researching what every particular API can carry out could also be helpful on your personal studying.
import string
import csv
from io import StringIO
from pptx import Presentation
import docx2txt
import PyPDF2
import spacy
import pandas as pd
import numpy as np
import nltk
import re
import openpyxl
from nltk.stem import WordNetLemmatizer
from sklearn.feature_extraction.textual content import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from gensim.parsing.preprocessing import STOPWORDS as SW
nltk.obtain('stopwords')
nltk.obtain('wordnet')
nltk.obtain('omw-1.4')
nltk.obtain('averaged_perceptron_tagger')
from nltk.corpus import wordnet
import networkx as nx
from networkx.algorithms.shortest_paths import weighted
import glob
The Hurdle
One huge hurdle I needed to overcome was the necessity for the advice machine’s skill to check various kinds of recordsdata. For instance, I needed to see if an excel spreadsheet has info related or is linked to the knowledge inside a PowerPoint and tutorial PDF journal. The trick to doing this was studying each file kind into Python and reworking every object right into a single string of phrases. This normalizes all the info and permits for the calculation of a similarity metric.
PDF Studying Class
The primary class we are going to have a look at for this mission is the pdfReader class which is ready to format a PDF to be readable in Python. Of all of the file codecs, I’d argue that PDFs are one of the vital necessary since most of the journal articles downloaded from analysis repositories similar to Google Scholar are in PDF format.
class pdfReader:def __init__(self, file_path: str) -> str:
self.file_path = file_path
def PDF_one_pager(self) -> str:
"""A perform which returns a one line string of the
pdf.
Returns:
one_page_pdf (str): A one line string of the pdf.
"""
content material = ""
p = open(self.file_path, "rb")
pdf = PyPDF2.PdfReader(p)
num_pages = len(pdf.pages)
for i in vary(0, num_pages):
content material += pdf.pages[i].extract_text() + "n"
content material = " ".be part of(content material.exchange(u"xa0", " ").strip().break up())
page_number_removal = r"d{1,3} of d{1,3}"
page_number_removal_pattern = re.compile(page_number_removal, re.IGNORECASE)
content material = re.sub(page_number_removal_pattern, '',content material)
return content material
def pdf_reader(self) -> str:
"""A perform which may learn .pdf formatted recordsdata
and returns a python readable pdf.
Returns:
read_pdf: A python readable .pdf file.
"""
opener = open(self.file_path,'rb')
read_pdf = PyPDF2.PdfFileReader(opener)
return read_pdf
def pdf_info(self) -> dict:
"""A perform which returns an info dictionary of a
pdf.
Returns:
dict(pdf_info_dict): A dictionary containing the meta
information of the item.
"""
opener = open(self.file_path,'rb')
read_pdf = PyPDF2.PdfFileReader(opener)
pdf_info_dict = {}
for key,worth in read_pdf.documentInfo.objects():
pdf_info_dict[re.sub('/',"",key)] = worth
return pdf_info_dict
def pdf_dictionary(self) -> dict:
"""A perform which returns a dictionary of
the item the place the keys are the pages
and the textual content inside the pages are the
values.
Returns:
dict(pdf_dict): A dictionary pages and textual content.
"""
opener = open(self.file_path,'rb')
read_pdf = PyPDF2.PdfReader(opener)
size = read_pdf.pages
pdf_dict = {}
for i in vary(size):
web page = read_pdf.getPage(i)
textual content = web page.extract_text()
pdf_dict[i] = textual content
return pdf_dict
Microsoft Powerpoint Reader
The pptReader class is able to studying Microsoft Powerpoint recordsdata into Python.
class pptReader:def __init__(self, file_path: str) -> None:
self.file_path = file_path
def ppt_text(self) -> str:
"""A perform that returns a string of textual content from all
of the slides in a pptReader object.
Returns:
textual content (str): A single string containing the textual content
inside every slide of the pptReader object.
"""
prs = Presentation(self.file_path)
textual content = str()
for slide in prs.slides:
for form in slide.shapes:
if not form.has_text_frame:
proceed
for paragraph in form.text_frame.paragraphs:
for run in paragraph.runs:
textual content += ' ' + run.textual content
return textual content
Microsoft Phrase Doc Reader
The wordDocReader class can be utilized for studying Microsoft Phrase Paperwork in Python. It makes use of the doc2txt API and returns a string of the textual content/info situated inside a given phrase doc.
class wordDocReader:
def __init__(self, file_path: str) -> str:
self.file_path = file_pathdef word_reader(self):
"""A perform that transforms a wordDocReader object right into a Python readable
phrase doc."""
textual content = docx2txt.course of(self.file_path)
textual content = textual content.exchange('n', ' ')
textual content = textual content.exchange('xa0', ' ')
textual content = textual content.exchange('t', ' ')
return textual content
Microsft Excel Reader
Generally researchers will embody excel sheets of their outcomes with their publications. With the ability to learn the column names, and even the values, might assist with recommending outcomes which are like what you might be looking for. For instance, what for those who had been researching info on the previous efficiency of a sure inventory? Perhaps you seek for the identify and image which is annotated in a historic efficiency excel sheet. This advice system would advocate the excel sheet to you to assist along with your analysis.
class xlsxReader:def __init__(self, file_path: str) -> str:
self.file_path = file_path
def xlsx_text(self):
"""A perform which returns the string of an
excel doc.
Returns:
textual content(str): String of textual content of a doc.
"""
inputExcelFile = self.file_path
textual content = str()
wb = openpyxl.load_workbook(inputExcelFile)
#This can save the excel sheet as a CSV file
for sn in wb.sheetnames:
excelFile = pd.read_excel(inputExcelFile, engine = 'openpyxl', sheet_name = sn)
excelFile.to_csv("ResultCsvFile.csv", index = None, header=True)
with open("ResultCsvFile.csv", "r") as csvFile:
traces = csvFile.learn().break up(",") # "rn" if wanted
for val in traces:
if val != '':
textual content += val + ' '
textual content = textual content.exchange('ufeff', '')
textual content = textual content.exchange('n', ' ')
return textCSV File Reader
The csvReader class will enable for CSV recordsdata to be included in your database and for use within the system’s suggestions.
class csvReader:def __init__(self, file_path: str) -> str:
self.file_path = file_path
def csv_text(self):
"""A perform which returns the string of a
csv doc.
Returns:
textual content(str): String of textual content of a doc.
"""
textual content = str()
with open(self.file_path, "r") as csvFile:
traces = csvFile.learn().break up(",") # "rn" if wanted
for val in traces:
textual content += val + ' '
textual content = textual content.exchange('ufeff', '')
textual content = textual content.exchange('n', ' ')
return textMicrosoft PowerPoint Reader
Right here’s a useful class. Not many individuals take into consideration how there’s beneficial info saved inside the our bodies of PowerPoint displays. These displays are by and huge created to visualise key concepts and knowledge to the viewers. The next class will assist relate any PowerPoints you may have in your database to different our bodies of data in hopes of steering you in direction of linked items of labor.
class pptReader:def __init__(self, file_path: str) -> str:
self.file_path = file_path
def ppt_text(self):
"""A perform which returns the string of a
Mirocsoft PowerPoint doc.
Returns:
textual content(str): String of textual content of a doc.
"""
prs = Presentation(self.file_path)
textual content = str()
for slide in prs.slides:
for form in slide.shapes:
if not form.has_text_frame:
proceed
for paragraph in form.text_frame.paragraphs:
for run in paragraph.runs:
textual content += ' ' + run.textual content
return textMicrosoft Phrase Doc Reader
The ultimate class for this method is a Microsoft Phrase doc reader. Phrase paperwork are one other beneficial supply of data. Many individuals will write studies, indicating their findings and concepts in phrase doc format.
class wordDocReader:
def __init__(self, file_path: str) -> str:
self.file_path = file_pathdef word_reader(self):
"""A perform which returns the string of a
Microsoft Phrase doc.
Returns:
textual content(str): String of textual content of a doc.
"""
textual content = docx2txt.course of(self.file_path)
textual content = textual content.exchange('n', ' ')
textual content = textual content.exchange('xa0', ' ')
textual content = textual content.exchange('t', ' ')
return textual content
That’s a wrap for the courses utilized in in the present day’s mission. Please be aware: there are tons of different file sorts you should utilize to reinforce your advice system. A present model of the code being developed will settle for photos and attempt to relate them to different paperwork inside a database!
Preprocessing
Let’s have a look at preprocess this information. This advice system was constructed for a repository of educational analysis, subsequently the necessity to break the textual content down utilizing the preprocessing steps guided by Pure Language Processing (NLP) was necessary.
The information processing class is solely known as datapreprocessor and the primary perform inside the class is a phrase components of speech tagger.
class dataprocessor:
def __init__(self):
return@staticmethod
def get_wordnet_pos(textual content: str) -> str:
"""Map POS tag to first character lemmatize() accepts
Inputs:
textual content(str): A string of textual content
Returns:
tag_dict(dict): A dictionary of tags
"""
tag = nltk.pos_tag([text])[0][1][0].higher()
tag_dict = {"J": wordnet.ADJ,
"N": wordnet.NOUN,
"V": wordnet.VERB,
"R": wordnet.ADV}
return tag_dict.get(tag, wordnet.NOUN)
This perform tags the components of speech in a phrase and can turn out to be useful later within the mission.
Second, there’s a perform that conducts the traditional NLP steps many people have seen earlier than. These steps are:
- Lowercase every phrase
- Take away the punctuation
- Take away digits (I solely needed to have a look at non-numeric info. This step might be taken out if desired)
- Stopword elimination.
- Lemmanitizaion. That is the place the get_wordnet_pos() perform turns out to be useful for together with components of speech!
@staticmethod
def preprocess(textual content: str):
"""A perform that prepoccesses textual content by the
steps of Pure Language Processing (NLP).
Inputs:
textual content(str): A string of textual contentReturns:
textual content(str): A processed string of textual content
"""
#lowercase
textual content = textual content.decrease()
#punctuation elimination
textual content = "".be part of([i for i in text if i not in string.punctuation])
#Digit elimination (Just for ALL numeric numbers)
textual content = [x for x in text.split(' ') if x.isnumeric() == False]
#Cease elimination
stopwords = nltk.corpus.stopwords.phrases('english')
custom_stopwords = ['n','nn', '&', ' ', '.', '-', '$', '@']
stopwords.lengthen(custom_stopwords)
textual content = [i for i in text if i not in stopwords]
textual content = ' '.be part of(phrase for phrase in textual content)
#lemmanization
lm = WordNetLemmatizer()
textual content = [lm.lemmatize(word, dataprocessor.get_wordnet_pos(word)) for word in text.split(' ')]
textual content = ' '.be part of(phrase for phrase in textual content)
textual content = re.sub(' +', ' ',textual content)
return textual content
Subsequent, there’s a perform to learn all the recordsdata into the system.
@staticmethod
def data_reader(list_file_names):
"""A perform that reads within the information from a listing of recordsdata.Inputs:
list_file_names(record): Checklist of the filepaths in a listing.
Returns:
text_list (record): A listing the place every worth is a string of textual content
for every file within the listing
file_dict(dict): Dictionary the place the keys are the filename and the values
are the knowledge discovered inside every given file
"""
text_list = []
reader = dataprocessor()
for file in list_file_names:
temp = file.break up('.')
filetype = temp[-1]
if filetype == "pdf":
file_pdf = pdfReader(file)
textual content = file_pdf.PDF_one_pager()
elif filetype == "docx":
word_doc_reader = wordDocReader(file)
textual content = word_doc_reader.word_reader()
elif filetype == "pptx" or filetype == 'ppt':
ppt_reader = pptReader(file)
textual content = ppt_reader.ppt_text()
elif filetype == "csv":
csv_reader = csvReader(file)
textual content = csv_reader.csv_text()
elif filetype == 'xlsx':
xl_reader = xlsxReader(file)
textual content = xl_reader.xlsx_text()
else:
print('File kind {} not supported!'.format(filetype))
proceed
textual content = reader.preprocess(textual content)
text_list.append(textual content)
file_dict = dict()
for i,file in enumerate(list_file_names):
file_dict[i] = (file, file.break up('/')[-1])
return text_list, file_dict
As that is the primary model of this method, I need to foot stomp that the code will be tailored to incorporate many different file sorts!
The subsequent perform is named the database_preprocess() which is used to course of all the recordsdata inside your given database. The enter is a listing of the recordsdata, every with its related string of textual content (processed already). The strings of textual content are then vectorized utilizing sklearn’s tfidVectorizer. What’s that precisely? Mainly, it can rework all of the textual content into completely different function vectors based mostly on the frequency of every given phrase. We do that so we are able to have a look at how carefully associated paperwork are utilizing similarity formulation referring to vector arithmetic.
@staticmethod
@staticmethod
def database_processor(file_dict,text_list: record):
"""A perform that transforms the textual content of every file inside the
database right into a vector.Inputs:
file_dixt(dict): Dictionary the place the keys are the filename and the values
are the knowledge discovered inside every given file
text_list (record): A listing the place every worth is a string of the textual content
for every file within the listing
Returns:
list_dense(record): A listing of the recordsdata' textual content was vectors.
vectorizer: The vectorizor used to rework the strings of textual content
file_vector_dict(dict): A dictionary the place the file names are the keys
and the vectors of every recordsdata' textual content are the values.
"""
file_vector_dict = dict()
vectorizer = TfidfVectorizer()
vectors = vectorizer.fit_transform(text_list)
feature_names = vectorizer.get_feature_names_out()
matrix = vectors.todense()
list_dense = matrix.tolist()
for i in vary(len(list_dense)):
file_vector_dict[file_dict[i][1]] = list_dense[i]
return list_dense, vectorizer, file_vector_dict
The rationale a vectorizer is created off of the database is that when a person offers a listing of phrases to seek for within the database, these phrases might be vectorized based mostly on their frequency in mentioned database. That is the largest weak spot of the present system. As we enhance the dimensions of the database, the time and computational allocation wanted for calculating similarities will enhance and decelerate the system. One advice given throughout a top quality management assembly was to make use of Reinforcement Studying for recommending completely different articles of knowledge.
Subsequent, we are able to use an enter processor that processes any phrase offered right into a vector. That is synonymous to while you kind a request right into a search engine.
@staticmethod
def input_processor(textual content, TDIF_vectorizor):
"""A perform which accepts a string of textual content and vectorizes the textual content utilizing a
TDIF vectorizoer.Inputs:
textual content(str): A string of textual content
TDIF_vectorizor: A pretrained vectorizor
Returns:
phrases(record): A listing of the enter textual content in vectored type.
"""
phrases = ''
total_words = len(textual content.break up(' '))
for phrase in textual content.break up(' '):
phrases += (phrase + ' ') * total_words
total_words -= 1
phrases = [words[:-1]]
phrases = TDIF_vectorizor.rework(phrases)
phrases = phrases.todense()
phrases = phrases.tolist()
return phrases
Since all the info inside and given to the database might be vectors, we are able to use cosine similarity to compute the angle between the vectors. The nearer the angle is to 0, the much less related the 2 mentioned vectors might be.
@staticmethod
def similarity_checker(vector_1, vector_2):
"""A perform which accepts two vectors and computes their cosine similarity.Inputs:
vector_1(int): A numerical vector
vector_2(int): A numerical vector
Returns:
cosine_similarity([vector_1], vector_2) (int): Cosine similarity rating
"""
vectors = [vector_1, vector_2]
for vec in vectors:
if np.ndim(vec) == 1:
vec = np.expand_dims(vec, axis=0)
return cosine_similarity([vector_1], vector_2)
As soon as the aptitude of discovering the similarity rating between two vectors is completed, rankings can now be created between the phrases being searched and the paperwork situated inside the database.
@staticmethod
def recommender(vector_file_list,query_vector, file_dict):
"""A perform which accepts a listing of vectors, question vectors, and a dictionary
pertaining to the record of vectors with their authentic values and file names.Inputs:
vector_file_list(record): A listing of vectors
query_vector(int): A numerical vector
file_dict(dict): A dictionary of filenames and textual content referring to the record
of vectors
Returns:
final_recommendation (record): A listing of the ultimate really useful recordsdata
similarity_list[:len(final_recommendation)] (record): A listing of the similarity
scores of the ultimate suggestions.
"""
similarity_list = []
score_dict = dict()
for i,file_vector in enumerate(vector_file_list):
x = dataprocessor.similarity_checker(file_vector, query_vector)
score_dict[file_dict[i][1]] = (x[0][0])
similarity_list.append(x)
similarity_list = sorted(similarity_list, reverse = True)
#Recommends the highest 20%
really useful = sorted(score_dict.objects(),
key=lambda x:-x[1])[:int(np.round(.5*len(similarity_list)))]
final_recommendation = []
for i in vary(len(really useful)):
final_recommendation.append(really useful[i][0])
#add in graph for higher than 3 recommendationa
return final_recommendation, similarity_list[:len(final_recommendation)]
The vector file record is the record of vectors we created from the recordsdata earlier than. The question vector is a vector of the phrases being searched. The file dictionary was created earlier which makes use of file names for the keys and the recordsdata’ textual content as values. Similarities are computed, after which a rating is created favoring probably the most related items of data to the queried phrases being really useful first. Observe, what if there are higher than 3 suggestions? Incorporating components of Networks and Graph Principle will add an additional degree of computational profit to this method and create extra assured suggestions.
Web page Rank Principle
Let’s take a fast detour and go over the idea of web page rank. Don’t get me fallacious, cosine similarity is a robust computation for measuring the similarity between vectors, put incorporating web page rank into your advice algorithm permits for similarity comparisons throughout a number of vectors (information inside your database).
Web page rank was first designed by Larry Web page to rank web sites and measure their significance [1]. The fundamental concept is {that a} web site will be deemed “extra necessary” if extra web sites are linked to it. Drawing from this concept, a node on a graph will be ranked as extra necessary if there’s a lower within the distance of its edge to different nodes. The shorter the collective distance a node has in comparison with different nodes in a graph, the extra necessary mentioned node is.
At the moment we are going to use one variation of PageRank known as eigenvector centrality. Eigenvector centrality is like PageRank in that it measures the connections between nodes of a graph, assigning larger scores for stronger connections. Largest distinction? Eigenvector centrality will account for the significance of nodes linked to a given node to estimate how necessary that node is. That is synonymous with saying, an individual who is aware of plenty of necessary folks could also be essential themselves by these sturdy relationships. All-in-all, these two algorithms are very shut in the way in which they’re carried out.
For this database, after the vectors are computed, they are often positioned right into a graph the place their edge distance is set by their similarity to different vectors.
@staticmethod
def ranker(recommendation_val, file_vec_dict):
"""A perform which accepts a listing of recommendaton values and a dictionary
recordsdata wihin the databse and their vectors.Inputs:
reccomendation_val(record): A listing of suggestions discovered by cosine
similarity
file_vec_dic(dict): A dictionary of the filenames as keys and their
textual content in vectors because the values.
Returns:
ec_recommended(record): A listing of the highest 20% suggestions discovered utilizing the
eigenvector centrality algorithm.
"""
my_graph = nx.Graph()
for i in vary(len(recommendation_val)):
file_1 = recommendation_val[i]
for j in vary(len(recommendation_val)):
file_2 = recommendation_val[j]
if i != j:
#Calculate sim_score between two values (weight)
edge_dist = cosine_similarity([file_vec_dict[recommendation_val[i]]],[file_vec_dict[recommendation_val[j]]])
#add an edge from file 1 to file 2 with the burden
my_graph.add_edge(file_1, file_2, weight=edge_dist)
#Pagerank the graph ]
rec = nx.eigenvector_centrality(my_graph)
#Takes 20% of the values
ec_recommended = sorted(rec.objects(), key=lambda x:-x[1])[:int(np.round(len(rec)))]
return ec_recommended
Okay, now what? We’ve the suggestions created through the use of the cosine similarity between every information level within the database, and proposals computed by the eigenvector centrality algorithm. Which suggestions ought to we output? Each!
@staticmethod
def weighted_final_rank(sim_list,ec_recommended,final_recommendation):
"""A perform which accepts a listing of similiarity values discovered by
cosine similairty, suggestions discovered by eigenvector centrality,
and the ultimate suggestions produced by cosine similarity.Inputs:
sim_list(record): A listing of all the similarity values for the recordsdata
inside the database.
ec_recommended(record): A listing of the highest 20% suggestions discovered utilizing the
eigenvector centrality algorithm.
final_recommendation (record): A listing of the ultimate suggestions discovered
through the use of cosine similarity.
Returns:
weighted_final_recommend(record): A listing of the ultimate suggestions for
the recordsdata within the database.
"""
final_dict = dict()
for i in vary(len(sim_list)):
val = (.8*sim_list[final_recommendation.index(ec_recommendation[i][0])].squeeze()) + (.2 * ec_recommendation[i][1])
final_dict[ec_recommendation[i][0]] = val
weighted_final_recommend = sorted(final_dict.objects(), key=lambda x:-x[1])[:int(np.round(len(final_dict)))]
return weighted_final_recommend
The ultimate perform of this script will weigh the completely different suggestions produced by cosine similarity and eigenvector centrality. At present, 80% of the burden might be given to the suggestions produced by the cosine similarity suggestions, and 20% of the burden might be given to eigenvector centrality suggestions. The ultimate suggestions will be computed based mostly on these weights and aggregated collectively to provide suggestions which are consultant of all of the similarity computations within the system. The weights can simply be modified by the developer to replicate which batch of suggestions they really feel are extra necessary.
Let’s do a fast instance with this code. The paperwork inside my database are all within the codecs beforehand mentioned and pertain to completely different areas of machine studying. Extra paperwork within the database are associated to Generative Adversarial Networks (GANS), so I’d suspect these to be really useful first when “Generative Adversarial Community” is the question time period.
path = '/content material/drive/MyDrive/database/'
db = [f for f in glob.glob(path + '*')]research_documents, file_dictionary = dataprocessor.data_reader(db)
list_files, vectorizer, file_vec_dict = dataprocessor.database_processor(file_dictionary,research_documents)
question = 'Generative Adversarial Networks'
question = dataprocessor.preprocess(question)
question = dataprocessor.input_processor(question, vectorizer)
advice, sim_list = dataprocessor.recommender(list_files,question, file_dictionary)
ec_recommendation = dataprocessor.ranker(advice, file_vec_dict)
final_weighted_recommended = dataprocessor.weighted_final_rank(sim_list,ec_recommendation, advice)
print(final_weighted_recommended)
Working this block of code produces the next suggestions, together with the burden worth for every advice.
[(‘GAN_presentation.pptx’, 0.3411272882084124), (‘Using GANs to Augment UAV Data_V2.docx’, 0.16293615818015078), (‘GANS_DAY_1.docx’, 0.12546058188955278), (‘ml_pdf.pdf’, 0.10864164490536887)]
Let’s strive yet one more. What if I question “Machine Studying” ?
[(‘ml_pdf.pdf’, 0.31244922151487337), (‘GAN_presentation.pptx’, 0.18170070184645432), (‘GANS_DAY_1.docx’, 0.14825501243059303), (‘Using GANs to Augment UAV Data_V2.docx’, 0.1309153863914564)]
Aha! As anticipated, the primary doc really useful is an introductory transient to machine studying! I solely used 7 paperwork for this instance, and the extra paperwork added, the extra suggestions one will obtain!
At the moment we checked out how one can create a advice system for recordsdata you accumulate (particularly if you’re amassing analysis for a mission). The primary function of this method is that it goes one step additional in computing the cosine similarity of vectors by adopting the eigenvector centrality algorithm for extra concise, and higher suggestions. Do this out in the present day, and I hope it helps you get a greater understanding of how associated the items of knowledge you possess are.
If you happen to loved in the present day’s studying, PLEASE give me a comply with and let me know if there’s one other matter you prefer to me to discover! If you happen to wouldn’t have a Medium account, join by my hyperlink here (I obtain a small fee while you do that)! Moreover, add me on LinkedIn, or be at liberty to succeed in out! Thanks for studying!
Sources
[ad_2]
Source link